diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000000..a888486d38 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,17 @@ +{ + "name": "prowler-plugins", + "description": "Prowler Cloud Security for Claude Code", + "owner": { + "name": "Prowler", + "email": "support@prowler.com" + }, + "plugins": [ + { + "name": "prowler", + "source": "./claude_plugins/prowler", + "description": "Prowler for Claude Code — cloud security and compliance skills powered by the Prowler MCP server. Bundles compliance triage and remediation; more skills coming.", + "category": "security", + "homepage": "https://prowler.com" + } + ] +} diff --git a/.config/wt.toml b/.config/wt.toml index 4690e8d918..c217b4b6e7 100644 --- a/.config/wt.toml +++ b/.config/wt.toml @@ -2,20 +2,19 @@ # Runs automatically on `wt switch --create`. # Block 1: setup + copy gitignored env files (.envrc, ui/.env.local) -# from the primary worktree — patterns selected via .worktreeinclude. +# from the primary worktree - patterns selected via .worktreeinclude. [[pre-start]] skills = "./skills/setup.sh --claude" -python = "poetry env use python3.12" envs = "wt step copy-ignored" -# Block 2: install Python deps (requires `poetry env use` from block 1). +# Block 2: install Python deps (uv manages the venv on `uv sync`). [[pre-start]] -deps = "poetry install --with dev" +deps = "uv sync" -# Block 3: reminder — last visible output before `wt switch` returns. +# Block 3: reminder - last visible output before `wt switch` returns. # Hooks can't mutate the parent shell, so venv activation is manual. [[pre-start]] -reminder = "echo '>> Reminder: activate the venv in this shell with: eval $(poetry env activate)'" +reminder = "echo '>> Reminder: activate the venv in this shell with: source .venv/bin/activate'" # Background: pnpm install runs while you start working. # Tail logs via `wt config state logs`. diff --git a/.env b/.env index dd67da71ed..60c80b1363 100644 --- a/.env +++ b/.env @@ -145,7 +145,7 @@ SENTRY_RELEASE=local NEXT_PUBLIC_SENTRY_ENVIRONMENT=${SENTRY_ENVIRONMENT} #### Prowler release version #### -NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.26.0 +NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.29.0 # Social login credentials SOCIAL_GOOGLE_OAUTH_CALLBACK_URL="${AUTH_URL}/api/auth/callback/google" diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..ae9a9b4f68 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,15 @@ +# These are supported funding model platforms + +github: [prowler-cloud] +# patreon: # Replace with a single Patreon username +# open_collective: # Replace with a single Open Collective username +# ko_fi: # Replace with a single Ko-fi username +# tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +# community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +# liberapay: # Replace with a single Liberapay username +# issuehunt: # Replace with a single IssueHunt username +# lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +# polar: # Replace with a single Polar username +# buy_me_a_coffee: # Replace with a single Buy Me a Coffee username +# thanks_dev: # Replace with a single thanks.dev username +# custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/actions/osv-scanner/action.yml b/.github/actions/osv-scanner/action.yml new file mode 100644 index 0000000000..4bfb5993cb --- /dev/null +++ b/.github/actions/osv-scanner/action.yml @@ -0,0 +1,169 @@ +name: 'OSV-Scanner' +description: 'Install osv-scanner and scan a lockfile, failing on HIGH/CRITICAL/UNKNOWN severity findings. Posts/updates a PR comment with findings on pull_request events (requires pull-requests: write).' +author: 'Prowler' + +inputs: + lockfile: + description: 'Path to the lockfile to scan, relative to the repository root (e.g. uv.lock, api/uv.lock, ui/pnpm-lock.yaml).' + required: true + severity-levels: + description: 'Comma-separated severity levels that fail the scan. Default: HIGH,CRITICAL,UNKNOWN.' + required: false + default: 'HIGH,CRITICAL,UNKNOWN' + version: + description: 'osv-scanner release tag to install. When overriding, you MUST also override binary-sha256.' + required: false + default: 'v2.3.8' + binary-sha256: + description: 'Expected SHA256 of osv-scanner_linux_amd64 for the given version. Default tracks v2.3.8. See https://github.com/google/osv-scanner/releases/download//osv-scanner_SHA256SUMS.' + required: false + default: 'bc98e15319ed0d515e3f9235287ba53cdc5535d576d24fd573978ecfe9ab92dc' + post-pr-comment: + description: 'Post or update a PR comment with the scan report. Only effective on pull_request events. Requires pull-requests: write permission on the caller job.' + required: false + default: 'true' + +runs: + using: 'composite' + steps: + - name: Install osv-scanner + shell: bash + env: + OSV_SCANNER_VERSION: ${{ inputs.version }} + # Download the binary AND the published SHA256SUMS file, then verify the + # binary checksum against the upstream-signed manifest. Aborts on mismatch. + run: | + set -euo pipefail + if command -v osv-scanner >/dev/null 2>&1; then + INSTALLED="$(osv-scanner --version 2>&1 | awk '/scanner version/ {print $NF; exit}')" + if [ "v${INSTALLED}" = "${OSV_SCANNER_VERSION}" ]; then + echo "osv-scanner ${OSV_SCANNER_VERSION} already installed." + exit 0 + fi + fi + BASE="https://github.com/google/osv-scanner/releases/download/${OSV_SCANNER_VERSION}" + BIN_NAME="osv-scanner_linux_amd64" + curl -fSL --retry 3 "${BASE}/${BIN_NAME}" -o "${RUNNER_TEMP}/${BIN_NAME}" + curl -fSL --retry 3 "${BASE}/osv-scanner_SHA256SUMS" -o "${RUNNER_TEMP}/osv-scanner_SHA256SUMS" + (cd "${RUNNER_TEMP}" && sha256sum --check --ignore-missing osv-scanner_SHA256SUMS) + chmod +x "${RUNNER_TEMP}/${BIN_NAME}" + sudo mv "${RUNNER_TEMP}/${BIN_NAME}" /usr/local/bin/osv-scanner + rm -f "${RUNNER_TEMP}/osv-scanner_SHA256SUMS" + osv-scanner --version + + - name: Run osv-scanner + id: scan + shell: bash + working-directory: ${{ github.workspace }} + env: + OSV_LOCKFILE: ${{ inputs.lockfile }} + OSV_SEVERITY_LEVELS: ${{ inputs.severity-levels }} + OSV_REPORT_FILE: ${{ runner.temp }}/osv-scanner-findings.json + # Per-vulnerability ignores (reason + expiry) live in osv-scanner.toml at the repo root, if present. + # Severity filter is enforced in the wrapper via OSV_SEVERITY_LEVELS. + # `continue-on-error: true` lets the PR-comment step run even when findings exist; + # the gate step below re-fails the job from the wrapper exit code. + continue-on-error: true + run: ./.github/scripts/osv-scan.sh --lockfile="${OSV_LOCKFILE}" + + - name: Post osv-scanner report on PR + if: >- + always() + && inputs.post-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: + OSV_REPORT_FILE: ${{ runner.temp }}/osv-scanner-findings.json + OSV_LOCKFILE: ${{ inputs.lockfile }} + OSV_SEVERITY_LEVELS: ${{ inputs.severity-levels }} + with: + script: | + const fs = require('fs'); + const lockfile = process.env.OSV_LOCKFILE; + const severityLevels = process.env.OSV_SEVERITY_LEVELS; + const reportFile = process.env.OSV_REPORT_FILE; + const marker = ``; + const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + + let findings = []; + if (fs.existsSync(reportFile)) { + try { + findings = JSON.parse(fs.readFileSync(reportFile, 'utf8')); + } catch (err) { + core.warning(`Could not parse ${reportFile}: ${err.message}`); + return; + } + } + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + const existing = comments.find(c => c.body?.includes(marker)); + + if (findings.length === 0) { + if (existing) { + await github.rest.issues.deleteComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + }); + core.info(`Deleted stale osv-scanner comment for ${lockfile}.`); + } else { + core.info(`No findings and no stale comment for ${lockfile}.`); + } + return; + } + + const sevIcon = (s) => ({ + CRITICAL: '🔴', HIGH: '🟠', MEDIUM: '🟡', LOW: '🟢', UNKNOWN: '⚪', + }[s] || '⚪'); + const escape = (s) => String(s ?? '').replace(/\|/g, '\\|').replace(/\n/g, ' '); + const rows = findings.map(f => + `| ${sevIcon(f.severity)} ${f.severity}${f.score ? ` (${f.score})` : ''} | \`${escape(f.id)}\` | \`${escape(f.ecosystem)}/${escape(f.package)}\` | \`${escape(f.version)}\` | ${escape(f.summary || '(no summary)')} |` + ); + + const body = [ + marker, + `## 🔒 osv-scanner: ${findings.length} finding(s) in \`${lockfile}\``, + '', + `Severity gate: \`${severityLevels}\``, + '', + '| Severity | ID | Package | Version | Summary |', + '|----------|----|---------|---------|---------|', + ...rows, + '', + `To accept a finding, add an \`[[IgnoredVulns]]\` entry to \`osv-scanner.toml\` at the repo root with a reason and \`ignoreUntil\`.`, + '', + `[View run](${runUrl})`, + ].join('\n'); + + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + core.info(`Updated osv-scanner comment for ${lockfile}.`); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); + core.info(`Posted new osv-scanner comment for ${lockfile}.`); + } + + - name: Enforce osv-scanner severity gate + shell: bash + env: + SCAN_OUTCOME: ${{ steps.scan.outcome }} + run: | + if [ "${SCAN_OUTCOME}" != "success" ]; then + echo "osv-scanner gate: scan reported findings (outcome=${SCAN_OUTCOME})" >&2 + exit 1 + fi diff --git a/.github/actions/setup-python-poetry/action.yml b/.github/actions/setup-python-uv/action.yml similarity index 55% rename from .github/actions/setup-python-poetry/action.yml rename to .github/actions/setup-python-uv/action.yml index fd6c082c7b..e23388d86e 100644 --- a/.github/actions/setup-python-poetry/action.yml +++ b/.github/actions/setup-python-uv/action.yml @@ -1,5 +1,5 @@ -name: 'Setup Python with Poetry' -description: 'Setup Python environment with Poetry and install dependencies' +name: 'Setup Python with uv' +description: 'Setup Python environment with uv and install dependencies' author: 'Prowler' inputs: @@ -7,23 +7,15 @@ inputs: description: 'Python version to use' required: true working-directory: - description: 'Working directory for Poetry' + description: 'Working directory for uv' required: false default: '.' - poetry-version: - description: 'Poetry version to install' + uv-version: + description: 'uv version to install' required: false - default: '2.3.4' + default: '0.11.14' install-dependencies: - description: 'Install Python dependencies with Poetry' - required: false - default: 'true' - update-lock: - description: 'Run `poetry lock` during setup. Only enable when a prior step mutates pyproject.toml (e.g. API `@master` VCS rewrite). Default: false.' - required: false - default: 'false' - enable-cache: - description: 'Whether to enable Poetry dependency caching via actions/setup-python' + description: 'Install Python dependencies with uv' required: false default: 'true' @@ -47,54 +39,52 @@ runs: sed -i "s|\(git+https://github.com/prowler-cloud/prowler[^@]*\)@master|\1@$BRANCH_NAME|g" pyproject.toml fi - - name: Install poetry - shell: bash - run: | - python -m pip install --upgrade pip - pipx install poetry==${INPUTS_POETRY_VERSION} - env: - INPUTS_POETRY_VERSION: ${{ inputs.poetry-version }} - - - name: Update poetry.lock with latest Prowler commit + - name: Update uv.lock with latest Prowler commit if: github.repository_owner == 'prowler-cloud' && github.repository != 'prowler-cloud/prowler' shell: bash working-directory: ${{ inputs.working-directory }} run: | LATEST_COMMIT=$(curl -s "https://api.github.com/repos/prowler-cloud/prowler/commits/master" | jq -r '.sha') echo "Latest commit hash: $LATEST_COMMIT" - sed -i '/url = "https:\/\/github\.com\/prowler-cloud\/prowler\.git"/,/resolved_reference = / { - s/resolved_reference = "[a-f0-9]\{40\}"/resolved_reference = "'"$LATEST_COMMIT"'"/ - }' poetry.lock - echo "Updated resolved_reference:" - grep -A2 -B2 "resolved_reference" poetry.lock + sed -i "s|\(git = \"https://github\.com/prowler-cloud/prowler\.git?rev=master\)#[a-f0-9]\{40\}\"|\1#${LATEST_COMMIT}\"|g" uv.lock + echo "Updated uv.lock entry:" + grep "prowler-cloud/prowler" uv.lock - - name: Update poetry.lock (prowler repo only) - if: github.repository == 'prowler-cloud/prowler' && inputs.update-lock == 'true' + - name: Update uv.lock SDK commit (prowler repo on push) + if: github.event_name == 'push' && github.ref == 'refs/heads/master' && github.repository == 'prowler-cloud/prowler' shell: bash working-directory: ${{ inputs.working-directory }} - run: poetry lock + run: | + LATEST_COMMIT=$(curl -s "https://api.github.com/repos/prowler-cloud/prowler/commits/master" | jq -r '.sha') + echo "Latest commit hash: $LATEST_COMMIT" + sed -i "s|\(git = \"https://github\.com/prowler-cloud/prowler\.git?rev=master\)#[a-f0-9]\{40\}\"|\1#${LATEST_COMMIT}\"|g" uv.lock + echo "Updated uv.lock entry:" + grep "prowler-cloud/prowler" uv.lock + + - name: Install uv + shell: bash + env: + UV_VERSION: ${{ inputs.uv-version }} + run: pip install --no-cache-dir --upgrade pip && pip install --no-cache-dir "uv==${UV_VERSION}" - name: Set up Python ${{ inputs.python-version }} uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: ${{ inputs.python-version }} - # Disable cache when callers skip dependency install: Poetry 2.3.4 creates - # the venv in a path setup-python can't hash, breaking the post-step save-cache. - cache: ${{ inputs.enable-cache == 'true' && 'poetry' || '' }} - cache-dependency-path: ${{ inputs.enable-cache == 'true' && format('{0}/poetry.lock', inputs.working-directory) || '' }} + cache: 'pip' - name: Install Python dependencies if: inputs.install-dependencies == 'true' shell: bash working-directory: ${{ inputs.working-directory }} run: | - poetry install --no-root - poetry run pip list + uv sync --no-install-project + uv run pip list - name: Update Prowler Cloud API Client if: github.repository_owner == 'prowler-cloud' && github.repository != 'prowler-cloud/prowler' shell: bash working-directory: ${{ inputs.working-directory }} run: | - poetry remove prowler-cloud-api-client - poetry add ./prowler-cloud-api-client + uv remove prowler-cloud-api-client + uv add ./prowler-cloud-api-client diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 953c0742f6..6a24af5fce 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,17 +6,17 @@ version: 2 updates: # v5 - - package-ecosystem: "pip" - directory: "/" - schedule: - interval: "monthly" - open-pull-requests-limit: 25 - target-branch: master - labels: - - "dependencies" - - "pip" - cooldown: - default-days: 7 + # - package-ecosystem: "pip" + # directory: "/" + # schedule: + # interval: "monthly" + # open-pull-requests-limit: 25 + # target-branch: master + # labels: + # - "dependencies" + # - "pip" + # cooldown: + # default-days: 7 # Dependabot Updates are temporary disabled - 2025/03/19 # - package-ecosystem: "pip" @@ -66,17 +66,17 @@ updates: cooldown: default-days: 7 - - package-ecosystem: "pre-commit" - directory: "/" - schedule: - interval: "monthly" - open-pull-requests-limit: 25 - target-branch: master - labels: - - "dependencies" - - "pre-commit" - cooldown: - default-days: 7 + # - package-ecosystem: "pre-commit" + # directory: "/" + # schedule: + # interval: "monthly" + # open-pull-requests-limit: 25 + # target-branch: master + # labels: + # - "dependencies" + # - "pre-commit" + # cooldown: + # default-days: 7 # Dependabot Updates are temporary disabled - 2025/04/15 # v4.6 diff --git a/.github/labeler.yml b/.github/labeler.yml index 49ec92e0d5..1b0dfd0400 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -72,6 +72,11 @@ provider/vercel: - any-glob-to-any-file: "prowler/providers/vercel/**" - any-glob-to-any-file: "tests/providers/vercel/**" +provider/okta: + - changed-files: + - any-glob-to-any-file: "prowler/providers/okta/**" + - any-glob-to-any-file: "tests/providers/okta/**" + github_actions: - changed-files: - any-glob-to-any-file: ".github/workflows/*" @@ -109,6 +114,8 @@ mutelist: - any-glob-to-any-file: "tests/providers/googleworkspace/lib/mutelist/**" - any-glob-to-any-file: "prowler/providers/vercel/lib/mutelist/**" - any-glob-to-any-file: "tests/providers/vercel/lib/mutelist/**" + - any-glob-to-any-file: "prowler/providers/okta/lib/mutelist/**" + - any-glob-to-any-file: "tests/providers/okta/lib/mutelist/**" integration/s3: - changed-files: diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 28365d4acb..f2065a3db0 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -36,6 +36,7 @@ Please add a detailed description of how to review this PR. #### UI - [ ] All issue/task requirements work as expected on the UI +- [ ] If this PR adds or updates npm dependencies, include package-health evidence (maintenance, popularity, known vulnerabilities, license, release age) and explain why existing/native alternatives are insufficient. - [ ] Screenshots/Video of the functionality flow (if applicable) - Mobile (X < 640px) - [ ] Screenshots/Video of the functionality flow (if applicable) - Table (640px > X < 1024px) - [ ] Screenshots/Video of the functionality flow (if applicable) - Desktop (X > 1024px) @@ -48,7 +49,7 @@ Please add a detailed description of how to review this PR. - [ ] Performance test results (if applicable) - [ ] Any other relevant evidence of the implementation (if applicable) - [ ] Verify if API specs need to be regenerated. -- [ ] Check if version updates are required (e.g., specs, Poetry, etc.). +- [ ] Check if version updates are required (e.g., specs, uv, etc.). - [ ] Ensure new entries are added to [CHANGELOG.md](https://github.com/prowler-cloud/prowler/blob/master/api/CHANGELOG.md), if applicable. ### License diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 0000000000..be910a7f4e --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,140 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:best-practices", + ":enablePreCommit", + ":semanticCommits", + ":enableVulnerabilityAlertsWithLabel(security)", + "docker:enableMajor", + "helpers:pinGitHubActionDigestsToSemver", + "helpers:disableTypesNodeMajor", + "security:openssf-scorecard", + "customManagers:githubActionsVersions", + "customManagers:dockerfileVersions" + ], + "timezone": "Europe/Madrid", + "baseBranchPatterns": [ + "master" + ], + "labels": [ + "dependencies" + ], + "dependencyDashboardTitle": "Dependency Dashboard", + "prConcurrentLimit": 20, + "prHourlyLimit": 10, + "vulnerabilityAlerts": { + "prHourlyLimit": 0, + "prConcurrentLimit": 0 + }, + "configMigration": true, + "minimumReleaseAge": "7 days", + "rangeStrategy": "pin", + "packageRules": [ + { + "description": "Patches: 1st of every month, Madrid overnight window (22:00-06:00)", + "matchUpdateTypes": [ + "patch" + ], + "schedule": [ + "* 22-23,0-5 1 * *" + ], + "enabled": false + }, + { + "description": "Minors: 8th of every 3 months, Madrid overnight window (22:00-06:00)", + "matchUpdateTypes": [ + "minor" + ], + "schedule": [ + "* 22-23,0-5 8 */3 *" + ], + "enabled": false + }, + { + "description": "Majors: 15th of every 3 months, Madrid overnight window", + "matchUpdateTypes": [ + "major" + ], + "schedule": [ + "* 22-23,0-5 15 */3 *" + ], + "enabled": false + }, + { + "description": "GitHub Actions - single grouped PR, no changelog, scope=ci", + "matchManagers": [ + "github-actions" + ], + "groupName": "github-actions", + "semanticCommitScope": "ci", + "addLabels": [ + "no-changelog" + ] + }, + { + "description": "Docker images - single grouped PR, no changelog, scope=docker", + "matchManagers": [ + "dockerfile", + "docker-compose" + ], + "groupName": "docker", + "semanticCommitScope": "docker", + "addLabels": [ + "no-changelog" + ] + }, + { + "description": "Pre-commit hooks - single grouped PR, scope=pre-commit", + "matchManagers": [ + "pre-commit" + ], + "groupName": "pre-commit hooks", + "semanticCommitScope": "pre-commit", + "addLabels": [ + "no-changelog" + ] + }, + { + "description": "UI - scope=ui", + "matchFileNames": [ + "ui/**" + ], + "semanticCommitScope": "ui" + }, + { + "description": "API - scope=api", + "matchFileNames": [ + "api/**" + ], + "semanticCommitScope": "api" + }, + { + "description": "MCP server - scope=mcp", + "matchFileNames": [ + "mcp_server/**" + ], + "semanticCommitScope": "mcp" + }, + { + "description": "Python SDK (root) - scope=sdk", + "matchFileNames": [ + "pyproject.toml", + "poetry.lock", + "util/prowler-bulk-provisioning/**" + ], + "semanticCommitScope": "sdk" + }, + { + "description": "UI devDependencies - no changelog", + "matchFileNames": [ + "ui/**" + ], + "matchDepTypes": [ + "devDependencies" + ], + "addLabels": [ + "no-changelog" + ] + } + ] +} diff --git a/.github/scripts/osv-scan.sh b/.github/scripts/osv-scan.sh new file mode 100755 index 0000000000..d9c2e1a901 --- /dev/null +++ b/.github/scripts/osv-scan.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +# Run osv-scanner and fail when findings match the configured severity levels. +# +# Replaces `safety check --policy-file .safety-policy.yml`. Used by: +# - .github/actions/osv-scanner/action.yml (composite action) +# - .github/workflows/api-security.yml, sdk-security.yml, ui-security.yml +# +# Severity levels (comma-separated) are read from OSV_SEVERITY_LEVELS. +# Default: HIGH,CRITICAL,UNKNOWN — preserves prior .safety-policy.yml policy +# (ignore-cvss-severity-below: 7 + ignore-cvss-unknown-severity: False). +# osv-scanner has no native CVSS threshold (google/osv-scanner#1400, closed +# not-planned). Severity is derived from $group.max_severity (numeric CVSS +# score string) which osv-scanner emits per group. +# +# CVSS v3 score → categorical label: +# CRITICAL >= 9.0 +# HIGH >= 7.0 +# MEDIUM >= 4.0 +# LOW > 0.0 +# UNKNOWN no score available +# +# Per-vulnerability ignores (with reason + expiry) live in osv-scanner.toml at +# the repo root, if it exists. Without that file, osv-scanner uses defaults. +# +# Usage: +# osv-scan.sh [osv-scanner pass-through args...] +# Examples: +# osv-scan.sh --lockfile=uv.lock +# osv-scan.sh --recursive . +# OSV_SEVERITY_LEVELS=CRITICAL osv-scan.sh --lockfile=uv.lock + +set -euo pipefail + +ROOT="$(git rev-parse --show-toplevel)" +CONFIG="${ROOT}/osv-scanner.toml" +SEVERITY_LEVELS="${OSV_SEVERITY_LEVELS:-HIGH,CRITICAL,UNKNOWN}" + +for bin in osv-scanner jq; do + if ! command -v "${bin}" >/dev/null 2>&1; then + echo "error: ${bin} not found in PATH" >&2 + exit 2 + fi +done + +SCAN_ARGS=() +if [ -f "${CONFIG}" ]; then + SCAN_ARGS+=(--config="${CONFIG}") +fi + +# Exit codes: 0=clean, 1=findings, 127=no supported files, 128=internal error. +STDERR="$(mktemp)" +trap 'rm -f "${STDERR}"' EXIT + +set +e +OUTPUT="$(osv-scanner scan source "${SCAN_ARGS[@]}" --format=json "$@" 2>"${STDERR}")" +RC=$? +set -e + +case "${RC}" in + 0|1) ;; + 127) echo "osv-scanner: no supported lockfiles in scan target"; exit 0 ;; + *) + echo "osv-scanner: exited with code ${RC}" >&2 + [ -s "${STDERR}" ] && cat "${STDERR}" >&2 + exit "${RC}" + ;; +esac + +# Build a JSON array of normalized severity levels for jq. +SEVERITY_JSON="$(printf '%s' "${SEVERITY_LEVELS}" | jq -Rc ' + split(",") | map(ascii_upcase | sub("^\\s+"; "") | sub("\\s+$"; "")) +')" + +# Walk each vulnerability, look up its group's max_severity (numeric CVSS), +# map to a categorical label, then filter by OSV_SEVERITY_LEVELS. +FINDINGS="$(printf '%s' "${OUTPUT}" | jq --argjson sevs "${SEVERITY_JSON}" ' + [ .results[]?.packages[]? + | . as $pkg + | ($pkg.groups // []) as $groups + | ($pkg.vulnerabilities // [])[] + | . as $vuln + | ([ $groups[] | select((.ids // []) | index($vuln.id)) ][0] // {}) as $group + | (($group.max_severity // "") | tonumber? // null) as $score + | (if $score == null then "UNKNOWN" + elif $score >= 9.0 then "CRITICAL" + elif $score >= 7.0 then "HIGH" + elif $score >= 4.0 then "MEDIUM" + elif $score > 0 then "LOW" + else "UNKNOWN" + end) as $label + | { + id: $vuln.id, + severity: $label, + score: $score, + summary: ($vuln.summary // null), + package: $pkg.package.name, + version: $pkg.package.version, + ecosystem: $pkg.package.ecosystem + } + | select(.severity as $s | $sevs | any(. == $s)) + ] +')" + +COUNT="$(printf '%s' "${FINDINGS}" | jq 'length')" + +# Write the findings JSON to OSV_REPORT_FILE so callers (e.g. the composite +# action's PR-comment step) can consume the same data the gate decision uses. +if [ -n "${OSV_REPORT_FILE:-}" ]; then + printf '%s' "${FINDINGS}" > "${OSV_REPORT_FILE}" +fi + +if [ "${COUNT}" -gt 0 ]; then + echo "osv-scanner: ${COUNT} finding(s) at severity ${SEVERITY_LEVELS}" + printf '%s' "${FINDINGS}" | jq -r ' + .[] | " [\(.severity)\(if .score then " \(.score)" else "" end)] \(.id) \(.ecosystem)/\(.package)@\(.version) — \(.summary // "(no summary)")" + ' + echo + echo "To accept a finding, create osv-scanner.toml at the repo root with a reason and ignoreUntil." + exit 1 +fi + +echo "osv-scanner: no findings at severity levels: ${SEVERITY_LEVELS}" diff --git a/.github/workflows/api-bump-version.yml b/.github/workflows/api-bump-version.yml deleted file mode 100644 index 6abdc8c84c..0000000000 --- a/.github/workflows/api-bump-version.yml +++ /dev/null @@ -1,291 +0,0 @@ -name: 'API: Bump Version' - -on: - release: - types: - - 'published' - -concurrency: - group: ${{ github.workflow }}-${{ github.event.release.tag_name }} - cancel-in-progress: false - -env: - PROWLER_VERSION: ${{ github.event.release.tag_name }} - BASE_BRANCH: master - -permissions: {} - -jobs: - detect-release-type: - runs-on: ubuntu-latest - timeout-minutes: 5 - permissions: - contents: read - outputs: - is_minor: ${{ steps.detect.outputs.is_minor }} - is_patch: ${{ steps.detect.outputs.is_patch }} - major_version: ${{ steps.detect.outputs.major_version }} - minor_version: ${{ steps.detect.outputs.minor_version }} - patch_version: ${{ steps.detect.outputs.patch_version }} - current_api_version: ${{ steps.get_api_version.outputs.current_api_version }} - steps: - - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 - with: - egress-policy: audit - - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: Get current API version - id: get_api_version - run: | - CURRENT_API_VERSION=$(grep -oP '^version = "\K[^"]+' api/pyproject.toml) - echo "current_api_version=${CURRENT_API_VERSION}" >> "${GITHUB_OUTPUT}" - echo "Current API version: $CURRENT_API_VERSION" - - - name: Detect release type and parse version - id: detect - run: | - if [[ $PROWLER_VERSION =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then - MAJOR_VERSION=${BASH_REMATCH[1]} - MINOR_VERSION=${BASH_REMATCH[2]} - PATCH_VERSION=${BASH_REMATCH[3]} - - echo "major_version=${MAJOR_VERSION}" >> "${GITHUB_OUTPUT}" - echo "minor_version=${MINOR_VERSION}" >> "${GITHUB_OUTPUT}" - echo "patch_version=${PATCH_VERSION}" >> "${GITHUB_OUTPUT}" - - if (( MAJOR_VERSION != 5 )); then - echo "::error::Releasing another Prowler major version, aborting..." - exit 1 - fi - - if (( PATCH_VERSION == 0 )); then - echo "is_minor=true" >> "${GITHUB_OUTPUT}" - echo "is_patch=false" >> "${GITHUB_OUTPUT}" - echo "✓ Minor release detected: $PROWLER_VERSION" - else - echo "is_minor=false" >> "${GITHUB_OUTPUT}" - echo "is_patch=true" >> "${GITHUB_OUTPUT}" - echo "✓ Patch release detected: $PROWLER_VERSION" - fi - else - echo "::error::Invalid version syntax: '$PROWLER_VERSION' (must be X.Y.Z)" - exit 1 - fi - - bump-minor-version: - needs: detect-release-type - if: needs.detect-release-type.outputs.is_minor == 'true' - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - contents: read - pull-requests: write - steps: - - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 - with: - egress-policy: audit - - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: Calculate next API minor version - run: | - MAJOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION} - MINOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION} - CURRENT_API_VERSION="${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_CURRENT_API_VERSION}" - - # API version follows Prowler minor + 1 - # For Prowler 5.17.0 -> API 1.18.0 - # For next master (Prowler 5.18.0) -> API 1.19.0 - NEXT_API_VERSION=1.$((MINOR_VERSION + 2)).0 - - echo "CURRENT_API_VERSION=${CURRENT_API_VERSION}" >> "${GITHUB_ENV}" - echo "NEXT_API_VERSION=${NEXT_API_VERSION}" >> "${GITHUB_ENV}" - - echo "Prowler release version: ${MAJOR_VERSION}.${MINOR_VERSION}.0" - echo "Current API version: $CURRENT_API_VERSION" - echo "Next API minor version (for master): $NEXT_API_VERSION" - env: - NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION: ${{ needs.detect-release-type.outputs.major_version }} - NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION: ${{ needs.detect-release-type.outputs.minor_version }} - NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_CURRENT_API_VERSION: ${{ needs.detect-release-type.outputs.current_api_version }} - - - name: Bump API versions in files for master - run: | - set -e - - sed -i "s|version = \"${CURRENT_API_VERSION}\"|version = \"${NEXT_API_VERSION}\"|" api/pyproject.toml - sed -i "s|spectacular_settings.VERSION = \"${CURRENT_API_VERSION}\"|spectacular_settings.VERSION = \"${NEXT_API_VERSION}\"|" api/src/backend/api/v1/views.py - sed -i "s| version: ${CURRENT_API_VERSION}| version: ${NEXT_API_VERSION}|" api/src/backend/api/specs/v1.yaml - - echo "Files modified:" - git --no-pager diff - - - name: Create PR for next API minor version to master - uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 - with: - author: prowler-bot <179230569+prowler-bot@users.noreply.github.com> - token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} - base: master - commit-message: 'chore(api): Bump version to v${{ env.NEXT_API_VERSION }}' - branch: api-version-bump-to-v${{ env.NEXT_API_VERSION }} - title: 'chore(api): Bump version to v${{ env.NEXT_API_VERSION }}' - labels: no-changelog,skip-sync - body: | - ### Description - - Bump Prowler API version to v${{ env.NEXT_API_VERSION }} after releasing Prowler v${{ env.PROWLER_VERSION }}. - - ### License - - By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. - - - name: Checkout version branch - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: v${{ needs.detect-release-type.outputs.major_version }}.${{ needs.detect-release-type.outputs.minor_version }} - persist-credentials: false - - - name: Calculate first API patch version - run: | - MAJOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION} - MINOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION} - CURRENT_API_VERSION="${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_CURRENT_API_VERSION}" - VERSION_BRANCH=v${MAJOR_VERSION}.${MINOR_VERSION} - - # API version follows Prowler minor + 1 - # For Prowler 5.17.0 release -> version branch v5.17 should have API 1.18.1 - FIRST_API_PATCH_VERSION=1.$((MINOR_VERSION + 1)).1 - - echo "CURRENT_API_VERSION=${CURRENT_API_VERSION}" >> "${GITHUB_ENV}" - echo "FIRST_API_PATCH_VERSION=${FIRST_API_PATCH_VERSION}" >> "${GITHUB_ENV}" - echo "VERSION_BRANCH=${VERSION_BRANCH}" >> "${GITHUB_ENV}" - - echo "Prowler release version: ${MAJOR_VERSION}.${MINOR_VERSION}.0" - echo "First API patch version (for ${VERSION_BRANCH}): $FIRST_API_PATCH_VERSION" - echo "Version branch: $VERSION_BRANCH" - env: - NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION: ${{ needs.detect-release-type.outputs.major_version }} - NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION: ${{ needs.detect-release-type.outputs.minor_version }} - NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_CURRENT_API_VERSION: ${{ needs.detect-release-type.outputs.current_api_version }} - - - name: Bump API versions in files for version branch - run: | - set -e - - sed -i "s|version = \"${CURRENT_API_VERSION}\"|version = \"${FIRST_API_PATCH_VERSION}\"|" api/pyproject.toml - sed -i "s|spectacular_settings.VERSION = \"${CURRENT_API_VERSION}\"|spectacular_settings.VERSION = \"${FIRST_API_PATCH_VERSION}\"|" api/src/backend/api/v1/views.py - sed -i "s| version: ${CURRENT_API_VERSION}| version: ${FIRST_API_PATCH_VERSION}|" api/src/backend/api/specs/v1.yaml - - echo "Files modified:" - git --no-pager diff - - - name: Create PR for first API patch version to version branch - uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 - with: - author: prowler-bot <179230569+prowler-bot@users.noreply.github.com> - token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} - base: ${{ env.VERSION_BRANCH }} - commit-message: 'chore(api): Bump version to v${{ env.FIRST_API_PATCH_VERSION }}' - branch: api-version-bump-to-v${{ env.FIRST_API_PATCH_VERSION }} - title: 'chore(api): Bump version to v${{ env.FIRST_API_PATCH_VERSION }}' - labels: no-changelog,skip-sync - body: | - ### Description - - Bump Prowler API version to v${{ env.FIRST_API_PATCH_VERSION }} in version branch after releasing Prowler v${{ env.PROWLER_VERSION }}. - - ### License - - By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. - - bump-patch-version: - needs: detect-release-type - if: needs.detect-release-type.outputs.is_patch == 'true' - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - contents: read - pull-requests: write - steps: - - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 - with: - egress-policy: audit - - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: Calculate next API patch version - run: | - MAJOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION} - MINOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION} - PATCH_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_PATCH_VERSION} - CURRENT_API_VERSION="${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_CURRENT_API_VERSION}" - VERSION_BRANCH=v${MAJOR_VERSION}.${MINOR_VERSION} - - # Extract current API patch to increment it - if [[ $CURRENT_API_VERSION =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then - API_PATCH=${BASH_REMATCH[3]} - - # API version follows Prowler minor + 1 - # Keep same API minor (based on Prowler minor), increment patch - NEXT_API_PATCH_VERSION=1.$((MINOR_VERSION + 1)).$((API_PATCH + 1)) - - echo "CURRENT_API_VERSION=${CURRENT_API_VERSION}" >> "${GITHUB_ENV}" - echo "NEXT_API_PATCH_VERSION=${NEXT_API_PATCH_VERSION}" >> "${GITHUB_ENV}" - echo "VERSION_BRANCH=${VERSION_BRANCH}" >> "${GITHUB_ENV}" - - echo "Prowler release version: ${MAJOR_VERSION}.${MINOR_VERSION}.${PATCH_VERSION}" - echo "Current API version: $CURRENT_API_VERSION" - echo "Next API patch version: $NEXT_API_PATCH_VERSION" - echo "Target branch: $VERSION_BRANCH" - else - echo "::error::Invalid API version format: $CURRENT_API_VERSION" - exit 1 - fi - env: - NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION: ${{ needs.detect-release-type.outputs.major_version }} - NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION: ${{ needs.detect-release-type.outputs.minor_version }} - NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_PATCH_VERSION: ${{ needs.detect-release-type.outputs.patch_version }} - NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_CURRENT_API_VERSION: ${{ needs.detect-release-type.outputs.current_api_version }} - - - name: Bump API versions in files for version branch - run: | - set -e - - sed -i "s|version = \"${CURRENT_API_VERSION}\"|version = \"${NEXT_API_PATCH_VERSION}\"|" api/pyproject.toml - sed -i "s|spectacular_settings.VERSION = \"${CURRENT_API_VERSION}\"|spectacular_settings.VERSION = \"${NEXT_API_PATCH_VERSION}\"|" api/src/backend/api/v1/views.py - sed -i "s| version: ${CURRENT_API_VERSION}| version: ${NEXT_API_PATCH_VERSION}|" api/src/backend/api/specs/v1.yaml - - echo "Files modified:" - git --no-pager diff - - - name: Create PR for next API patch version to version branch - uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 - with: - author: prowler-bot <179230569+prowler-bot@users.noreply.github.com> - token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} - base: ${{ env.VERSION_BRANCH }} - commit-message: 'chore(api): Bump version to v${{ env.NEXT_API_PATCH_VERSION }}' - branch: api-version-bump-to-v${{ env.NEXT_API_PATCH_VERSION }} - title: 'chore(api): Bump version to v${{ env.NEXT_API_PATCH_VERSION }}' - labels: no-changelog,skip-sync - body: | - ### Description - - Bump Prowler API version to v${{ env.NEXT_API_PATCH_VERSION }} after releasing Prowler v${{ env.PROWLER_VERSION }}. - - ### License - - By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. diff --git a/.github/workflows/api-code-quality.yml b/.github/workflows/api-code-quality.yml index 1724e51d70..362f0dbbbf 100644 --- a/.github/workflows/api-code-quality.yml +++ b/.github/workflows/api-code-quality.yml @@ -35,7 +35,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block allowed-endpoints: > @@ -43,6 +43,7 @@ jobs: pypi.org:443 files.pythonhosted.org:443 api.github.com:443 + raw.githubusercontent.com:443 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -52,7 +53,7 @@ jobs: - name: Check for API changes id: check-changes - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 with: files: | api/** @@ -63,26 +64,25 @@ jobs: api/CHANGELOG.md api/AGENTS.md - - name: Setup Python with Poetry + - name: Setup Python with uv if: steps.check-changes.outputs.any_changed == 'true' - uses: ./.github/actions/setup-python-poetry + uses: ./.github/actions/setup-python-uv with: python-version: ${{ matrix.python-version }} working-directory: ./api - update-lock: 'true' - - name: Poetry check + - name: uv lock check if: steps.check-changes.outputs.any_changed == 'true' - run: poetry check --lock + run: uv lock --check - name: Ruff lint if: steps.check-changes.outputs.any_changed == 'true' - run: poetry run ruff check . --exclude contrib + run: uv run ruff check . --exclude contrib - name: Ruff format if: steps.check-changes.outputs.any_changed == 'true' - run: poetry run ruff format --check . --exclude contrib + run: uv run ruff format --check . --exclude contrib - name: Pylint if: steps.check-changes.outputs.any_changed == 'true' - run: poetry run pylint --disable=W,C,R,E -j 0 -rn -sn src/ + run: uv run pylint --disable=W,C,R,E -j 0 -rn -sn src/ diff --git a/.github/workflows/api-codeql.yml b/.github/workflows/api-codeql.yml index 915a5b2722..634c63cb11 100644 --- a/.github/workflows/api-codeql.yml +++ b/.github/workflows/api-codeql.yml @@ -44,7 +44,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block allowed-endpoints: > @@ -61,12 +61,12 @@ jobs: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4 + uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 with: languages: ${{ matrix.language }} config-file: ./.github/codeql/api-codeql-config.yml - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4 + uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 with: category: '/language:${{ matrix.language }}' diff --git a/.github/workflows/api-container-build-push.yml b/.github/workflows/api-container-build-push.yml index e6403ed689..d4835db7d5 100644 --- a/.github/workflows/api-container-build-push.yml +++ b/.github/workflows/api-container-build-push.yml @@ -46,7 +46,7 @@ jobs: contents: read steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block @@ -65,7 +65,7 @@ jobs: contents: read steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: audit @@ -108,7 +108,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block allowed-endpoints: > @@ -122,6 +122,7 @@ jobs: github.com:443 powershellinfraartifacts-gkhedzdeaghdezhr.z01.azurefd.net:443 production.cloudflare.docker.com:443 + production.cloudfront.docker.com:443 pypi.org:443 registry-1.docker.io:443 release-assets.githubusercontent.com:443 @@ -132,14 +133,18 @@ jobs: with: persist-credentials: false - - name: Pin prowler SDK to latest master commit - if: github.event_name == 'push' + - name: Refresh prowler SDK pin to current branch tip run: | - LATEST_SHA=$(git ls-remote https://github.com/prowler-cloud/prowler.git refs/heads/master | cut -f1) - sed -i "s|prowler-cloud/prowler.git@master|prowler-cloud/prowler.git@${LATEST_SHA}|" api/pyproject.toml + # api/pyproject.toml has `@master` on master and `@v5.X` on release + # branches (set by prepare-release.yml). uv lock --upgrade-package + # re-resolves whichever ref is present against the current branch tip + # and writes the SHA into api/uv.lock. The Dockerfile runs + # `uv sync --locked`, which is what actually drives the install. + pip install --no-cache-dir "uv==0.11.14" + (cd api && uv lock --upgrade-package prowler) - name: Login to DockerHub - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -150,7 +155,7 @@ jobs: - name: Build and push API container for ${{ matrix.arch }} id: container-push if: github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'workflow_dispatch' - uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 with: context: ${{ env.WORKING_DIRECTORY }} push: true @@ -158,7 +163,7 @@ jobs: tags: | ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-${{ matrix.arch }} cache-from: type=gha,scope=${{ matrix.arch }} - cache-to: type=gha,mode=max,scope=${{ matrix.arch }} + cache-to: type=gha,mode=${{ github.event_name == 'pull_request' && 'min' || 'max' }},scope=${{ matrix.arch }} # Create and push multi-architecture manifest create-manifest: @@ -170,7 +175,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block allowed-endpoints: > @@ -179,8 +184,9 @@ jobs: registry-1.docker.io:443 auth.docker.io:443 production.cloudflare.docker.com:443 + production.cloudfront.docker.com:443 - name: Login to DockerHub - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -230,7 +236,7 @@ jobs: contents: read steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: audit @@ -277,7 +283,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block allowed-endpoints: > diff --git a/.github/workflows/api-container-checks.yml b/.github/workflows/api-container-checks.yml index 5b59939db9..44482e2428 100644 --- a/.github/workflows/api-container-checks.yml +++ b/.github/workflows/api-container-checks.yml @@ -5,10 +5,16 @@ on: branches: - 'master' - 'v5.*' + paths: + - 'api/**' + - '.github/workflows/api-container-checks.yml' pull_request: branches: - 'master' - 'v5.*' + paths: + - 'api/**' + - '.github/workflows/api-container-checks.yml' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -30,7 +36,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block allowed-endpoints: > @@ -44,7 +50,7 @@ jobs: - name: Check if Dockerfile changed id: dockerfile-changed - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 with: files: api/Dockerfile @@ -57,16 +63,7 @@ jobs: api-container-build-and-scan: if: github.repository == 'prowler-cloud/prowler' - runs-on: ${{ matrix.runner }} - strategy: - matrix: - include: - - platform: linux/amd64 - runner: ubuntu-latest - arch: amd64 - - platform: linux/arm64 - runner: ubuntu-24.04-arm - arch: arm64 + runs-on: ubuntu-latest timeout-minutes: 30 permissions: contents: read @@ -75,7 +72,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block allowed-endpoints: > @@ -86,6 +83,7 @@ jobs: registry-1.docker.io:443 auth.docker.io:443 production.cloudflare.docker.com:443 + production.cloudfront.docker.com:443 debian.map.fastlydns.net:80 release-assets.githubusercontent.com:443 objects.githubusercontent.com:443 @@ -106,7 +104,7 @@ jobs: - name: Check for API changes id: check-changes - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 with: files: api/** files_ignore: | @@ -119,23 +117,22 @@ jobs: if: steps.check-changes.outputs.any_changed == 'true' uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - - name: Build container for ${{ matrix.arch }} + - name: Build container if: steps.check-changes.outputs.any_changed == 'true' - uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 with: context: ${{ env.API_WORKING_DIR }} push: false load: true - platforms: ${{ matrix.platform }} - tags: ${{ env.IMAGE_NAME }}:${{ github.sha }}-${{ matrix.arch }} - cache-from: type=gha,scope=${{ matrix.arch }} - cache-to: type=gha,mode=max,scope=${{ matrix.arch }} + tags: ${{ env.IMAGE_NAME }}:${{ github.sha }} + cache-from: type=gha + cache-to: type=gha,mode=${{ github.event_name == 'pull_request' && 'min' || 'max' }} - - name: Scan container with Trivy for ${{ matrix.arch }} + - name: Scan container with Trivy if: steps.check-changes.outputs.any_changed == 'true' uses: ./.github/actions/trivy-scan with: image-name: ${{ env.IMAGE_NAME }} - image-tag: ${{ github.sha }}-${{ matrix.arch }} + image-tag: ${{ github.sha }} fail-on-critical: 'false' severity: 'CRITICAL' diff --git a/.github/workflows/api-security.yml b/.github/workflows/api-security.yml index 7b8dc72cb1..ba7f5fe58a 100644 --- a/.github/workflows/api-security.yml +++ b/.github/workflows/api-security.yml @@ -5,10 +5,24 @@ on: branches: - "master" - "v5.*" + paths: + - 'api/**' + - '.github/workflows/api-tests.yml' + - '.github/workflows/api-security.yml' + - '.github/actions/setup-python-uv/**' + - '.github/actions/osv-scanner/**' + - '.github/scripts/osv-scan.sh' pull_request: branches: - "master" - "v5.*" + paths: + - 'api/**' + - '.github/workflows/api-tests.yml' + - '.github/workflows/api-security.yml' + - '.github/actions/setup-python-uv/**' + - '.github/actions/osv-scanner/**' + - '.github/scripts/osv-scan.sh' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -25,6 +39,7 @@ jobs: timeout-minutes: 15 permissions: contents: read + pull-requests: write # osv-scanner action posts/updates a PR comment with findings strategy: matrix: python-version: @@ -35,17 +50,20 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block allowed-endpoints: > pypi.org:443 files.pythonhosted.org:443 github.com:443 - auth.safetycli.com:443 - pyup.io:443 - data.safetycli.com:443 api.github.com:443 + objects.githubusercontent.com:443 + raw.githubusercontent.com:443 + release-assets.githubusercontent.com:443 + api.osv.dev:443 + api.deps.dev:443 + osv-vulnerabilities.storage.googleapis.com:443 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -55,35 +73,39 @@ jobs: - name: Check for API changes id: check-changes - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 with: files: | api/** .github/workflows/api-security.yml - .safety-policy.yml + .github/actions/osv-scanner/** + .github/scripts/osv-scan.sh files_ignore: | api/docs/** api/README.md api/CHANGELOG.md api/AGENTS.md - - name: Setup Python with Poetry + - name: Setup Python with uv if: steps.check-changes.outputs.any_changed == 'true' - uses: ./.github/actions/setup-python-poetry + uses: ./.github/actions/setup-python-uv with: python-version: ${{ matrix.python-version }} working-directory: ./api - update-lock: 'true' - name: Bandit if: steps.check-changes.outputs.any_changed == 'true' - run: poetry run bandit -q -lll -x '*_test.py,./contrib/' -r . + # Exclude .venv because uv places the project venv inside ./api; otherwise + # bandit would recurse into installed third-party packages. + run: uv run bandit -q -lll -x '*_test.py,./contrib/,./.venv/' -r . - - name: Safety + - name: Dependency vulnerability scan with osv-scanner if: steps.check-changes.outputs.any_changed == 'true' - # Accepted CVEs, severity threshold, and ignore expirations live in ../.safety-policy.yml - run: poetry run safety check --policy-file ../.safety-policy.yml + uses: ./.github/actions/osv-scanner + with: + lockfile: api/uv.lock - name: Vulture - if: steps.check-changes.outputs.any_changed == 'true' - run: poetry run vulture --exclude "contrib,tests,conftest.py" --min-confidence 100 . + # Run even when osv-scanner reports findings so dead-code signal isn't masked by SCA failures. + if: ${{ !cancelled() && steps.check-changes.outputs.any_changed == 'true' }} + run: uv run vulture --exclude "contrib,tests,conftest.py,.venv" --min-confidence 100 . diff --git a/.github/workflows/api-tests.yml b/.github/workflows/api-tests.yml index 3d4e7e1799..36937bdc68 100644 --- a/.github/workflows/api-tests.yml +++ b/.github/workflows/api-tests.yml @@ -78,7 +78,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block allowed-endpoints: > @@ -87,6 +87,7 @@ jobs: files.pythonhosted.org:443 cli.codecov.io:443 keybase.io:443 + raw.githubusercontent.com:443 ingest.codecov.io:443 storage.googleapis.com:443 o26192.ingest.us.sentry.io:443 @@ -101,7 +102,7 @@ jobs: - name: Check for API changes id: check-changes - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 with: files: | api/** @@ -112,17 +113,16 @@ jobs: api/CHANGELOG.md api/AGENTS.md - - name: Setup Python with Poetry + - name: Setup Python with uv if: steps.check-changes.outputs.any_changed == 'true' - uses: ./.github/actions/setup-python-poetry + uses: ./.github/actions/setup-python-uv with: python-version: ${{ matrix.python-version }} working-directory: ./api - update-lock: 'true' - name: Run tests with pytest if: steps.check-changes.outputs.any_changed == 'true' - run: poetry run pytest --cov=./src/backend --cov-report=xml src/backend + run: uv run pytest --cov=./src/backend --cov-report=xml src/backend - name: Upload coverage reports to Codecov if: steps.check-changes.outputs.any_changed == 'true' diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index ff46749641..b1ea9ec7a2 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -30,7 +30,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block allowed-endpoints: > diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml new file mode 100644 index 0000000000..079e3134b7 --- /dev/null +++ b/.github/workflows/bump-version.yml @@ -0,0 +1,409 @@ +name: 'Release: Bump Versions' + +on: + release: + types: + - 'published' + +concurrency: + group: release-bump-versions-${{ github.event.release.tag_name }} + cancel-in-progress: false + +env: + PROWLER_VERSION: ${{ github.event.release.tag_name }} + DOCS_FILE: docs/getting-started/installation/prowler-app.mdx + +permissions: {} + +jobs: + detect-release-type: + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + outputs: + is_minor: ${{ steps.detect.outputs.is_minor }} + is_patch: ${{ steps.detect.outputs.is_patch }} + major_version: ${{ steps.detect.outputs.major_version }} + minor_version: ${{ steps.detect.outputs.minor_version }} + patch_version: ${{ steps.detect.outputs.patch_version }} + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + with: + egress-policy: audit + + - name: Detect release type and parse version + id: detect + run: | + if [[ $PROWLER_VERSION =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then + MAJOR_VERSION=${BASH_REMATCH[1]} + MINOR_VERSION=${BASH_REMATCH[2]} + PATCH_VERSION=${BASH_REMATCH[3]} + + echo "major_version=${MAJOR_VERSION}" >> "${GITHUB_OUTPUT}" + echo "minor_version=${MINOR_VERSION}" >> "${GITHUB_OUTPUT}" + echo "patch_version=${PATCH_VERSION}" >> "${GITHUB_OUTPUT}" + + if (( MAJOR_VERSION != 5 )); then + echo "::error::Releasing another Prowler major version, aborting..." + exit 1 + fi + + if (( PATCH_VERSION == 0 )); then + echo "is_minor=true" >> "${GITHUB_OUTPUT}" + echo "is_patch=false" >> "${GITHUB_OUTPUT}" + echo "✓ Minor release detected: $PROWLER_VERSION" + else + echo "is_minor=false" >> "${GITHUB_OUTPUT}" + echo "is_patch=true" >> "${GITHUB_OUTPUT}" + echo "✓ Patch release detected: $PROWLER_VERSION" + fi + else + echo "::error::Invalid version syntax: '$PROWLER_VERSION' (must be X.Y.Z)" + exit 1 + fi + + bump-minor-master: + name: Bump versions on master (minor release) + needs: detect-release-type + if: needs.detect-release-type.outputs.is_minor == 'true' + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + pull-requests: write + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + with: + egress-policy: audit + + - name: Checkout master + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: master + persist-credentials: false + + - name: Compute next versions for master + run: | + MAJOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION} + MINOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION} + + # SDK / UI / docs mirror the Prowler version directly. + NEXT_SDK_VERSION=${MAJOR_VERSION}.$((MINOR_VERSION + 1)).0 + + # API is an independent stream: 1..X + # After Prowler 5.M.0 release, master moves on to next API minor: 1.(M+2).0 + NEXT_API_VERSION=1.$((MINOR_VERSION + 2)).0 + + # Read current versions to drive sed replacements. + CURRENT_API_VERSION=$(grep -oP '^version = "\K[^"]+' api/pyproject.toml) + CURRENT_DOCS_VERSION=$(grep -oP 'PROWLER_UI_VERSION="\K[^"]+' "${DOCS_FILE}") + + echo "NEXT_SDK_VERSION=${NEXT_SDK_VERSION}" >> "${GITHUB_ENV}" + echo "NEXT_API_VERSION=${NEXT_API_VERSION}" >> "${GITHUB_ENV}" + echo "CURRENT_API_VERSION=${CURRENT_API_VERSION}" >> "${GITHUB_ENV}" + echo "CURRENT_DOCS_VERSION=${CURRENT_DOCS_VERSION}" >> "${GITHUB_ENV}" + + echo "Released Prowler version: $PROWLER_VERSION" + echo "Next SDK/UI version (master): $NEXT_SDK_VERSION" + echo "Next API version (master): $NEXT_API_VERSION (current: $CURRENT_API_VERSION)" + echo "Docs target version: $PROWLER_VERSION (current: $CURRENT_DOCS_VERSION)" + env: + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION: ${{ needs.detect-release-type.outputs.major_version }} + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION: ${{ needs.detect-release-type.outputs.minor_version }} + + - name: Decide whether to bump docs on master + id: docs_decision + run: | + # Skip docs bump if master is already at or ahead of the release version + # (re-run, or patch shipped against an older minor line). + HIGHEST=$(printf '%s\n%s\n' "${CURRENT_DOCS_VERSION}" "${PROWLER_VERSION}" | sort -V | tail -n1) + if [[ "${CURRENT_DOCS_VERSION}" == "${PROWLER_VERSION}" || "${HIGHEST}" != "${PROWLER_VERSION}" ]]; then + echo "skip=true" >> "${GITHUB_OUTPUT}" + echo "Skipping docs bump: current ($CURRENT_DOCS_VERSION) >= release ($PROWLER_VERSION)" + else + echo "skip=false" >> "${GITHUB_OUTPUT}" + fi + + - name: Bump SDK version (pyproject.toml, config.py) + run: | + set -e + sed -i "s|version = \"${PROWLER_VERSION}\"|version = \"${NEXT_SDK_VERSION}\"|" pyproject.toml + sed -i "s|prowler_version = \"${PROWLER_VERSION}\"|prowler_version = \"${NEXT_SDK_VERSION}\"|" prowler/config/config.py + + - name: Bump API version (api/pyproject.toml, specs/v1.yaml) + run: | + set -e + sed -i "s|version = \"${CURRENT_API_VERSION}\"|version = \"${NEXT_API_VERSION}\"|" api/pyproject.toml + sed -i "s| version: ${CURRENT_API_VERSION}| version: ${NEXT_API_VERSION}|" api/src/backend/api/specs/v1.yaml + + - name: Regenerate lockfiles after version bump + run: | + set -e + # The bumps above edit pyproject.toml / api/pyproject.toml but leave + # uv.lock / api/uv.lock stale, which makes `uv sync --locked` fail in + # the container builds. Refresh both with the uv version the images + # pin (plain `uv lock`, no --upgrade: only the version line changes). + pip install --no-cache-dir "uv==0.11.14" + uv lock + (cd api && uv lock) + + - name: Bump UI version (.env) + run: | + set -e + sed -i "s|NEXT_PUBLIC_PROWLER_RELEASE_VERSION=.*|NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v${NEXT_SDK_VERSION}|" .env + + - name: Bump docs versions (prowler-app.mdx) + if: steps.docs_decision.outputs.skip == 'false' + run: | + set -e + sed -i "s|PROWLER_UI_VERSION=\"${CURRENT_DOCS_VERSION}\"|PROWLER_UI_VERSION=\"${PROWLER_VERSION}\"|" "${DOCS_FILE}" + sed -i "s|PROWLER_API_VERSION=\"${CURRENT_DOCS_VERSION}\"|PROWLER_API_VERSION=\"${PROWLER_VERSION}\"|" "${DOCS_FILE}" + + - name: Show consolidated diff + run: git --no-pager diff + + - name: Create PR for next versions to master + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + with: + author: prowler-bot <179230569+prowler-bot@users.noreply.github.com> + token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} + base: master + commit-message: 'chore(release): Bump versions to v${{ env.NEXT_SDK_VERSION }}' + branch: release-version-bump-to-v${{ env.NEXT_SDK_VERSION }} + title: 'chore(release): Bump versions to v${{ env.NEXT_SDK_VERSION }}' + labels: no-changelog,skip-sync + body: | + ### Description + + Bump Prowler versions on master after releasing Prowler v${{ env.PROWLER_VERSION }}. + + | Area | File(s) | New version | + | --- | --- | --- | + | SDK | `pyproject.toml`, `prowler/config/config.py` | v${{ env.NEXT_SDK_VERSION }} | + | API | `api/pyproject.toml`, `api/src/backend/api/specs/v1.yaml` | v${{ env.NEXT_API_VERSION }} | + | UI | `.env` (`NEXT_PUBLIC_PROWLER_RELEASE_VERSION`) | v${{ env.NEXT_SDK_VERSION }} | + | Docs | `docs/getting-started/installation/prowler-app.mdx` (`PROWLER_UI_VERSION`, `PROWLER_API_VERSION`) | v${{ env.PROWLER_VERSION }} (skipped if already at or ahead) | + + ### License + + By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. + + bump-minor-version-branch: + name: Bump versions on version branch (minor release) + needs: detect-release-type + if: needs.detect-release-type.outputs.is_minor == 'true' + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + pull-requests: write + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + with: + egress-policy: audit + + - name: Checkout version branch + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: v${{ needs.detect-release-type.outputs.major_version }}.${{ needs.detect-release-type.outputs.minor_version }} + persist-credentials: false + + - name: Compute first patch versions for version branch + run: | + MAJOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION} + MINOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION} + VERSION_BRANCH=v${MAJOR_VERSION}.${MINOR_VERSION} + + # SDK / UI first patch mirrors Prowler version directly. + FIRST_PATCH_VERSION=${MAJOR_VERSION}.${MINOR_VERSION}.1 + + # API on this branch stays on the 1..X stream, starting at .1 + FIRST_API_PATCH_VERSION=1.$((MINOR_VERSION + 1)).1 + + CURRENT_API_VERSION=$(grep -oP '^version = "\K[^"]+' api/pyproject.toml) + + echo "FIRST_PATCH_VERSION=${FIRST_PATCH_VERSION}" >> "${GITHUB_ENV}" + echo "FIRST_API_PATCH_VERSION=${FIRST_API_PATCH_VERSION}" >> "${GITHUB_ENV}" + echo "CURRENT_API_VERSION=${CURRENT_API_VERSION}" >> "${GITHUB_ENV}" + echo "VERSION_BRANCH=${VERSION_BRANCH}" >> "${GITHUB_ENV}" + + echo "Released Prowler version: $PROWLER_VERSION" + echo "Version branch: $VERSION_BRANCH" + echo "First SDK/UI patch: $FIRST_PATCH_VERSION" + echo "First API patch: $FIRST_API_PATCH_VERSION (current: $CURRENT_API_VERSION)" + env: + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION: ${{ needs.detect-release-type.outputs.major_version }} + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION: ${{ needs.detect-release-type.outputs.minor_version }} + + - name: Bump SDK version (pyproject.toml, config.py) + run: | + set -e + sed -i "s|version = \"${PROWLER_VERSION}\"|version = \"${FIRST_PATCH_VERSION}\"|" pyproject.toml + sed -i "s|prowler_version = \"${PROWLER_VERSION}\"|prowler_version = \"${FIRST_PATCH_VERSION}\"|" prowler/config/config.py + + - name: Bump API version (api/pyproject.toml, specs/v1.yaml) + run: | + set -e + sed -i "s|version = \"${CURRENT_API_VERSION}\"|version = \"${FIRST_API_PATCH_VERSION}\"|" api/pyproject.toml + sed -i "s| version: ${CURRENT_API_VERSION}| version: ${FIRST_API_PATCH_VERSION}|" api/src/backend/api/specs/v1.yaml + + - name: Regenerate lockfiles after version bump + run: | + set -e + # The bumps above edit pyproject.toml / api/pyproject.toml but leave + # uv.lock / api/uv.lock stale, which makes `uv sync --locked` fail in + # the container builds. Refresh both with the uv version the images + # pin (plain `uv lock`, no --upgrade: only the version line changes). + pip install --no-cache-dir "uv==0.11.14" + uv lock + (cd api && uv lock) + + - name: Bump UI version (.env) + run: | + set -e + sed -i "s|NEXT_PUBLIC_PROWLER_RELEASE_VERSION=.*|NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v${FIRST_PATCH_VERSION}|" .env + + - name: Show consolidated diff + run: git --no-pager diff + + - name: Create PR for first patch versions to version branch + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + with: + author: prowler-bot <179230569+prowler-bot@users.noreply.github.com> + token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} + base: ${{ env.VERSION_BRANCH }} + commit-message: 'chore(release): Bump versions to v${{ env.FIRST_PATCH_VERSION }}' + branch: release-version-bump-to-v${{ env.FIRST_PATCH_VERSION }} + title: 'chore(release): Bump versions to v${{ env.FIRST_PATCH_VERSION }}' + labels: no-changelog,skip-sync + body: | + ### Description + + Bump Prowler versions on `${{ env.VERSION_BRANCH }}` after releasing Prowler v${{ env.PROWLER_VERSION }}. + + | Area | File(s) | New version | + | --- | --- | --- | + | SDK | `pyproject.toml`, `prowler/config/config.py` | v${{ env.FIRST_PATCH_VERSION }} | + | API | `api/pyproject.toml`, `api/src/backend/api/specs/v1.yaml` | v${{ env.FIRST_API_PATCH_VERSION }} | + | UI | `.env` (`NEXT_PUBLIC_PROWLER_RELEASE_VERSION`) | v${{ env.FIRST_PATCH_VERSION }} | + | Docs | (not touched on version branches) | — | + + ### License + + By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. + + bump-patch-version-branch: + name: Bump versions on version branch (patch release) + needs: detect-release-type + if: needs.detect-release-type.outputs.is_patch == 'true' + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + pull-requests: write + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + with: + egress-policy: audit + + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Compute next patch versions + run: | + MAJOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION} + MINOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION} + PATCH_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_PATCH_VERSION} + VERSION_BRANCH=v${MAJOR_VERSION}.${MINOR_VERSION} + + # SDK / UI patch mirrors Prowler version directly. + NEXT_PATCH_VERSION=${MAJOR_VERSION}.${MINOR_VERSION}.$((PATCH_VERSION + 1)) + + CURRENT_API_VERSION=$(grep -oP '^version = "\K[^"]+' api/pyproject.toml) + + # API on this branch stays on 1..X; bump its patch component. + if [[ $CURRENT_API_VERSION =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then + API_PATCH=${BASH_REMATCH[3]} + NEXT_API_PATCH_VERSION=1.$((MINOR_VERSION + 1)).$((API_PATCH + 1)) + else + echo "::error::Invalid API version format: $CURRENT_API_VERSION" + exit 1 + fi + + echo "NEXT_PATCH_VERSION=${NEXT_PATCH_VERSION}" >> "${GITHUB_ENV}" + echo "NEXT_API_PATCH_VERSION=${NEXT_API_PATCH_VERSION}" >> "${GITHUB_ENV}" + echo "CURRENT_API_VERSION=${CURRENT_API_VERSION}" >> "${GITHUB_ENV}" + echo "VERSION_BRANCH=${VERSION_BRANCH}" >> "${GITHUB_ENV}" + + echo "Released Prowler version: $PROWLER_VERSION" + echo "Version branch: $VERSION_BRANCH" + echo "Next SDK/UI patch: $NEXT_PATCH_VERSION" + echo "Next API patch: $NEXT_API_PATCH_VERSION (current: $CURRENT_API_VERSION)" + env: + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION: ${{ needs.detect-release-type.outputs.major_version }} + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION: ${{ needs.detect-release-type.outputs.minor_version }} + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_PATCH_VERSION: ${{ needs.detect-release-type.outputs.patch_version }} + + - name: Bump SDK version (pyproject.toml, config.py) + run: | + set -e + sed -i "s|version = \"${PROWLER_VERSION}\"|version = \"${NEXT_PATCH_VERSION}\"|" pyproject.toml + sed -i "s|prowler_version = \"${PROWLER_VERSION}\"|prowler_version = \"${NEXT_PATCH_VERSION}\"|" prowler/config/config.py + + - name: Bump API version (api/pyproject.toml, specs/v1.yaml) + run: | + set -e + sed -i "s|version = \"${CURRENT_API_VERSION}\"|version = \"${NEXT_API_PATCH_VERSION}\"|" api/pyproject.toml + sed -i "s| version: ${CURRENT_API_VERSION}| version: ${NEXT_API_PATCH_VERSION}|" api/src/backend/api/specs/v1.yaml + + - name: Regenerate lockfiles after version bump + run: | + set -e + # The bumps above edit pyproject.toml / api/pyproject.toml but leave + # uv.lock / api/uv.lock stale, which makes `uv sync --locked` fail in + # the container builds. Refresh both with the uv version the images + # pin (plain `uv lock`, no --upgrade: only the version line changes). + pip install --no-cache-dir "uv==0.11.14" + uv lock + (cd api && uv lock) + + - name: Bump UI version (.env) + run: | + set -e + sed -i "s|NEXT_PUBLIC_PROWLER_RELEASE_VERSION=.*|NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v${NEXT_PATCH_VERSION}|" .env + + - name: Show consolidated diff + run: git --no-pager diff + + - name: Create PR for next patch versions to version branch + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + with: + author: prowler-bot <179230569+prowler-bot@users.noreply.github.com> + token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} + base: ${{ env.VERSION_BRANCH }} + commit-message: 'chore(release): Bump versions to v${{ env.NEXT_PATCH_VERSION }}' + branch: release-version-bump-to-v${{ env.NEXT_PATCH_VERSION }} + title: 'chore(release): Bump versions to v${{ env.NEXT_PATCH_VERSION }}' + labels: no-changelog,skip-sync + body: | + ### Description + + Bump Prowler versions on `${{ env.VERSION_BRANCH }}` after releasing Prowler v${{ env.PROWLER_VERSION }}. + + | Area | File(s) | New version | + | --- | --- | --- | + | SDK | `pyproject.toml`, `prowler/config/config.py` | v${{ env.NEXT_PATCH_VERSION }} | + | API | `api/pyproject.toml`, `api/src/backend/api/specs/v1.yaml` | v${{ env.NEXT_API_PATCH_VERSION }} | + | UI | `.env` (`NEXT_PUBLIC_PROWLER_RELEASE_VERSION`) | v${{ env.NEXT_PATCH_VERSION }} | + | Docs | (not touched on version branches) | — | + + ### License + + By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. diff --git a/.github/workflows/ci-zizmor.yml b/.github/workflows/ci-zizmor.yml index a66af9684c..c0c68d21b9 100644 --- a/.github/workflows/ci-zizmor.yml +++ b/.github/workflows/ci-zizmor.yml @@ -36,7 +36,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block allowed-endpoints: > @@ -51,6 +51,6 @@ jobs: persist-credentials: false - name: Run zizmor - uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2 + uses: zizmorcore/zizmor-action@a16621b09c6db4281f81a93cb393b05dcd7b7165 # v0.5.5 with: token: ${{ github.token }} diff --git a/.github/workflows/comment-label-update.yml b/.github/workflows/comment-label-update.yml index 9649d18cfc..0af688b412 100644 --- a/.github/workflows/comment-label-update.yml +++ b/.github/workflows/comment-label-update.yml @@ -22,7 +22,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: audit diff --git a/.github/workflows/conventional-commit.yml b/.github/workflows/conventional-commit.yml index 5fc31003a4..502881bc73 100644 --- a/.github/workflows/conventional-commit.yml +++ b/.github/workflows/conventional-commit.yml @@ -4,8 +4,6 @@ on: pull_request: branches: - 'master' - - 'v3' - - 'v4.*' - 'v5.*' types: - 'opened' @@ -28,7 +26,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: audit diff --git a/.github/workflows/create-backport-label.yml b/.github/workflows/create-backport-label.yml index d7aa5709ae..4af9fefd5f 100644 --- a/.github/workflows/create-backport-label.yml +++ b/.github/workflows/create-backport-label.yml @@ -25,7 +25,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: audit @@ -43,14 +43,11 @@ jobs: echo "Processing release tag: $RELEASE_TAG" - # Remove 'v' prefix if present (e.g., v3.2.0 -> 3.2.0) VERSION_ONLY="${RELEASE_TAG#v}" - # Check if it's a minor version (X.Y.0) if [[ "$VERSION_ONLY" =~ ^([0-9]+)\.([0-9]+)\.0$ ]]; then echo "Release $RELEASE_TAG (version $VERSION_ONLY) is a minor version. Proceeding to create backport label." - # Extract X.Y from X.Y.0 (e.g., 5.6 from 5.6.0) MAJOR="${BASH_REMATCH[1]}" MINOR="${BASH_REMATCH[2]}" TWO_DIGIT_VERSION="${MAJOR}.${MINOR}" @@ -62,7 +59,6 @@ jobs: echo "Label name: $LABEL_NAME" echo "Label description: $LABEL_DESC" - # Check if label already exists if gh label list --repo ${{ github.repository }} --limit 1000 | grep -q "^${LABEL_NAME}[[:space:]]"; then echo "Label '$LABEL_NAME' already exists." else diff --git a/.github/workflows/docs-bump-version.yml b/.github/workflows/docs-bump-version.yml deleted file mode 100644 index 80c482b6c7..0000000000 --- a/.github/workflows/docs-bump-version.yml +++ /dev/null @@ -1,97 +0,0 @@ -name: 'Docs: Bump Version' - -on: - release: - types: - - 'published' - -concurrency: - group: ${{ github.workflow }}-${{ github.event.release.tag_name }} - cancel-in-progress: false - -env: - PROWLER_VERSION: ${{ github.event.release.tag_name }} - BASE_BRANCH: master - DOCS_FILE: docs/getting-started/installation/prowler-app.mdx - -permissions: {} - -jobs: - bump-version: - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - contents: read - pull-requests: write - steps: - - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 - with: - egress-policy: audit - - - name: Validate release version - run: | - if [[ ! $PROWLER_VERSION =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then - echo "::error::Invalid version syntax: '$PROWLER_VERSION' (must be X.Y.Z)" - exit 1 - fi - if (( ${BASH_REMATCH[1]} != 5 )); then - echo "::error::Releasing another Prowler major version, aborting..." - exit 1 - fi - - - name: Checkout master branch - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: ${{ env.BASE_BRANCH }} - persist-credentials: false - - - name: Read current docs version on master - id: docs_version - run: | - CURRENT_DOCS_VERSION=$(grep -oP 'PROWLER_UI_VERSION="\K[^"]+' "${DOCS_FILE}") - echo "CURRENT_DOCS_VERSION=${CURRENT_DOCS_VERSION}" >> "${GITHUB_ENV}" - echo "Current docs version on master: $CURRENT_DOCS_VERSION" - echo "Target release version: $PROWLER_VERSION" - - # Skip if master is already at or ahead of the release version - # (re-run, or patch shipped against an older minor line) - HIGHEST=$(printf '%s\n%s\n' "${CURRENT_DOCS_VERSION}" "${PROWLER_VERSION}" | sort -V | tail -n1) - if [[ "${CURRENT_DOCS_VERSION}" == "${PROWLER_VERSION}" || "${HIGHEST}" != "${PROWLER_VERSION}" ]]; then - echo "skip=true" >> "${GITHUB_OUTPUT}" - echo "Skipping bump: current ($CURRENT_DOCS_VERSION) >= release ($PROWLER_VERSION)" - else - echo "skip=false" >> "${GITHUB_OUTPUT}" - fi - - - name: Bump versions in documentation - if: steps.docs_version.outputs.skip == 'false' - run: | - set -e - sed -i "s|PROWLER_UI_VERSION=\"${CURRENT_DOCS_VERSION}\"|PROWLER_UI_VERSION=\"${PROWLER_VERSION}\"|" "${DOCS_FILE}" - sed -i "s|PROWLER_API_VERSION=\"${CURRENT_DOCS_VERSION}\"|PROWLER_API_VERSION=\"${PROWLER_VERSION}\"|" "${DOCS_FILE}" - echo "Files modified:" - git --no-pager diff - - - name: Create PR for documentation update to master - if: steps.docs_version.outputs.skip == 'false' - uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 - with: - author: prowler-bot <179230569+prowler-bot@users.noreply.github.com> - token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} - base: ${{ env.BASE_BRANCH }} - commit-message: 'chore(docs): Bump version to v${{ env.PROWLER_VERSION }}' - branch: docs-version-bump-to-v${{ env.PROWLER_VERSION }} - title: 'chore(docs): Bump version to v${{ env.PROWLER_VERSION }}' - labels: no-changelog,skip-sync - body: | - ### Description - - Update Prowler documentation version references to v${{ env.PROWLER_VERSION }} after releasing Prowler v${{ env.PROWLER_VERSION }}. - - ### Files Updated - - `docs/getting-started/installation/prowler-app.mdx`: `PROWLER_UI_VERSION` and `PROWLER_API_VERSION` - - ### License - - By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. diff --git a/.github/workflows/find-secrets.yml b/.github/workflows/find-secrets.yml index 88f84d6729..6f8b47d17e 100644 --- a/.github/workflows/find-secrets.yml +++ b/.github/workflows/find-secrets.yml @@ -25,7 +25,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: # We can't block as Trufflehog needs to verify secrets against vendors egress-policy: audit @@ -37,10 +37,13 @@ jobs: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - fetch-depth: 0 + # PRs only need the diff range; push to master/release walks the new range from event.before. + # 50 is enough headroom for the longest realistic PR/push chain without paying for a full clone. + fetch-depth: 50 persist-credentials: false - - name: Scan for secrets with TruffleHog - uses: trufflesecurity/trufflehog@ef6e76c3c4023279497fab4721ffa071a722fd05 # v3.92.4 + - name: Scan diff for secrets with TruffleHog + # Action auto-injects --since-commit/--branch from event payload; passing them in extra_args produces duplicate flags. + uses: trufflesecurity/trufflehog@37b77001d0174ebec2fcca2bd83ff83a6d45a3ab # v3.95.3 with: - extra_args: '--results=verified,unknown' + extra_args: --results=verified,unknown diff --git a/.github/workflows/helm-chart-checks.yml b/.github/workflows/helm-chart-checks.yml index 66f5954f69..1691f21d35 100644 --- a/.github/workflows/helm-chart-checks.yml +++ b/.github/workflows/helm-chart-checks.yml @@ -33,7 +33,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: audit diff --git a/.github/workflows/helm-chart-release.yml b/.github/workflows/helm-chart-release.yml index 43dcf4d355..ca179adeef 100644 --- a/.github/workflows/helm-chart-release.yml +++ b/.github/workflows/helm-chart-release.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: audit diff --git a/.github/workflows/issue-lock-on-close.yml b/.github/workflows/issue-lock-on-close.yml index 1848585745..3778c77d05 100644 --- a/.github/workflows/issue-lock-on-close.yml +++ b/.github/workflows/issue-lock-on-close.yml @@ -22,7 +22,7 @@ jobs: issues: write steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block allowed-endpoints: > diff --git a/.github/workflows/issue-triage.lock.yml b/.github/workflows/issue-triage.lock.yml index b694e8ecef..6533306322 100644 --- a/.github/workflows/issue-triage.lock.yml +++ b/.github/workflows/issue-triage.lock.yml @@ -66,12 +66,12 @@ jobs: title: ${{ steps.compute-text.outputs.title }} steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: audit - name: Setup Scripts - uses: github/gh-aw/actions/setup@9382be3ca9ac18917e111a99d4e6bbff58d0dccc # v0.43.23 + uses: github/gh-aw/actions/setup@4d44d0e89851a877f4ddc0cb6c0197e42b1016c5 # v0.73.0 with: destination: /opt/gh-aw/actions - name: Check workflow file timestamps @@ -135,12 +135,12 @@ jobs: secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: audit - name: Setup Scripts - uses: github/gh-aw/actions/setup@9382be3ca9ac18917e111a99d4e6bbff58d0dccc # v0.43.23 + uses: github/gh-aw/actions/setup@4d44d0e89851a877f4ddc0cb6c0197e42b1016c5 # v0.73.0 with: destination: /opt/gh-aw/actions - name: Checkout repository @@ -870,12 +870,12 @@ jobs: total_count: ${{ steps.missing_tool.outputs.total_count }} steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: audit - name: Setup Scripts - uses: github/gh-aw/actions/setup@9382be3ca9ac18917e111a99d4e6bbff58d0dccc # v0.43.23 + uses: github/gh-aw/actions/setup@4d44d0e89851a877f4ddc0cb6c0197e42b1016c5 # v0.73.0 with: destination: /opt/gh-aw/actions - name: Download agent output artifact @@ -982,12 +982,12 @@ jobs: success: ${{ steps.parse_results.outputs.success }} steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: audit - name: Setup Scripts - uses: github/gh-aw/actions/setup@9382be3ca9ac18917e111a99d4e6bbff58d0dccc # v0.43.23 + uses: github/gh-aw/actions/setup@4d44d0e89851a877f4ddc0cb6c0197e42b1016c5 # v0.73.0 with: destination: /opt/gh-aw/actions - name: Download agent artifacts @@ -1091,12 +1091,12 @@ jobs: activated: ${{ (steps.check_membership.outputs.is_team_member == 'true') && (steps.check_rate_limit.outputs.rate_limit_ok == 'true') }} steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: audit - name: Setup Scripts - uses: github/gh-aw/actions/setup@9382be3ca9ac18917e111a99d4e6bbff58d0dccc # v0.43.23 + uses: github/gh-aw/actions/setup@4d44d0e89851a877f4ddc0cb6c0197e42b1016c5 # v0.73.0 with: destination: /opt/gh-aw/actions - name: Add eyes reaction for immediate feedback @@ -1164,12 +1164,12 @@ jobs: process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: audit - name: Setup Scripts - uses: github/gh-aw/actions/setup@9382be3ca9ac18917e111a99d4e6bbff58d0dccc # v0.43.23 + uses: github/gh-aw/actions/setup@4d44d0e89851a877f4ddc0cb6c0197e42b1016c5 # v0.73.0 with: destination: /opt/gh-aw/actions - name: Download agent output artifact diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index d55e60ae88..5d519b20d1 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -27,12 +27,12 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: audit - name: Apply labels to PR - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 + uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0 with: sync-labels: true @@ -46,7 +46,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: audit diff --git a/.github/workflows/markdown-lint.yml b/.github/workflows/markdown-lint.yml new file mode 100644 index 0000000000..1a01e97762 --- /dev/null +++ b/.github/workflows/markdown-lint.yml @@ -0,0 +1,60 @@ +name: 'Docs: Markdown Lint' + +on: + push: + branches: + - 'master' + - 'v5.*' + pull_request: + branches: + - 'master' + - 'v5.*' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: {} + +jobs: + markdown-lint: + if: github.repository == 'prowler-cloud/prowler' + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + + steps: + - name: Harden Runner + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + with: + egress-policy: block + allowed-endpoints: > + api.github.com:443 + github.com:443 + registry.npmjs.org:443 + release-assets.githubusercontent.com:443 + + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version-file: ui/.nvmrc + + - name: Setup pnpm + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 + with: + package_json_file: ui/package.json + run_install: false + + - name: Run markdownlint + # Pin must match .pre-commit-config.yaml so prek and CI behave identically. + # pnpm dlx doesn't accept --ignore-scripts as a flag; the env var + # disables postinstall scripts on transitives the same way. + env: + pnpm_config_ignore_scripts: 'true' + run: pnpm dlx markdownlint-cli@0.45.0 '**/*.md' diff --git a/.github/workflows/mcp-container-build-push.yml b/.github/workflows/mcp-container-build-push.yml index 90157db9ab..784d8b9595 100644 --- a/.github/workflows/mcp-container-build-push.yml +++ b/.github/workflows/mcp-container-build-push.yml @@ -45,7 +45,7 @@ jobs: contents: read steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block @@ -64,7 +64,7 @@ jobs: contents: read steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: audit @@ -106,7 +106,7 @@ jobs: packages: write steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block allowed-endpoints: > @@ -114,6 +114,7 @@ jobs: registry-1.docker.io:443 auth.docker.io:443 production.cloudflare.docker.com:443 + production.cloudfront.docker.com:443 ghcr.io:443 pkg-containers.githubusercontent.com:443 files.pythonhosted.org:443 @@ -125,7 +126,7 @@ jobs: persist-credentials: false - name: Login to DockerHub - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -136,7 +137,7 @@ jobs: - name: Build and push MCP container for ${{ matrix.arch }} id: container-push if: github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'workflow_dispatch' - uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 with: context: ${{ env.WORKING_DIRECTORY }} push: true @@ -152,7 +153,7 @@ jobs: org.opencontainers.image.created=${{ github.event_name == 'release' && github.event.release.published_at || github.event.head_commit.timestamp }} ${{ github.event_name == 'release' && format('org.opencontainers.image.version={0}', env.RELEASE_TAG) || '' }} cache-from: type=gha,scope=${{ matrix.arch }} - cache-to: type=gha,mode=max,scope=${{ matrix.arch }} + cache-to: type=gha,mode=${{ github.event_name == 'pull_request' && 'min' || 'max' }},scope=${{ matrix.arch }} # Create and push multi-architecture manifest create-manifest: @@ -164,18 +165,19 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block allowed-endpoints: > registry-1.docker.io:443 auth.docker.io:443 production.cloudflare.docker.com:443 + production.cloudfront.docker.com:443 github.com:443 release-assets.githubusercontent.com:443 - name: Login to DockerHub - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -225,7 +227,7 @@ jobs: contents: read steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: audit @@ -272,7 +274,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block allowed-endpoints: > diff --git a/.github/workflows/mcp-container-checks.yml b/.github/workflows/mcp-container-checks.yml index b205232b0c..7493e1b763 100644 --- a/.github/workflows/mcp-container-checks.yml +++ b/.github/workflows/mcp-container-checks.yml @@ -5,10 +5,16 @@ on: branches: - 'master' - 'v5.*' + paths: + - 'mcp_server/**' + - '.github/workflows/mcp-container-checks.yml' pull_request: branches: - 'master' - 'v5.*' + paths: + - 'mcp_server/**' + - '.github/workflows/mcp-container-checks.yml' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -30,7 +36,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block allowed-endpoints: > @@ -44,7 +50,7 @@ jobs: - name: Check if Dockerfile changed id: dockerfile-changed - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 with: files: mcp_server/Dockerfile @@ -56,16 +62,7 @@ jobs: mcp-container-build-and-scan: if: github.repository == 'prowler-cloud/prowler' - runs-on: ${{ matrix.runner }} - strategy: - matrix: - include: - - platform: linux/amd64 - runner: ubuntu-latest - arch: amd64 - - platform: linux/arm64 - runner: ubuntu-24.04-arm - arch: arm64 + runs-on: ubuntu-latest timeout-minutes: 30 permissions: contents: read @@ -74,7 +71,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block allowed-endpoints: > @@ -82,6 +79,7 @@ jobs: registry-1.docker.io:443 auth.docker.io:443 production.cloudflare.docker.com:443 + production.cloudfront.docker.com:443 ghcr.io:443 pkg-containers.githubusercontent.com:443 files.pythonhosted.org:443 @@ -101,7 +99,7 @@ jobs: - name: Check for MCP changes id: check-changes - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 with: files: mcp_server/** files_ignore: | @@ -112,23 +110,22 @@ jobs: if: steps.check-changes.outputs.any_changed == 'true' uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - - name: Build MCP container for ${{ matrix.arch }} + - name: Build MCP container if: steps.check-changes.outputs.any_changed == 'true' - uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 with: context: ${{ env.MCP_WORKING_DIR }} push: false load: true - platforms: ${{ matrix.platform }} - tags: ${{ env.IMAGE_NAME }}:${{ github.sha }}-${{ matrix.arch }} - cache-from: type=gha,scope=${{ matrix.arch }} - cache-to: type=gha,mode=max,scope=${{ matrix.arch }} + tags: ${{ env.IMAGE_NAME }}:${{ github.sha }} + cache-from: type=gha + cache-to: type=gha,mode=${{ github.event_name == 'pull_request' && 'min' || 'max' }} - - name: Scan MCP container with Trivy for ${{ matrix.arch }} + - name: Scan MCP container with Trivy if: steps.check-changes.outputs.any_changed == 'true' uses: ./.github/actions/trivy-scan with: image-name: ${{ env.IMAGE_NAME }} - image-tag: ${{ github.sha }}-${{ matrix.arch }} + image-tag: ${{ github.sha }} fail-on-critical: 'false' severity: 'CRITICAL' diff --git a/.github/workflows/mcp-pypi-release.yml b/.github/workflows/mcp-pypi-release.yml index cccda1f664..124f67eb19 100644 --- a/.github/workflows/mcp-pypi-release.yml +++ b/.github/workflows/mcp-pypi-release.yml @@ -29,7 +29,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: audit @@ -67,7 +67,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: audit @@ -86,12 +86,34 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} + # The MCP server version (mcp_server/pyproject.toml) is decoupled from the Prowler release + # version: it only changes when MCP code changes. mcp-bump-version.yml normally keeps it in + # sync with mcp_server/CHANGELOG.md (separate from the release bump-version.yml), but this + # publish workflow still runs on every release. + # Pre-flight PyPI check covers the legitimate "no MCP changes for this release" case (and any + # workflow_dispatch re-runs) without failing with HTTP 400 (version exists). + - name: Check if prowler-mcp version already exists on PyPI + id: pypi-check + working-directory: ${{ env.WORKING_DIRECTORY }} + run: | + MCP_VERSION=$(grep '^version' pyproject.toml | head -1 | sed -E 's/^version[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/') + echo "mcp_version=${MCP_VERSION}" >> "$GITHUB_OUTPUT" + if curl -fsS "https://pypi.org/pypi/prowler-mcp/${MCP_VERSION}/json" >/dev/null 2>&1; then + echo "skip=true" >> "$GITHUB_OUTPUT" + echo "::notice title=Skipping prowler-mcp publish::Version ${MCP_VERSION} already exists on PyPI; bump mcp_server/pyproject.toml to publish a new release." + else + echo "skip=false" >> "$GITHUB_OUTPUT" + echo "::notice title=Publishing prowler-mcp::Version ${MCP_VERSION} not on PyPI yet; proceeding." + fi + - name: Build prowler-mcp package + if: steps.pypi-check.outputs.skip != 'true' working-directory: ${{ env.WORKING_DIRECTORY }} run: uv build - name: Publish prowler-mcp package to PyPI - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 + if: steps.pypi-check.outputs.skip != 'true' + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 with: packages-dir: ${{ env.WORKING_DIRECTORY }}/dist/ print-hash: true diff --git a/.github/workflows/mcp-security.yml b/.github/workflows/mcp-security.yml new file mode 100644 index 0000000000..66a5cd8c0d --- /dev/null +++ b/.github/workflows/mcp-security.yml @@ -0,0 +1,75 @@ +name: 'MCP: Security' + +on: + push: + branches: + - 'master' + - 'v5.*' + paths: + - 'mcp_server/pyproject.toml' + - 'mcp_server/uv.lock' + - '.github/workflows/mcp-security.yml' + - '.github/actions/osv-scanner/**' + - '.github/scripts/osv-scan.sh' + pull_request: + branches: + - 'master' + - 'v5.*' + paths: + - 'mcp_server/pyproject.toml' + - 'mcp_server/uv.lock' + - '.github/workflows/mcp-security.yml' + - '.github/actions/osv-scanner/**' + - '.github/scripts/osv-scan.sh' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: {} + +jobs: + mcp-security-scans: + if: github.repository == 'prowler-cloud/prowler' + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + pull-requests: write # osv-scanner action posts/updates a PR comment with findings + + steps: + - name: Harden Runner + uses: step-security/harden-runner@a5ad31d6a139d249332a2605b85202e8c0b78450 # v2.19.1 + with: + egress-policy: block + allowed-endpoints: > + github.com:443 + api.github.com:443 + objects.githubusercontent.com:443 + release-assets.githubusercontent.com:443 + api.osv.dev:443 + api.deps.dev:443 + osv-vulnerabilities.storage.googleapis.com:443 + + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + # zizmor: ignore[artipacked] + persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch + + - name: Check for MCP dependency changes + id: check-changes + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 + with: + files: | + mcp_server/pyproject.toml + mcp_server/uv.lock + .github/workflows/mcp-security.yml + .github/actions/osv-scanner/** + .github/scripts/osv-scan.sh + + - name: Dependency vulnerability scan with osv-scanner + if: steps.check-changes.outputs.any_changed == 'true' + uses: ./.github/actions/osv-scanner + with: + lockfile: mcp_server/uv.lock diff --git a/.github/workflows/nightly-arm64-container-builds.yml b/.github/workflows/nightly-arm64-container-builds.yml new file mode 100644 index 0000000000..061ec61ff2 --- /dev/null +++ b/.github/workflows/nightly-arm64-container-builds.yml @@ -0,0 +1,98 @@ +name: 'Nightly: ARM64 Container Builds' + +# Mitigation for amd64-only PR container-checks: build amd64+arm64 nightly against +# master to keep arm-specific Dockerfile regressions caught quickly. Build only — +# no push, no Trivy (weekly checks already cover that). + +on: + schedule: + - cron: '0 4 * * *' + workflow_dispatch: {} + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +permissions: {} + +jobs: + build-arm64: + if: github.repository == 'prowler-cloud/prowler' + runs-on: ubuntu-24.04-arm + timeout-minutes: 60 + permissions: + contents: read + strategy: + fail-fast: false + matrix: + include: + - component: sdk + context: . + dockerfile: ./Dockerfile + image_name: prowler + - component: api + context: ./api + dockerfile: ./api/Dockerfile + image_name: prowler-api + - component: ui + context: ./ui + dockerfile: ./ui/Dockerfile + image_name: prowler-ui + target: prod + build_args: | + NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51LwpXXXX + - component: mcp + context: ./mcp_server + dockerfile: ./mcp_server/Dockerfile + image_name: prowler-mcp + + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + with: + egress-policy: audit + + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + + - name: Build ${{ matrix.component }} container (linux/arm64) + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + with: + context: ${{ matrix.context }} + file: ${{ matrix.dockerfile }} + target: ${{ matrix.target }} + push: false + load: false + platforms: linux/arm64 + tags: ${{ matrix.image_name }}:nightly-arm64 + build-args: ${{ matrix.build_args }} + cache-from: type=gha,scope=arm64 + cache-to: type=gha,mode=min,scope=arm64 + + notify-failure: + needs: build-arm64 + if: failure() && github.event_name == 'schedule' + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + with: + egress-policy: audit + + - name: Notify Slack on failure + uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1 + with: + method: chat.postMessage + token: ${{ secrets.SLACK_BOT_TOKEN }} + payload: | + channel: ${{ secrets.SLACK_PLATFORM_DEPLOYMENTS }} + text: ":rotating_light: Nightly arm64 container build failed for prowler — <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|view run>" + errors: true diff --git a/.github/workflows/pr-check-changelog.yml b/.github/workflows/pr-check-changelog.yml index fa01ba4cbe..0b9bc20552 100644 --- a/.github/workflows/pr-check-changelog.yml +++ b/.github/workflows/pr-check-changelog.yml @@ -31,7 +31,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block allowed-endpoints: > @@ -41,20 +41,25 @@ jobs: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - fetch-depth: 0 + fetch-depth: 1 # zizmor: ignore[artipacked] persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch + - name: Fetch PR base ref for tj-actions/changed-files + env: + BASE_REF: ${{ github.event.pull_request.base.ref }} + run: git fetch --depth=1 origin "${BASE_REF}" + - name: Get changed files id: changed-files - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 with: files: | api/** ui/** prowler/** mcp_server/** - poetry.lock + uv.lock pyproject.toml - name: Check for folder changes and changelog presence @@ -79,9 +84,9 @@ jobs: fi done - # Check root-level dependency files (poetry.lock, pyproject.toml) + # Check root-level dependency files (uv.lock, pyproject.toml) # These are associated with the prowler folder changelog - root_deps_changed=$(echo "${STEPS_CHANGED_FILES_OUTPUTS_ALL_CHANGED_FILES}" | tr ' ' '\n' | grep -E "^(poetry\.lock|pyproject\.toml)$" || true) + root_deps_changed=$(echo "${STEPS_CHANGED_FILES_OUTPUTS_ALL_CHANGED_FILES}" | tr ' ' '\n' | grep -E "^(uv\.lock|pyproject\.toml)$" || true) if [ -n "$root_deps_changed" ]; then echo "Detected changes in root dependency files: $root_deps_changed" # Check if prowler/CHANGELOG.md was already updated (might have been caught above) diff --git a/.github/workflows/pr-check-compliance-mapping.yml b/.github/workflows/pr-check-compliance-mapping.yml index be934d5983..4df61f49b0 100644 --- a/.github/workflows/pr-check-compliance-mapping.yml +++ b/.github/workflows/pr-check-compliance-mapping.yml @@ -35,7 +35,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block allowed-endpoints: > @@ -45,13 +45,18 @@ jobs: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - fetch-depth: 0 + fetch-depth: 1 # zizmor: ignore[artipacked] persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch + - name: Fetch PR base ref for tj-actions/changed-files + env: + BASE_REF: ${{ github.event.pull_request.base.ref }} + run: git fetch --depth=1 origin "${BASE_REF}" + - name: Get changed files id: changed-files - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 with: files: | prowler/providers/**/services/**/*.metadata.json diff --git a/.github/workflows/pr-conflict-checker.yml b/.github/workflows/pr-conflict-checker.yml index 667f807be8..ff3871bf73 100644 --- a/.github/workflows/pr-conflict-checker.yml +++ b/.github/workflows/pr-conflict-checker.yml @@ -28,7 +28,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: audit @@ -36,12 +36,18 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.event.pull_request.head.sha }} - fetch-depth: 0 - persist-credentials: false + fetch-depth: 1 + # zizmor: ignore[artipacked] + persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch + + - name: Fetch PR base ref for tj-actions/changed-files + env: + BASE_REF: ${{ github.event.pull_request.base.ref }} + run: git fetch --depth=1 origin "${BASE_REF}" - name: Get changed files id: changed-files - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 with: files: '**' diff --git a/.github/workflows/pr-merged.yml b/.github/workflows/pr-merged.yml index 4a98ac70c4..2a1f3d77f1 100644 --- a/.github/workflows/pr-merged.yml +++ b/.github/workflows/pr-merged.yml @@ -26,7 +26,7 @@ jobs: contents: read steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block allowed-endpoints: > diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 07caea7255..ca324aa006 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -29,7 +29,7 @@ jobs: pull-requests: write steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: audit @@ -40,12 +40,11 @@ jobs: token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} persist-credentials: false - - name: Setup Python with Poetry - uses: ./.github/actions/setup-python-poetry + - name: Setup Python with uv + uses: ./.github/actions/setup-python-uv with: python-version: '3.12' install-dependencies: 'false' - enable-cache: 'false' - name: Configure Git run: | @@ -54,7 +53,7 @@ jobs: - name: Parse version and determine branch run: | - # Validate version format (reusing pattern from sdk-bump-version.yml) + # Validate version format (reusing pattern from bump-version.yml) if [[ $PROWLER_VERSION =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then MAJOR_VERSION=${BASH_REMATCH[1]} MINOR_VERSION=${BASH_REMATCH[2]} @@ -300,17 +299,6 @@ jobs: fi echo "✓ api/pyproject.toml prowler dependency: $CURRENT_PROWLER_REF" - - name: Verify API version in api/src/backend/api/v1/views.py - if: ${{ env.HAS_API_CHANGES == 'true' }} - run: | - CURRENT_API_VERSION=$(grep 'spectacular_settings.VERSION = ' api/src/backend/api/v1/views.py | sed -E 's/.*spectacular_settings.VERSION = "([^"]+)".*/\1/' | tr -d '[:space:]') - API_VERSION_TRIMMED=$(echo "$API_VERSION" | tr -d '[:space:]') - if [ "$CURRENT_API_VERSION" != "$API_VERSION_TRIMMED" ]; then - echo "ERROR: API version mismatch in views.py (expected: '$API_VERSION_TRIMMED', found: '$CURRENT_API_VERSION')" - exit 1 - fi - echo "✓ api/src/backend/api/v1/views.py version: $CURRENT_API_VERSION" - - name: Verify API version in api/src/backend/api/specs/v1.yaml if: ${{ env.HAS_API_CHANGES == 'true' }} run: | @@ -339,17 +327,18 @@ jobs: exit 1 fi - # Update poetry lock file - echo "Updating poetry.lock file..." + # Update uv lock file + echo "Updating uv.lock file..." + pip install --no-cache-dir uv==0.11.14 cd api - poetry lock + uv lock cd .. echo "✓ Prepared prowler dependency update to: $UPDATED_PROWLER_REF" - name: Create PR for API dependency update if: ${{ env.PATCH_VERSION == '0' }} - uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 with: token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} commit-message: 'chore(api): update prowler dependency to ${{ env.BRANCH_NAME }} for release ${{ env.PROWLER_VERSION }}' @@ -357,7 +346,7 @@ jobs: base: ${{ env.BRANCH_NAME }} add-paths: | api/pyproject.toml - api/poetry.lock + api/uv.lock title: "chore(api): Update prowler dependency to ${{ env.BRANCH_NAME }} for release ${{ env.PROWLER_VERSION }}" body: | ### Description @@ -366,7 +355,7 @@ jobs: **Changes:** - Updates `api/pyproject.toml` prowler dependency from `@master` to `@${{ env.BRANCH_NAME }}` - - Updates `api/poetry.lock` file with resolved dependencies + - Updates `api/uv.lock` file with resolved dependencies This PR should be merged into the `${{ env.BRANCH_NAME }}` release branch. diff --git a/.github/workflows/renovate-config-validate.yml b/.github/workflows/renovate-config-validate.yml new file mode 100644 index 0000000000..af32eac074 --- /dev/null +++ b/.github/workflows/renovate-config-validate.yml @@ -0,0 +1,57 @@ +name: 'CI: Renovate Config Validate' + +on: + pull_request: + branches: + - 'master' + paths: + - '.github/renovate.json' + - '.pre-commit-config.yaml' + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +permissions: {} + +env: + # renovate: datasource=pypi depName=prek + PREK_VERSION: '0.4.0' + +jobs: + validate: + name: Validate Renovate config + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + + steps: + - name: Harden Runner + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + with: + egress-policy: block + allowed-endpoints: > + api.github.com:443 + github.com:443 + objects.githubusercontent.com:443 + codeload.github.com:443 + release-assets.githubusercontent.com:443 + pypi.org:443 + files.pythonhosted.org:443 + registry.npmjs.org:443 + nodejs.org:443 + + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Set up uv + uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 + + - name: Install prek + run: uv tool install "prek==${PREK_VERSION}" + + - name: Validate Renovate config + run: prek run renovate-config-validator --files .github/renovate.json diff --git a/.github/workflows/sdk-bump-version.yml b/.github/workflows/sdk-bump-version.yml deleted file mode 100644 index c3f018c0d3..0000000000 --- a/.github/workflows/sdk-bump-version.yml +++ /dev/null @@ -1,247 +0,0 @@ -name: 'SDK: Bump Version' - -on: - release: - types: - - 'published' - -concurrency: - group: ${{ github.workflow }}-${{ github.event.release.tag_name }} - cancel-in-progress: false - -env: - PROWLER_VERSION: ${{ github.event.release.tag_name }} - BASE_BRANCH: master - -permissions: {} - -jobs: - detect-release-type: - runs-on: ubuntu-latest - timeout-minutes: 5 - permissions: - contents: read - outputs: - is_minor: ${{ steps.detect.outputs.is_minor }} - is_patch: ${{ steps.detect.outputs.is_patch }} - major_version: ${{ steps.detect.outputs.major_version }} - minor_version: ${{ steps.detect.outputs.minor_version }} - patch_version: ${{ steps.detect.outputs.patch_version }} - steps: - - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 - with: - egress-policy: audit - - - name: Detect release type and parse version - id: detect - run: | - if [[ $PROWLER_VERSION =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then - MAJOR_VERSION=${BASH_REMATCH[1]} - MINOR_VERSION=${BASH_REMATCH[2]} - PATCH_VERSION=${BASH_REMATCH[3]} - - echo "major_version=${MAJOR_VERSION}" >> "${GITHUB_OUTPUT}" - echo "minor_version=${MINOR_VERSION}" >> "${GITHUB_OUTPUT}" - echo "patch_version=${PATCH_VERSION}" >> "${GITHUB_OUTPUT}" - - if (( MAJOR_VERSION != 5 )); then - echo "::error::Releasing another Prowler major version, aborting..." - exit 1 - fi - - if (( PATCH_VERSION == 0 )); then - echo "is_minor=true" >> "${GITHUB_OUTPUT}" - echo "is_patch=false" >> "${GITHUB_OUTPUT}" - echo "✓ Minor release detected: $PROWLER_VERSION" - else - echo "is_minor=false" >> "${GITHUB_OUTPUT}" - echo "is_patch=true" >> "${GITHUB_OUTPUT}" - echo "✓ Patch release detected: $PROWLER_VERSION" - fi - else - echo "::error::Invalid version syntax: '$PROWLER_VERSION' (must be X.Y.Z)" - exit 1 - fi - - bump-minor-version: - needs: detect-release-type - if: needs.detect-release-type.outputs.is_minor == 'true' - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - contents: read - pull-requests: write - steps: - - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 - with: - egress-policy: audit - - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: Calculate next minor version - run: | - MAJOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION} - MINOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION} - - NEXT_MINOR_VERSION=${MAJOR_VERSION}.$((MINOR_VERSION + 1)).0 - echo "NEXT_MINOR_VERSION=${NEXT_MINOR_VERSION}" >> "${GITHUB_ENV}" - - echo "Current version: $PROWLER_VERSION" - echo "Next minor version: $NEXT_MINOR_VERSION" - env: - NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION: ${{ needs.detect-release-type.outputs.major_version }} - NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION: ${{ needs.detect-release-type.outputs.minor_version }} - - - name: Bump versions in files for master - run: | - set -e - - sed -i "s|version = \"${PROWLER_VERSION}\"|version = \"${NEXT_MINOR_VERSION}\"|" pyproject.toml - sed -i "s|prowler_version = \"${PROWLER_VERSION}\"|prowler_version = \"${NEXT_MINOR_VERSION}\"|" prowler/config/config.py - - echo "Files modified:" - git --no-pager diff - - - name: Create PR for next minor version to master - uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 - with: - author: prowler-bot <179230569+prowler-bot@users.noreply.github.com> - token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} - base: master - commit-message: 'chore(sdk): Bump version to v${{ env.NEXT_MINOR_VERSION }}' - branch: sdk-version-bump-to-v${{ env.NEXT_MINOR_VERSION }} - title: 'chore(sdk): Bump version to v${{ env.NEXT_MINOR_VERSION }}' - labels: no-changelog,skip-sync - body: | - ### Description - - Bump Prowler version to v${{ env.NEXT_MINOR_VERSION }} after releasing v${{ env.PROWLER_VERSION }}. - - ### License - - By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. - - - name: Checkout version branch - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: v${{ needs.detect-release-type.outputs.major_version }}.${{ needs.detect-release-type.outputs.minor_version }} - persist-credentials: false - - - name: Calculate first patch version - run: | - MAJOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION} - MINOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION} - - FIRST_PATCH_VERSION=${MAJOR_VERSION}.${MINOR_VERSION}.1 - VERSION_BRANCH=v${MAJOR_VERSION}.${MINOR_VERSION} - - echo "FIRST_PATCH_VERSION=${FIRST_PATCH_VERSION}" >> "${GITHUB_ENV}" - echo "VERSION_BRANCH=${VERSION_BRANCH}" >> "${GITHUB_ENV}" - - echo "First patch version: $FIRST_PATCH_VERSION" - echo "Version branch: $VERSION_BRANCH" - env: - NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION: ${{ needs.detect-release-type.outputs.major_version }} - NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION: ${{ needs.detect-release-type.outputs.minor_version }} - - - name: Bump versions in files for version branch - run: | - set -e - - sed -i "s|version = \"${PROWLER_VERSION}\"|version = \"${FIRST_PATCH_VERSION}\"|" pyproject.toml - sed -i "s|prowler_version = \"${PROWLER_VERSION}\"|prowler_version = \"${FIRST_PATCH_VERSION}\"|" prowler/config/config.py - - echo "Files modified:" - git --no-pager diff - - - name: Create PR for first patch version to version branch - uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 - with: - author: prowler-bot <179230569+prowler-bot@users.noreply.github.com> - token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} - base: ${{ env.VERSION_BRANCH }} - commit-message: 'chore(sdk): Bump version to v${{ env.FIRST_PATCH_VERSION }}' - branch: sdk-version-bump-to-v${{ env.FIRST_PATCH_VERSION }} - title: 'chore(sdk): Bump version to v${{ env.FIRST_PATCH_VERSION }}' - labels: no-changelog,skip-sync - body: | - ### Description - - Bump Prowler version to v${{ env.FIRST_PATCH_VERSION }} in version branch after releasing v${{ env.PROWLER_VERSION }}. - - ### License - - By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. - - bump-patch-version: - needs: detect-release-type - if: needs.detect-release-type.outputs.is_patch == 'true' - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - contents: read - pull-requests: write - steps: - - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 - with: - egress-policy: audit - - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: Calculate next patch version - run: | - MAJOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION} - MINOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION} - PATCH_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_PATCH_VERSION} - - NEXT_PATCH_VERSION=${MAJOR_VERSION}.${MINOR_VERSION}.$((PATCH_VERSION + 1)) - VERSION_BRANCH=v${MAJOR_VERSION}.${MINOR_VERSION} - - echo "NEXT_PATCH_VERSION=${NEXT_PATCH_VERSION}" >> "${GITHUB_ENV}" - echo "VERSION_BRANCH=${VERSION_BRANCH}" >> "${GITHUB_ENV}" - - echo "Current version: $PROWLER_VERSION" - echo "Next patch version: $NEXT_PATCH_VERSION" - echo "Target branch: $VERSION_BRANCH" - env: - NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION: ${{ needs.detect-release-type.outputs.major_version }} - NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION: ${{ needs.detect-release-type.outputs.minor_version }} - NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_PATCH_VERSION: ${{ needs.detect-release-type.outputs.patch_version }} - - - name: Bump versions in files for version branch - run: | - set -e - - sed -i "s|version = \"${PROWLER_VERSION}\"|version = \"${NEXT_PATCH_VERSION}\"|" pyproject.toml - sed -i "s|prowler_version = \"${PROWLER_VERSION}\"|prowler_version = \"${NEXT_PATCH_VERSION}\"|" prowler/config/config.py - - echo "Files modified:" - git --no-pager diff - - - name: Create PR for next patch version to version branch - uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 - with: - author: prowler-bot <179230569+prowler-bot@users.noreply.github.com> - token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} - base: ${{ env.VERSION_BRANCH }} - commit-message: 'chore(sdk): Bump version to v${{ env.NEXT_PATCH_VERSION }}' - branch: sdk-version-bump-to-v${{ env.NEXT_PATCH_VERSION }} - title: 'chore(sdk): Bump version to v${{ env.NEXT_PATCH_VERSION }}' - labels: no-changelog,skip-sync - body: | - ### Description - - Bump Prowler version to v${{ env.NEXT_PATCH_VERSION }} after releasing v${{ env.PROWLER_VERSION }}. - - ### License - - By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. diff --git a/.github/workflows/sdk-check-duplicate-test-names.yml b/.github/workflows/sdk-check-duplicate-test-names.yml index 17c595ca11..7c81e3ae3f 100644 --- a/.github/workflows/sdk-check-duplicate-test-names.yml +++ b/.github/workflows/sdk-check-duplicate-test-names.yml @@ -5,6 +5,9 @@ on: branches: - 'master' - 'v5.*' + paths: + - 'tests/providers/**/*_test.py' + - '.github/workflows/sdk-check-duplicate-test-names.yml' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -22,7 +25,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block allowed-endpoints: > diff --git a/.github/workflows/sdk-code-quality.yml b/.github/workflows/sdk-code-quality.yml index b777ee1657..5c76547989 100644 --- a/.github/workflows/sdk-code-quality.yml +++ b/.github/workflows/sdk-code-quality.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block allowed-endpoints: > @@ -48,7 +48,7 @@ jobs: - name: Check for SDK changes id: check-changes - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 with: files: ./** files_ignore: | @@ -71,24 +71,26 @@ jobs: contrib/** **/AGENTS.md - - name: Setup Python with Poetry + - name: Setup Python with uv if: steps.check-changes.outputs.any_changed == 'true' - uses: ./.github/actions/setup-python-poetry + uses: ./.github/actions/setup-python-uv with: python-version: ${{ matrix.python-version }} - - name: Check Poetry lock file + - name: Check uv lock file if: steps.check-changes.outputs.any_changed == 'true' - run: poetry check --lock + run: uv lock --check - name: Lint with flake8 if: steps.check-changes.outputs.any_changed == 'true' - run: poetry run flake8 . --ignore=E266,W503,E203,E501,W605,E128 --exclude contrib,ui,api,skills + run: uv run flake8 . --ignore=E266,W503,E203,E501,W605,E128 --exclude .venv,contrib,ui,api,skills,mcp_server - name: Check format with black if: steps.check-changes.outputs.any_changed == 'true' - run: poetry run black --exclude "api|ui|skills" --check . + # mcp_server has its own pyproject and uses ruff format, exclude it so SDK black + # does not fight ruff over rules it never formatted. + run: uv run black --exclude "\.venv|api|ui|skills|mcp_server" --check . - name: Lint with pylint if: steps.check-changes.outputs.any_changed == 'true' - run: poetry run pylint --disable=W,C,R,E -j 0 -rn -sn prowler/ + run: uv run pylint --disable=W,C,R,E -j 0 -rn -sn prowler/ diff --git a/.github/workflows/sdk-codeql.yml b/.github/workflows/sdk-codeql.yml index 0985ec1228..0696f7c6a5 100644 --- a/.github/workflows/sdk-codeql.yml +++ b/.github/workflows/sdk-codeql.yml @@ -51,7 +51,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block allowed-endpoints: > @@ -66,12 +66,12 @@ jobs: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4 + uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 with: languages: ${{ matrix.language }} config-file: ./.github/codeql/sdk-codeql-config.yml - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4 + uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 with: category: '/language:${{ matrix.language }}' diff --git a/.github/workflows/sdk-container-build-push.yml b/.github/workflows/sdk-container-build-push.yml index 8a2bba691c..2dbaeec4a5 100644 --- a/.github/workflows/sdk-container-build-push.yml +++ b/.github/workflows/sdk-container-build-push.yml @@ -3,9 +3,7 @@ name: 'SDK: Container Build and Push' on: push: branches: - - 'v3' # For v3-latest - - 'v4.6' # For v4-latest - - 'master' # For latest + - 'master' paths-ignore: - '.github/**' - '!.github/workflows/sdk-container-build-push.yml' @@ -56,14 +54,13 @@ jobs: timeout-minutes: 5 outputs: prowler_version: ${{ steps.get-prowler-version.outputs.prowler_version }} - prowler_version_major: ${{ steps.get-prowler-version.outputs.prowler_version_major }} latest_tag: ${{ steps.get-prowler-version.outputs.latest_tag }} stable_tag: ${{ steps.get-prowler-version.outputs.stable_tag }} permissions: contents: read steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block allowed-endpoints: > @@ -76,48 +73,19 @@ jobs: with: persist-credentials: false - - name: Setup Python with Poetry - uses: ./.github/actions/setup-python-poetry - with: - python-version: ${{ env.PYTHON_VERSION }} - install-dependencies: 'false' - enable-cache: 'false' - - - name: Inject poetry-bumpversion plugin - run: pipx inject poetry poetry-bumpversion - - name: Get Prowler version and set tags id: get-prowler-version run: | - PROWLER_VERSION="$(poetry version -s 2>/dev/null)" + PROWLER_VERSION="$(grep -E '^version = ' pyproject.toml | sed -E 's/version = "([^"]+)"/\1/' | tr -d '[:space:]')" echo "prowler_version=${PROWLER_VERSION}" >> "${GITHUB_OUTPUT}" - # Extract major version PROWLER_VERSION_MAJOR="${PROWLER_VERSION%%.*}" - echo "prowler_version_major=${PROWLER_VERSION_MAJOR}" >> "${GITHUB_OUTPUT}" - - # Set version-specific tags - case ${PROWLER_VERSION_MAJOR} in - 3) - echo "latest_tag=v3-latest" >> "${GITHUB_OUTPUT}" - echo "stable_tag=v3-stable" >> "${GITHUB_OUTPUT}" - echo "✓ Prowler v3 detected - tags: v3-latest, v3-stable" - ;; - 4) - echo "latest_tag=v4-latest" >> "${GITHUB_OUTPUT}" - echo "stable_tag=v4-stable" >> "${GITHUB_OUTPUT}" - echo "✓ Prowler v4 detected - tags: v4-latest, v4-stable" - ;; - 5) - echo "latest_tag=latest" >> "${GITHUB_OUTPUT}" - echo "stable_tag=stable" >> "${GITHUB_OUTPUT}" - echo "✓ Prowler v5 detected - tags: latest, stable" - ;; - *) - echo "::error::Unsupported Prowler major version: ${PROWLER_VERSION_MAJOR}" - exit 1 - ;; - esac + if [[ "${PROWLER_VERSION_MAJOR}" != "5" ]]; then + echo "::error::Unsupported Prowler major version: ${PROWLER_VERSION_MAJOR}" + exit 1 + fi + echo "latest_tag=latest" >> "${GITHUB_OUTPUT}" + echo "stable_tag=stable" >> "${GITHUB_OUTPUT}" notify-release-started: if: github.repository == 'prowler-cloud/prowler' && (github.event_name == 'release' || github.event_name == 'workflow_dispatch') @@ -130,7 +98,7 @@ jobs: contents: read steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: audit @@ -173,7 +141,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block allowed-endpoints: > @@ -181,6 +149,7 @@ jobs: public.ecr.aws:443 registry-1.docker.io:443 production.cloudflare.docker.com:443 + production.cloudfront.docker.com:443 auth.docker.io:443 debian.map.fastlydns.net:80 github.com:443 @@ -199,13 +168,13 @@ jobs: persist-credentials: false - name: Login to DockerHub - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to Public ECR - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: public.ecr.aws username: ${{ secrets.PUBLIC_ECR_AWS_ACCESS_KEY_ID }} @@ -219,7 +188,7 @@ jobs: - name: Build and push SDK container for ${{ matrix.arch }} id: container-push if: github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'workflow_dispatch' - uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 with: context: . file: ${{ env.DOCKERFILE_PATH }} @@ -228,7 +197,7 @@ jobs: tags: | ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.latest_tag }}-${{ matrix.arch }} cache-from: type=gha,scope=${{ matrix.arch }} - cache-to: type=gha,mode=max,scope=${{ matrix.arch }} + cache-to: type=gha,mode=${{ github.event_name == 'pull_request' && 'min' || 'max' }},scope=${{ matrix.arch }} # Create and push multi-architecture manifest create-manifest: @@ -240,7 +209,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block allowed-endpoints: > @@ -248,19 +217,20 @@ jobs: auth.docker.io:443 public.ecr.aws:443 production.cloudflare.docker.com:443 + production.cloudfront.docker.com:443 github.com:443 release-assets.githubusercontent.com:443 api.ecr-public.us-east-1.amazonaws.com:443 - name: Login to DockerHub - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to Public ECR - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: public.ecr.aws username: ${{ secrets.PUBLIC_ECR_AWS_ACCESS_KEY_ID }} @@ -297,7 +267,7 @@ jobs: # Push to toniblyx/prowler only for current version (latest/stable/release tags) - name: Login to DockerHub (toniblyx) if: needs.setup.outputs.latest_tag == 'latest' - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: username: ${{ secrets.TONIBLYX_DOCKERHUB_USERNAME }} password: ${{ secrets.TONIBLYX_DOCKERHUB_PASSWORD }} @@ -322,7 +292,7 @@ jobs: # Re-login as prowlercloud for cleanup of intermediate tags - name: Login to DockerHub (prowlercloud) if: always() - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -350,7 +320,7 @@ jobs: contents: read steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: audit @@ -386,39 +356,3 @@ jobs: payload-file-path: "./.github/scripts/slack-messages/container-release-completed.json" step-outcome: ${{ steps.outcome.outputs.outcome }} update-ts: ${{ needs.notify-release-started.outputs.message-ts }} - - dispatch-v3-deployment: - needs: [setup, container-build-push] - if: always() && needs.setup.outputs.prowler_version_major == '3' && needs.setup.result == 'success' && needs.container-build-push.result == 'success' - runs-on: ubuntu-latest - timeout-minutes: 5 - permissions: - contents: read - - steps: - - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 - with: - egress-policy: audit - - - name: Calculate short SHA - id: short-sha - run: echo "short_sha=${GITHUB_SHA::7}" >> $GITHUB_OUTPUT - - - name: Dispatch v3 deployment (latest) - if: github.event_name == 'push' - uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4.0.1 - with: - token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} - repository: ${{ secrets.DISPATCH_OWNER }}/${{ secrets.DISPATCH_REPO }} - event-type: dispatch - client-payload: '{"version":"v3-latest","tag":"${{ steps.short-sha.outputs.short_sha }}"}' - - - name: Dispatch v3 deployment (release) - if: github.event_name == 'release' - uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4.0.1 - with: - token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} - repository: ${{ secrets.DISPATCH_OWNER }}/${{ secrets.DISPATCH_REPO }} - event-type: dispatch - client-payload: '{"version":"release","tag":"${{ needs.setup.outputs.prowler_version }}"}' diff --git a/.github/workflows/sdk-container-checks.yml b/.github/workflows/sdk-container-checks.yml index 3474df676e..42709ac6f7 100644 --- a/.github/workflows/sdk-container-checks.yml +++ b/.github/workflows/sdk-container-checks.yml @@ -5,10 +5,22 @@ on: branches: - 'master' - 'v5.*' + paths: + - 'prowler/**' + - 'Dockerfile*' + - 'pyproject.toml' + - 'uv.lock' + - '.github/workflows/sdk-container-checks.yml' pull_request: branches: - 'master' - 'v5.*' + paths: + - 'prowler/**' + - 'Dockerfile*' + - 'pyproject.toml' + - 'uv.lock' + - '.github/workflows/sdk-container-checks.yml' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -29,7 +41,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block allowed-endpoints: > @@ -43,7 +55,7 @@ jobs: - name: Check if Dockerfile changed id: dockerfile-changed - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 with: files: Dockerfile @@ -56,16 +68,7 @@ jobs: sdk-container-build-and-scan: if: github.repository == 'prowler-cloud/prowler' - runs-on: ${{ matrix.runner }} - strategy: - matrix: - include: - - platform: linux/amd64 - runner: ubuntu-latest - arch: amd64 - - platform: linux/arm64 - runner: ubuntu-24.04-arm - arch: arm64 + runs-on: ubuntu-latest timeout-minutes: 30 permissions: contents: read @@ -74,7 +77,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block allowed-endpoints: > @@ -82,6 +85,7 @@ jobs: registry-1.docker.io:443 auth.docker.io:443 production.cloudflare.docker.com:443 + production.cloudfront.docker.com:443 api.github.com:443 mirror.gcr.io:443 check.trivy.dev:443 @@ -105,7 +109,7 @@ jobs: - name: Check for SDK changes id: check-changes - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 with: files: ./** files_ignore: | @@ -132,23 +136,22 @@ jobs: if: steps.check-changes.outputs.any_changed == 'true' uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - - name: Build SDK container for ${{ matrix.arch }} + - name: Build SDK container if: steps.check-changes.outputs.any_changed == 'true' - uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 with: context: . push: false load: true - platforms: ${{ matrix.platform }} - tags: ${{ env.IMAGE_NAME }}:${{ github.sha }}-${{ matrix.arch }} - cache-from: type=gha,scope=${{ matrix.arch }} - cache-to: type=gha,mode=max,scope=${{ matrix.arch }} + tags: ${{ env.IMAGE_NAME }}:${{ github.sha }} + cache-from: type=gha + cache-to: type=gha,mode=${{ github.event_name == 'pull_request' && 'min' || 'max' }} - - name: Scan SDK container with Trivy for ${{ matrix.arch }} + - name: Scan SDK container with Trivy if: steps.check-changes.outputs.any_changed == 'true' uses: ./.github/actions/trivy-scan with: image-name: ${{ env.IMAGE_NAME }} - image-tag: ${{ github.sha }}-${{ matrix.arch }} + image-tag: ${{ github.sha }} fail-on-critical: 'false' severity: 'CRITICAL' diff --git a/.github/workflows/sdk-pypi-release.yml b/.github/workflows/sdk-pypi-release.yml index 7916431dec..06e3908be0 100644 --- a/.github/workflows/sdk-pypi-release.yml +++ b/.github/workflows/sdk-pypi-release.yml @@ -28,7 +28,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: audit @@ -66,7 +66,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: audit @@ -75,18 +75,17 @@ jobs: with: persist-credentials: false - - name: Setup Python with Poetry - uses: ./.github/actions/setup-python-poetry + - name: Setup Python with uv + uses: ./.github/actions/setup-python-uv with: python-version: ${{ env.PYTHON_VERSION }} install-dependencies: 'false' - enable-cache: 'false' - name: Build Prowler package - run: poetry build + run: uv build - name: Publish Prowler package to PyPI - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 with: print-hash: true @@ -103,7 +102,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: audit @@ -112,12 +111,11 @@ jobs: with: persist-credentials: false - - name: Setup Python with Poetry - uses: ./.github/actions/setup-python-poetry + - name: Setup Python with uv + uses: ./.github/actions/setup-python-uv with: python-version: ${{ env.PYTHON_VERSION }} install-dependencies: 'false' - enable-cache: 'false' - name: Install toml package run: pip install toml @@ -128,9 +126,9 @@ jobs: python util/replicate_pypi_package.py - name: Build prowler-cloud package - run: poetry build + run: uv build - name: Publish prowler-cloud package to PyPI - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 with: print-hash: true diff --git a/.github/workflows/sdk-refresh-aws-services-regions.yml b/.github/workflows/sdk-refresh-aws-services-regions.yml index 3ece09e8a5..41ec249a53 100644 --- a/.github/workflows/sdk-refresh-aws-services-regions.yml +++ b/.github/workflows/sdk-refresh-aws-services-regions.yml @@ -27,7 +27,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: audit @@ -47,7 +47,7 @@ jobs: run: pip install boto3 - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0 + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 with: aws-region: ${{ env.AWS_REGION }} role-to-assume: ${{ secrets.DEV_IAM_ROLE_ARN }} @@ -58,7 +58,7 @@ jobs: - name: Create pull request id: create-pr - uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 with: token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} author: 'prowler-bot <179230569+prowler-bot@users.noreply.github.com>' diff --git a/.github/workflows/sdk-refresh-oci-regions.yml b/.github/workflows/sdk-refresh-oci-regions.yml index 958d5beb80..65a36c7714 100644 --- a/.github/workflows/sdk-refresh-oci-regions.yml +++ b/.github/workflows/sdk-refresh-oci-regions.yml @@ -25,7 +25,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: audit @@ -55,7 +55,7 @@ jobs: - name: Create pull request id: create-pr - uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 with: token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} author: 'prowler-bot <179230569+prowler-bot@users.noreply.github.com>' diff --git a/.github/workflows/sdk-security.yml b/.github/workflows/sdk-security.yml index ceb6b1db1c..c18f56ea8f 100644 --- a/.github/workflows/sdk-security.yml +++ b/.github/workflows/sdk-security.yml @@ -5,10 +5,30 @@ on: branches: - 'master' - 'v5.*' + paths: + - 'prowler/**' + - 'tests/**' + - 'pyproject.toml' + - 'uv.lock' + - '.github/workflows/sdk-tests.yml' + - '.github/workflows/sdk-security.yml' + - '.github/actions/setup-python-uv/**' + - '.github/actions/osv-scanner/**' + - '.github/scripts/osv-scan.sh' pull_request: branches: - 'master' - 'v5.*' + paths: + - 'prowler/**' + - 'tests/**' + - 'pyproject.toml' + - 'uv.lock' + - '.github/workflows/sdk-tests.yml' + - '.github/workflows/sdk-security.yml' + - '.github/actions/setup-python-uv/**' + - '.github/actions/osv-scanner/**' + - '.github/scripts/osv-scan.sh' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -23,20 +43,23 @@ jobs: timeout-minutes: 15 permissions: contents: read + pull-requests: write # osv-scanner action posts/updates a PR comment with findings steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block allowed-endpoints: > pypi.org:443 files.pythonhosted.org:443 github.com:443 - auth.safetycli.com:443 - pyup.io:443 - data.safetycli.com:443 api.github.com:443 + objects.githubusercontent.com:443 + release-assets.githubusercontent.com:443 + api.osv.dev:443 + api.deps.dev:443 + osv-vulnerabilities.storage.googleapis.com:443 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -46,7 +69,7 @@ jobs: - name: Check for SDK changes id: check-changes - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 with: files: ./** @@ -71,21 +94,23 @@ jobs: contrib/** **/AGENTS.md - - name: Setup Python with Poetry + - name: Setup Python with uv if: steps.check-changes.outputs.any_changed == 'true' - uses: ./.github/actions/setup-python-poetry + uses: ./.github/actions/setup-python-uv with: python-version: '3.12' - name: Security scan with Bandit if: steps.check-changes.outputs.any_changed == 'true' - run: poetry run bandit -q -lll -x '*_test.py,./contrib/,./api/,./ui' -r . + run: uv run bandit -q -lll -x '*_test.py,./.venv/,./contrib/,./api/,./ui' -r . - - name: Security scan with Safety + - name: Dependency vulnerability scan with osv-scanner if: steps.check-changes.outputs.any_changed == 'true' - # Accepted CVEs, severity threshold, and ignore expirations live in .safety-policy.yml - run: poetry run safety check -r pyproject.toml --policy-file .safety-policy.yml + uses: ./.github/actions/osv-scanner + with: + lockfile: uv.lock - name: Dead code detection with Vulture - if: steps.check-changes.outputs.any_changed == 'true' - run: poetry run vulture --exclude "contrib,api,ui" --min-confidence 100 . + # Run even when osv-scanner reports findings so dead-code signal isn't masked by SCA failures. + if: ${{ !cancelled() && steps.check-changes.outputs.any_changed == 'true' }} + run: uv run vulture --exclude ".venv,contrib,api,ui" --min-confidence 100 . diff --git a/.github/workflows/sdk-tests.yml b/.github/workflows/sdk-tests.yml index 5009d379e1..b4b1630a3a 100644 --- a/.github/workflows/sdk-tests.yml +++ b/.github/workflows/sdk-tests.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block allowed-endpoints: > @@ -46,6 +46,7 @@ jobs: schema.ocsf.io:443 registry-1.docker.io:443 production.cloudflare.docker.com:443 + production.cloudfront.docker.com:443 powershellinfraartifacts-gkhedzdeaghdezhr.z01.azurefd.net:443 o26192.ingest.us.sentry.io:443 management.azure.com:443 @@ -69,7 +70,7 @@ jobs: - name: Check for SDK changes id: check-changes - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 with: files: ./** files_ignore: | @@ -92,9 +93,9 @@ jobs: contrib/** **/AGENTS.md - - name: Setup Python with Poetry + - name: Setup Python with uv if: steps.check-changes.outputs.any_changed == 'true' - uses: ./.github/actions/setup-python-poetry + uses: ./.github/actions/setup-python-uv with: python-version: ${{ matrix.python-version }} @@ -102,12 +103,12 @@ jobs: - name: Check if AWS files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-aws - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 with: files: | ./prowler/**/aws/** ./tests/**/aws/** - ./poetry.lock + ./uv.lock - name: Resolve AWS services under test if: steps.changed-aws.outputs.any_changed == 'true' @@ -209,11 +210,11 @@ jobs: echo "AWS service_paths='${STEPS_AWS_SERVICES_OUTPUTS_SERVICE_PATHS}'" if [ "${STEPS_AWS_SERVICES_OUTPUTS_RUN_ALL}" = "true" ]; then - poetry run pytest -n auto --cov=./prowler/providers/aws --cov-report=xml:aws_coverage.xml tests/providers/aws + uv run pytest -n auto --cov=./prowler/providers/aws --cov-report=xml:aws_coverage.xml tests/providers/aws elif [ -z "${STEPS_AWS_SERVICES_OUTPUTS_SERVICE_PATHS}" ]; then echo "No AWS service paths detected; skipping AWS tests." else - poetry run pytest -n auto --cov=./prowler/providers/aws --cov-report=xml:aws_coverage.xml ${STEPS_AWS_SERVICES_OUTPUTS_SERVICE_PATHS} + uv run pytest -n auto --cov=./prowler/providers/aws --cov-report=xml:aws_coverage.xml ${STEPS_AWS_SERVICES_OUTPUTS_SERVICE_PATHS} fi env: STEPS_AWS_SERVICES_OUTPUTS_RUN_ALL: ${{ steps.aws-services.outputs.run_all }} @@ -232,16 +233,16 @@ jobs: - name: Check if Azure files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-azure - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 with: files: | ./prowler/**/azure/** ./tests/**/azure/** - ./poetry.lock + ./uv.lock - name: Run Azure tests if: steps.changed-azure.outputs.any_changed == 'true' - run: poetry run pytest -n auto --cov=./prowler/providers/azure --cov-report=xml:azure_coverage.xml tests/providers/azure + run: uv run pytest -n auto --cov=./prowler/providers/azure --cov-report=xml:azure_coverage.xml tests/providers/azure - name: Upload Azure coverage to Codecov if: steps.changed-azure.outputs.any_changed == 'true' @@ -256,16 +257,16 @@ jobs: - name: Check if GCP files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-gcp - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 with: files: | ./prowler/**/gcp/** ./tests/**/gcp/** - ./poetry.lock + ./uv.lock - name: Run GCP tests if: steps.changed-gcp.outputs.any_changed == 'true' - run: poetry run pytest -n auto --cov=./prowler/providers/gcp --cov-report=xml:gcp_coverage.xml tests/providers/gcp + run: uv run pytest -n auto --cov=./prowler/providers/gcp --cov-report=xml:gcp_coverage.xml tests/providers/gcp - name: Upload GCP coverage to Codecov if: steps.changed-gcp.outputs.any_changed == 'true' @@ -280,16 +281,16 @@ jobs: - name: Check if Kubernetes files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-kubernetes - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 with: files: | ./prowler/**/kubernetes/** ./tests/**/kubernetes/** - ./poetry.lock + ./uv.lock - name: Run Kubernetes tests if: steps.changed-kubernetes.outputs.any_changed == 'true' - run: poetry run pytest -n auto --cov=./prowler/providers/kubernetes --cov-report=xml:kubernetes_coverage.xml tests/providers/kubernetes + run: uv run pytest -n auto --cov=./prowler/providers/kubernetes --cov-report=xml:kubernetes_coverage.xml tests/providers/kubernetes - name: Upload Kubernetes coverage to Codecov if: steps.changed-kubernetes.outputs.any_changed == 'true' @@ -304,16 +305,16 @@ jobs: - name: Check if GitHub files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-github - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 with: files: | ./prowler/**/github/** ./tests/**/github/** - ./poetry.lock + ./uv.lock - name: Run GitHub tests if: steps.changed-github.outputs.any_changed == 'true' - run: poetry run pytest -n auto --cov=./prowler/providers/github --cov-report=xml:github_coverage.xml tests/providers/github + run: uv run pytest -n auto --cov=./prowler/providers/github --cov-report=xml:github_coverage.xml tests/providers/github - name: Upload GitHub coverage to Codecov if: steps.changed-github.outputs.any_changed == 'true' @@ -324,20 +325,44 @@ jobs: flags: prowler-py${{ matrix.python-version }}-github files: ./github_coverage.xml + # Okta Provider + - name: Check if Okta files changed + if: steps.check-changes.outputs.any_changed == 'true' + id: changed-okta + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 + with: + files: | + ./prowler/**/okta/** + ./tests/**/okta/** + ./uv.lock + + - name: Run Okta tests + if: steps.changed-okta.outputs.any_changed == 'true' + run: uv run pytest -n auto --cov=./prowler/providers/okta --cov-report=xml:okta_coverage.xml tests/providers/okta + + - name: Upload Okta coverage to Codecov + if: steps.changed-okta.outputs.any_changed == 'true' + uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + with: + flags: prowler-py${{ matrix.python-version }}-okta + files: ./okta_coverage.xml + # NHN Provider - name: Check if NHN files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-nhn - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 with: files: | ./prowler/**/nhn/** ./tests/**/nhn/** - ./poetry.lock + ./uv.lock - name: Run NHN tests if: steps.changed-nhn.outputs.any_changed == 'true' - run: poetry run pytest -n auto --cov=./prowler/providers/nhn --cov-report=xml:nhn_coverage.xml tests/providers/nhn + run: uv run pytest -n auto --cov=./prowler/providers/nhn --cov-report=xml:nhn_coverage.xml tests/providers/nhn - name: Upload NHN coverage to Codecov if: steps.changed-nhn.outputs.any_changed == 'true' @@ -352,16 +377,16 @@ jobs: - name: Check if M365 files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-m365 - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 with: files: | ./prowler/**/m365/** ./tests/**/m365/** - ./poetry.lock + ./uv.lock - name: Run M365 tests if: steps.changed-m365.outputs.any_changed == 'true' - run: poetry run pytest -n auto --cov=./prowler/providers/m365 --cov-report=xml:m365_coverage.xml tests/providers/m365 + run: uv run pytest -n auto --cov=./prowler/providers/m365 --cov-report=xml:m365_coverage.xml tests/providers/m365 - name: Upload M365 coverage to Codecov if: steps.changed-m365.outputs.any_changed == 'true' @@ -376,16 +401,16 @@ jobs: - name: Check if IaC files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-iac - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 with: files: | ./prowler/**/iac/** ./tests/**/iac/** - ./poetry.lock + ./uv.lock - name: Run IaC tests if: steps.changed-iac.outputs.any_changed == 'true' - run: poetry run pytest -n auto --cov=./prowler/providers/iac --cov-report=xml:iac_coverage.xml tests/providers/iac + run: uv run pytest -n auto --cov=./prowler/providers/iac --cov-report=xml:iac_coverage.xml tests/providers/iac - name: Upload IaC coverage to Codecov if: steps.changed-iac.outputs.any_changed == 'true' @@ -400,16 +425,16 @@ jobs: - name: Check if MongoDB Atlas files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-mongodbatlas - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 with: files: | ./prowler/**/mongodbatlas/** ./tests/**/mongodbatlas/** - ./poetry.lock + ./uv.lock - name: Run MongoDB Atlas tests if: steps.changed-mongodbatlas.outputs.any_changed == 'true' - run: poetry run pytest -n auto --cov=./prowler/providers/mongodbatlas --cov-report=xml:mongodbatlas_coverage.xml tests/providers/mongodbatlas + run: uv run pytest -n auto --cov=./prowler/providers/mongodbatlas --cov-report=xml:mongodbatlas_coverage.xml tests/providers/mongodbatlas - name: Upload MongoDB Atlas coverage to Codecov if: steps.changed-mongodbatlas.outputs.any_changed == 'true' @@ -424,16 +449,16 @@ jobs: - name: Check if OCI files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-oraclecloud - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 with: files: | ./prowler/**/oraclecloud/** ./tests/**/oraclecloud/** - ./poetry.lock + ./uv.lock - name: Run OCI tests if: steps.changed-oraclecloud.outputs.any_changed == 'true' - run: poetry run pytest -n auto --cov=./prowler/providers/oraclecloud --cov-report=xml:oraclecloud_coverage.xml tests/providers/oraclecloud + run: uv run pytest -n auto --cov=./prowler/providers/oraclecloud --cov-report=xml:oraclecloud_coverage.xml tests/providers/oraclecloud - name: Upload OCI coverage to Codecov if: steps.changed-oraclecloud.outputs.any_changed == 'true' @@ -448,16 +473,16 @@ jobs: - name: Check if OpenStack files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-openstack - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 with: files: | ./prowler/**/openstack/** ./tests/**/openstack/** - ./poetry.lock + ./uv.lock - name: Run OpenStack tests if: steps.changed-openstack.outputs.any_changed == 'true' - run: poetry run pytest -n auto --cov=./prowler/providers/openstack --cov-report=xml:openstack_coverage.xml tests/providers/openstack + run: uv run pytest -n auto --cov=./prowler/providers/openstack --cov-report=xml:openstack_coverage.xml tests/providers/openstack - name: Upload OpenStack coverage to Codecov if: steps.changed-openstack.outputs.any_changed == 'true' @@ -472,16 +497,16 @@ jobs: - name: Check if Google Workspace files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-googleworkspace - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 with: files: | ./prowler/**/googleworkspace/** ./tests/**/googleworkspace/** - ./poetry.lock + ./uv.lock - name: Run Google Workspace tests if: steps.changed-googleworkspace.outputs.any_changed == 'true' - run: poetry run pytest -n auto --cov=./prowler/providers/googleworkspace --cov-report=xml:googleworkspace_coverage.xml tests/providers/googleworkspace + run: uv run pytest -n auto --cov=./prowler/providers/googleworkspace --cov-report=xml:googleworkspace_coverage.xml tests/providers/googleworkspace - name: Upload Google Workspace coverage to Codecov if: steps.changed-googleworkspace.outputs.any_changed == 'true' @@ -496,16 +521,16 @@ jobs: - name: Check if Vercel files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-vercel - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 with: files: | ./prowler/**/vercel/** ./tests/**/vercel/** - ./poetry.lock + ./uv.lock - name: Run Vercel tests if: steps.changed-vercel.outputs.any_changed == 'true' - run: poetry run pytest -n auto --cov=./prowler/providers/vercel --cov-report=xml:vercel_coverage.xml tests/providers/vercel + run: uv run pytest -n auto --cov=./prowler/providers/vercel --cov-report=xml:vercel_coverage.xml tests/providers/vercel - name: Upload Vercel coverage to Codecov if: steps.changed-vercel.outputs.any_changed == 'true' @@ -546,16 +571,16 @@ jobs: - name: Check if Lib files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-lib - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 with: files: | ./prowler/lib/** ./tests/lib/** - ./poetry.lock + ./uv.lock - name: Run Lib tests if: steps.changed-lib.outputs.any_changed == 'true' - run: poetry run pytest -n auto --cov=./prowler/lib --cov-report=xml:lib_coverage.xml tests/lib + run: uv run pytest -n auto --cov=./prowler/lib --cov-report=xml:lib_coverage.xml tests/lib - name: Upload Lib coverage to Codecov if: steps.changed-lib.outputs.any_changed == 'true' @@ -570,16 +595,16 @@ jobs: - name: Check if Config files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-config - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 with: files: | ./prowler/config/** ./tests/config/** - ./poetry.lock + ./uv.lock - name: Run Config tests if: steps.changed-config.outputs.any_changed == 'true' - run: poetry run pytest -n auto --cov=./prowler/config --cov-report=xml:config_coverage.xml tests/config + run: uv run pytest -n auto --cov=./prowler/config --cov-report=xml:config_coverage.xml tests/config - name: Upload Config coverage to Codecov if: steps.changed-config.outputs.any_changed == 'true' diff --git a/.github/workflows/test-impact-analysis.yml b/.github/workflows/test-impact-analysis.yml index 88500551f2..625d0f483f 100644 --- a/.github/workflows/test-impact-analysis.yml +++ b/.github/workflows/test-impact-analysis.yml @@ -52,7 +52,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block allowed-endpoints: > @@ -68,7 +68,7 @@ jobs: - name: Get changed files id: changed-files - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 - name: Setup Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 diff --git a/.github/workflows/ui-bump-version.yml b/.github/workflows/ui-bump-version.yml deleted file mode 100644 index 273778e1c2..0000000000 --- a/.github/workflows/ui-bump-version.yml +++ /dev/null @@ -1,253 +0,0 @@ -name: 'UI: Bump Version' - -on: - release: - types: - - 'published' - -concurrency: - group: ${{ github.workflow }}-${{ github.event.release.tag_name }} - cancel-in-progress: false - -env: - PROWLER_VERSION: ${{ github.event.release.tag_name }} - BASE_BRANCH: master - -permissions: {} - -jobs: - detect-release-type: - runs-on: ubuntu-latest - timeout-minutes: 5 - permissions: - contents: read - outputs: - is_minor: ${{ steps.detect.outputs.is_minor }} - is_patch: ${{ steps.detect.outputs.is_patch }} - major_version: ${{ steps.detect.outputs.major_version }} - minor_version: ${{ steps.detect.outputs.minor_version }} - patch_version: ${{ steps.detect.outputs.patch_version }} - steps: - - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 - with: - egress-policy: audit - - - name: Detect release type and parse version - id: detect - run: | - if [[ $PROWLER_VERSION =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then - MAJOR_VERSION=${BASH_REMATCH[1]} - MINOR_VERSION=${BASH_REMATCH[2]} - PATCH_VERSION=${BASH_REMATCH[3]} - - echo "major_version=${MAJOR_VERSION}" >> "${GITHUB_OUTPUT}" - echo "minor_version=${MINOR_VERSION}" >> "${GITHUB_OUTPUT}" - echo "patch_version=${PATCH_VERSION}" >> "${GITHUB_OUTPUT}" - - if (( MAJOR_VERSION != 5 )); then - echo "::error::Releasing another Prowler major version, aborting..." - exit 1 - fi - - if (( PATCH_VERSION == 0 )); then - echo "is_minor=true" >> "${GITHUB_OUTPUT}" - echo "is_patch=false" >> "${GITHUB_OUTPUT}" - echo "✓ Minor release detected: $PROWLER_VERSION" - else - echo "is_minor=false" >> "${GITHUB_OUTPUT}" - echo "is_patch=true" >> "${GITHUB_OUTPUT}" - echo "✓ Patch release detected: $PROWLER_VERSION" - fi - else - echo "::error::Invalid version syntax: '$PROWLER_VERSION' (must be X.Y.Z)" - exit 1 - fi - - bump-minor-version: - needs: detect-release-type - if: needs.detect-release-type.outputs.is_minor == 'true' - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - contents: read - pull-requests: write - steps: - - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 - with: - egress-policy: audit - - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: Calculate next minor version - run: | - MAJOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION} - MINOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION} - - NEXT_MINOR_VERSION=${MAJOR_VERSION}.$((MINOR_VERSION + 1)).0 - echo "NEXT_MINOR_VERSION=${NEXT_MINOR_VERSION}" >> "${GITHUB_ENV}" - - echo "Current version: $PROWLER_VERSION" - echo "Next minor version: $NEXT_MINOR_VERSION" - env: - NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION: ${{ needs.detect-release-type.outputs.major_version }} - NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION: ${{ needs.detect-release-type.outputs.minor_version }} - - - name: Bump UI version in .env for master - run: | - set -e - - sed -i "s|NEXT_PUBLIC_PROWLER_RELEASE_VERSION=.*|NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v${NEXT_MINOR_VERSION}|" .env - - echo "Files modified:" - git --no-pager diff - - - name: Create PR for next minor version to master - uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 - with: - author: prowler-bot <179230569+prowler-bot@users.noreply.github.com> - token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} - base: master - commit-message: 'chore(ui): Bump version to v${{ env.NEXT_MINOR_VERSION }}' - branch: ui-version-bump-to-v${{ env.NEXT_MINOR_VERSION }} - title: 'chore(ui): Bump version to v${{ env.NEXT_MINOR_VERSION }}' - labels: no-changelog,skip-sync - body: | - ### Description - - Bump Prowler UI version to v${{ env.NEXT_MINOR_VERSION }} after releasing Prowler v${{ env.PROWLER_VERSION }}. - - ### Files Updated - - `.env`: `NEXT_PUBLIC_PROWLER_RELEASE_VERSION` - - ### License - - By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. - - - name: Checkout version branch - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: v${{ needs.detect-release-type.outputs.major_version }}.${{ needs.detect-release-type.outputs.minor_version }} - persist-credentials: false - - - name: Calculate first patch version - run: | - MAJOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION} - MINOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION} - - FIRST_PATCH_VERSION=${MAJOR_VERSION}.${MINOR_VERSION}.1 - VERSION_BRANCH=v${MAJOR_VERSION}.${MINOR_VERSION} - - echo "FIRST_PATCH_VERSION=${FIRST_PATCH_VERSION}" >> "${GITHUB_ENV}" - echo "VERSION_BRANCH=${VERSION_BRANCH}" >> "${GITHUB_ENV}" - - echo "First patch version: $FIRST_PATCH_VERSION" - echo "Version branch: $VERSION_BRANCH" - env: - NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION: ${{ needs.detect-release-type.outputs.major_version }} - NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION: ${{ needs.detect-release-type.outputs.minor_version }} - - - name: Bump UI version in .env for version branch - run: | - set -e - - sed -i "s|NEXT_PUBLIC_PROWLER_RELEASE_VERSION=.*|NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v${FIRST_PATCH_VERSION}|" .env - - echo "Files modified:" - git --no-pager diff - - - name: Create PR for first patch version to version branch - uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 - with: - author: prowler-bot <179230569+prowler-bot@users.noreply.github.com> - token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} - base: ${{ env.VERSION_BRANCH }} - commit-message: 'chore(ui): Bump version to v${{ env.FIRST_PATCH_VERSION }}' - branch: ui-version-bump-to-v${{ env.FIRST_PATCH_VERSION }} - title: 'chore(ui): Bump version to v${{ env.FIRST_PATCH_VERSION }}' - labels: no-changelog,skip-sync - body: | - ### Description - - Bump Prowler UI version to v${{ env.FIRST_PATCH_VERSION }} in version branch after releasing Prowler v${{ env.PROWLER_VERSION }}. - - ### Files Updated - - `.env`: `NEXT_PUBLIC_PROWLER_RELEASE_VERSION` - - ### License - - By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. - - bump-patch-version: - needs: detect-release-type - if: needs.detect-release-type.outputs.is_patch == 'true' - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - contents: read - pull-requests: write - steps: - - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 - with: - egress-policy: audit - - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: Calculate next patch version - run: | - MAJOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION} - MINOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION} - PATCH_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_PATCH_VERSION} - - NEXT_PATCH_VERSION=${MAJOR_VERSION}.${MINOR_VERSION}.$((PATCH_VERSION + 1)) - VERSION_BRANCH=v${MAJOR_VERSION}.${MINOR_VERSION} - - echo "NEXT_PATCH_VERSION=${NEXT_PATCH_VERSION}" >> "${GITHUB_ENV}" - echo "VERSION_BRANCH=${VERSION_BRANCH}" >> "${GITHUB_ENV}" - - echo "Current version: $PROWLER_VERSION" - echo "Next patch version: $NEXT_PATCH_VERSION" - echo "Target branch: $VERSION_BRANCH" - env: - NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION: ${{ needs.detect-release-type.outputs.major_version }} - NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION: ${{ needs.detect-release-type.outputs.minor_version }} - NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_PATCH_VERSION: ${{ needs.detect-release-type.outputs.patch_version }} - - - name: Bump UI version in .env for version branch - run: | - set -e - - sed -i "s|NEXT_PUBLIC_PROWLER_RELEASE_VERSION=.*|NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v${NEXT_PATCH_VERSION}|" .env - - echo "Files modified:" - git --no-pager diff - - - name: Create PR for next patch version to version branch - uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 - with: - author: prowler-bot <179230569+prowler-bot@users.noreply.github.com> - token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} - base: ${{ env.VERSION_BRANCH }} - commit-message: 'chore(ui): Bump version to v${{ env.NEXT_PATCH_VERSION }}' - branch: ui-version-bump-to-v${{ env.NEXT_PATCH_VERSION }} - title: 'chore(ui): Bump version to v${{ env.NEXT_PATCH_VERSION }}' - labels: no-changelog,skip-sync - body: | - ### Description - - Bump Prowler UI version to v${{ env.NEXT_PATCH_VERSION }} after releasing Prowler v${{ env.PROWLER_VERSION }}. - - ### Files Updated - - `.env`: `NEXT_PUBLIC_PROWLER_RELEASE_VERSION` - - ### License - - By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. diff --git a/.github/workflows/ui-codeql.yml b/.github/workflows/ui-codeql.yml index c8b65e77d3..3dcb4f42eb 100644 --- a/.github/workflows/ui-codeql.yml +++ b/.github/workflows/ui-codeql.yml @@ -47,7 +47,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block allowed-endpoints: > @@ -62,12 +62,12 @@ jobs: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4 + uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 with: languages: ${{ matrix.language }} config-file: ./.github/codeql/ui-codeql-config.yml - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4 + uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 with: category: '/language:${{ matrix.language }}' diff --git a/.github/workflows/ui-container-build-push.yml b/.github/workflows/ui-container-build-push.yml index 6fab2d1efb..984256af7f 100644 --- a/.github/workflows/ui-container-build-push.yml +++ b/.github/workflows/ui-container-build-push.yml @@ -48,7 +48,7 @@ jobs: contents: read steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: audit @@ -67,7 +67,7 @@ jobs: contents: read steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: audit @@ -110,12 +110,13 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block allowed-endpoints: > registry-1.docker.io:443 production.cloudflare.docker.com:443 + production.cloudfront.docker.com:443 auth.docker.io:443 registry.npmjs.org:443 dl-cdn.alpinelinux.org:443 @@ -129,7 +130,7 @@ jobs: persist-credentials: false - name: Login to DockerHub - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -140,7 +141,7 @@ jobs: - name: Build and push UI container for ${{ matrix.arch }} id: container-push if: github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'workflow_dispatch' - uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 with: context: ${{ env.WORKING_DIRECTORY }} build-args: | @@ -151,7 +152,7 @@ jobs: tags: | ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-${{ matrix.arch }} cache-from: type=gha,scope=${{ matrix.arch }} - cache-to: type=gha,mode=max,scope=${{ matrix.arch }} + cache-to: type=gha,mode=${{ github.event_name == 'pull_request' && 'min' || 'max' }},scope=${{ matrix.arch }} # Create and push multi-architecture manifest create-manifest: @@ -163,7 +164,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block allowed-endpoints: > @@ -172,9 +173,10 @@ jobs: registry-1.docker.io:443 auth.docker.io:443 production.cloudflare.docker.com:443 + production.cloudfront.docker.com:443 - name: Login to DockerHub - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -224,7 +226,7 @@ jobs: contents: read steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: audit @@ -271,7 +273,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block allowed-endpoints: > diff --git a/.github/workflows/ui-container-checks.yml b/.github/workflows/ui-container-checks.yml index eb7b508b58..0eae703fce 100644 --- a/.github/workflows/ui-container-checks.yml +++ b/.github/workflows/ui-container-checks.yml @@ -5,10 +5,16 @@ on: branches: - 'master' - 'v5.*' + paths: + - 'ui/**' + - '.github/workflows/ui-container-checks.yml' pull_request: branches: - 'master' - 'v5.*' + paths: + - 'ui/**' + - '.github/workflows/ui-container-checks.yml' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -30,7 +36,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block allowed-endpoints: > @@ -44,7 +50,7 @@ jobs: - name: Check if Dockerfile changed id: dockerfile-changed - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 with: files: ui/Dockerfile @@ -57,16 +63,7 @@ jobs: ui-container-build-and-scan: if: github.repository == 'prowler-cloud/prowler' - runs-on: ${{ matrix.runner }} - strategy: - matrix: - include: - - platform: linux/amd64 - runner: ubuntu-latest - arch: amd64 - - platform: linux/arm64 - runner: ubuntu-24.04-arm - arch: arm64 + runs-on: ubuntu-latest timeout-minutes: 30 permissions: contents: read @@ -75,7 +72,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block allowed-endpoints: > @@ -83,6 +80,7 @@ jobs: registry-1.docker.io:443 auth.docker.io:443 production.cloudflare.docker.com:443 + production.cloudfront.docker.com:443 registry.npmjs.org:443 dl-cdn.alpinelinux.org:443 fonts.googleapis.com:443 @@ -102,7 +100,7 @@ jobs: - name: Check for UI changes id: check-changes - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 with: files: ui/** files_ignore: | @@ -114,26 +112,25 @@ jobs: if: steps.check-changes.outputs.any_changed == 'true' uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - - name: Build UI container for ${{ matrix.arch }} + - name: Build UI container if: steps.check-changes.outputs.any_changed == 'true' - uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 with: context: ${{ env.UI_WORKING_DIR }} target: prod push: false load: true - platforms: ${{ matrix.platform }} - tags: ${{ env.IMAGE_NAME }}:${{ github.sha }}-${{ matrix.arch }} - cache-from: type=gha,scope=${{ matrix.arch }} - cache-to: type=gha,mode=max,scope=${{ matrix.arch }} + tags: ${{ env.IMAGE_NAME }}:${{ github.sha }} + cache-from: type=gha + cache-to: type=gha,mode=${{ github.event_name == 'pull_request' && 'min' || 'max' }} build-args: | NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51LwpXXXX - - name: Scan UI container with Trivy for ${{ matrix.arch }} + - name: Scan UI container with Trivy if: steps.check-changes.outputs.any_changed == 'true' uses: ./.github/actions/trivy-scan with: image-name: ${{ env.IMAGE_NAME }} - image-tag: ${{ github.sha }}-${{ matrix.arch }} + image-tag: ${{ github.sha }} fail-on-critical: 'false' severity: 'CRITICAL' diff --git a/.github/workflows/ui-e2e-tests-v2.yml b/.github/workflows/ui-e2e-tests-v2.yml index 2a91e460be..67b4a701da 100644 --- a/.github/workflows/ui-e2e-tests-v2.yml +++ b/.github/workflows/ui-e2e-tests-v2.yml @@ -15,6 +15,10 @@ on: - 'ui/**' - 'api/**' # API changes can affect UI E2E +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + permissions: {} jobs: @@ -81,7 +85,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: audit @@ -126,6 +130,12 @@ jobs: echo "AWS_ACCESS_KEY_ID=${{ secrets.E2E_AWS_PROVIDER_ACCESS_KEY }}" >> .env echo "AWS_SECRET_ACCESS_KEY=${{ secrets.E2E_AWS_PROVIDER_SECRET_KEY }}" >> .env + - name: Build API image from current code + # docker-compose.yml references prowlercloud/prowler-api:latest from the registry, + # which lags behind PR changes; build locally so E2E exercises the API image + # produced by this PR. + run: docker build -t prowlercloud/prowler-api:latest ./api + - name: Start API services run: | export PROWLER_API_VERSION=latest @@ -154,7 +164,7 @@ jobs: for fixture in api/fixtures/dev/*.json; do if [ -f "$fixture" ]; then echo "Loading $fixture" - poetry run python manage.py loaddata "$fixture" --database admin + uv run python manage.py loaddata "$fixture" --database admin fi done ' @@ -174,7 +184,7 @@ jobs: run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - name: Setup pnpm and Next.js cache - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: | ${{ env.STORE_PATH }} @@ -194,7 +204,7 @@ jobs: run: pnpm run build - name: Cache Playwright browsers - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 id: playwright-cache with: path: ~/.cache/ms-playwright @@ -266,7 +276,7 @@ jobs: with: name: playwright-report path: ui/playwright-report/ - retention-days: 30 + retention-days: 7 - name: Cleanup services if: always() @@ -285,7 +295,7 @@ jobs: contents: read steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: audit diff --git a/.github/workflows/ui-security.yml b/.github/workflows/ui-security.yml new file mode 100644 index 0000000000..470fae1f9c --- /dev/null +++ b/.github/workflows/ui-security.yml @@ -0,0 +1,75 @@ +name: 'UI: Security' + +on: + push: + branches: + - 'master' + - 'v5.*' + paths: + - 'ui/package.json' + - 'ui/pnpm-lock.yaml' + - '.github/workflows/ui-security.yml' + - '.github/actions/osv-scanner/**' + - '.github/scripts/osv-scan.sh' + pull_request: + branches: + - 'master' + - 'v5.*' + paths: + - 'ui/package.json' + - 'ui/pnpm-lock.yaml' + - '.github/workflows/ui-security.yml' + - '.github/actions/osv-scanner/**' + - '.github/scripts/osv-scan.sh' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: {} + +jobs: + ui-security-scans: + if: github.repository == 'prowler-cloud/prowler' + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + pull-requests: write # osv-scanner action posts/updates a PR comment with findings + + steps: + - name: Harden Runner + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + with: + egress-policy: block + allowed-endpoints: > + github.com:443 + api.github.com:443 + objects.githubusercontent.com:443 + release-assets.githubusercontent.com:443 + api.osv.dev:443 + api.deps.dev:443 + osv-vulnerabilities.storage.googleapis.com:443 + + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + # zizmor: ignore[artipacked] + persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch + + - name: Check for UI dependency changes + id: check-changes + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 + with: + files: | + ui/package.json + ui/pnpm-lock.yaml + .github/workflows/ui-security.yml + .github/actions/osv-scanner/** + .github/scripts/osv-scan.sh + + - name: Dependency vulnerability scan with osv-scanner + if: steps.check-changes.outputs.any_changed == 'true' + uses: ./.github/actions/osv-scanner + with: + lockfile: ui/pnpm-lock.yaml diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index c11e90b8f4..91e2324c07 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -1,14 +1,14 @@ -name: 'UI: Tests' +name: "UI: Tests" on: push: branches: - - 'master' - - 'v5.*' + - "master" + - "v5.*" pull_request: branches: - - 'master' - - 'v5.*' + - "master" + - "v5.*" concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -16,7 +16,7 @@ concurrency: env: UI_WORKING_DIR: ./ui - NODE_VERSION: '24.13.0' + NODE_VERSION: "24.13.0" permissions: {} @@ -32,7 +32,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: block allowed-endpoints: > @@ -42,6 +42,9 @@ jobs: fonts.gstatic.com:443 api.github.com:443 release-assets.githubusercontent.com:443 + cdn.playwright.dev:443 + objects.githubusercontent.com:443 + playwright.download.prss.microsoft.com:443 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -51,7 +54,7 @@ jobs: - name: Check for UI changes id: check-changes - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 with: files: | ui/** @@ -64,7 +67,7 @@ jobs: - name: Get changed source files for targeted tests id: changed-source if: steps.check-changes.outputs.any_changed == 'true' - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 with: files: | ui/**/*.ts @@ -80,7 +83,7 @@ jobs: - name: Check for critical path changes (run all tests) id: critical-changes if: steps.check-changes.outputs.any_changed == 'true' - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 with: files: | ui/lib/** @@ -110,7 +113,7 @@ jobs: - name: Setup pnpm and Next.js cache if: steps.check-changes.outputs.any_changed == 'true' - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: | ${{ env.STORE_PATH }} @@ -129,11 +132,15 @@ jobs: if: steps.check-changes.outputs.any_changed == 'true' run: pnpm run healthcheck + - name: Run pnpm audit + if: steps.check-changes.outputs.any_changed == 'true' + run: pnpm run audit + - name: Run unit tests (all - critical paths changed) if: steps.check-changes.outputs.any_changed == 'true' && steps.critical-changes.outputs.any_changed == 'true' run: | echo "Critical paths changed - running ALL unit tests" - pnpm run test:run + pnpm run test:unit - name: Run unit tests (related to changes only) if: steps.check-changes.outputs.any_changed == 'true' && steps.critical-changes.outputs.any_changed != 'true' && steps.changed-source.outputs.all_changed_files != '' @@ -142,7 +149,7 @@ jobs: echo "${STEPS_CHANGED_SOURCE_OUTPUTS_ALL_CHANGED_FILES}" # Convert space-separated to vitest related format (remove ui/ prefix for relative paths) CHANGED_FILES=$(echo "${STEPS_CHANGED_SOURCE_OUTPUTS_ALL_CHANGED_FILES}" | tr ' ' '\n' | sed 's|^ui/||' | tr '\n' ' ') - pnpm exec vitest related $CHANGED_FILES --run + pnpm exec vitest related $CHANGED_FILES --run --project unit env: STEPS_CHANGED_SOURCE_OUTPUTS_ALL_CHANGED_FILES: ${{ steps.changed-source.outputs.all_changed_files }} @@ -150,7 +157,25 @@ jobs: if: steps.check-changes.outputs.any_changed == 'true' && steps.critical-changes.outputs.any_changed != 'true' && steps.changed-source.outputs.all_changed_files == '' run: | echo "Only test files changed - running ALL unit tests" - pnpm run test:run + pnpm run test:unit + + - name: Cache Playwright browsers + if: steps.check-changes.outputs.any_changed == 'true' + id: playwright-cache + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-chromium-${{ hashFiles('ui/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-playwright-chromium- + + - name: Install Playwright Chromium browser + if: steps.check-changes.outputs.any_changed == 'true' && steps.playwright-cache.outputs.cache-hit != 'true' + run: pnpm exec playwright install chromium + + - name: Run browser tests + if: steps.check-changes.outputs.any_changed == 'true' + run: pnpm run test:browser - name: Build application if: steps.check-changes.outputs.any_changed == 'true' diff --git a/.github/zizmor.yml b/.github/zizmor.yml index 931ef1b94c..0cfc6215e1 100644 --- a/.github/zizmor.yml +++ b/.github/zizmor.yml @@ -1,21 +1,19 @@ rules: secrets-outside-env: ignore: - - api-bump-version.yml - api-container-build-push.yml - api-tests.yml - backport.yml - - docs-bump-version.yml + - bump-version.yml - issue-triage.lock.yml - mcp-container-build-push.yml + - nightly-arm64-container-builds.yml - pr-merged.yml - prepare-release.yml - - sdk-bump-version.yml - sdk-container-build-push.yml - sdk-refresh-aws-services-regions.yml - sdk-refresh-oci-regions.yml - sdk-tests.yml - - ui-bump-version.yml - ui-container-build-push.yml - ui-e2e-tests-v2.yml superfluous-actions: diff --git a/.gitignore b/.gitignore index 9e0e4da849..4d73ffe990 100644 --- a/.gitignore +++ b/.gitignore @@ -60,7 +60,6 @@ htmlcov/ **/mcp-config.json **/mcpServers.json .mcp/ -.mcp.json # AI Coding Assistants - Cursor .cursorignore diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000000..02a11d111b --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,10 @@ +{ + "extends": "markdownlint/style/prettier", + "first-line-h1": false, + "no-duplicate-heading": { + "siblings_only": true + }, + "no-inline-html": false, + "line-length": false, + "no-bare-urls": false +} diff --git a/.markdownlintignore b/.markdownlintignore new file mode 100644 index 0000000000..7aa58ccd1a --- /dev/null +++ b/.markdownlintignore @@ -0,0 +1,16 @@ +node_modules/ +ui/node_modules/ +.git/ +.venv/ +**/.venv/ +dist/ +build/ +htmlcov/ +.next/ +ui/.next/ +ui/out/ +contrib/ + +# Auto-generated content (keepachangelog format legitimately repeats section headings). +# Revisit with the team — see beads task on markdownlint rule triage. +**/CHANGELOG.md diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1f55160b4c..4e79141bb7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ # P40 — security scanners # P50 — dependency validation -default_install_hook_types: [pre-commit, pre-push] +default_install_hook_types: [pre-commit] repos: ## GENERAL (prek built-in — no external repo needed) @@ -44,9 +44,19 @@ repos: rev: v1.24.1 hooks: - id: zizmor - files: ^\.github/ + # zizmor only audits workflows, composite actions and dependabot + # config; broader paths trip exit 3 ("no audit was performed"). + files: ^\.github/(workflows|actions)/.+\.ya?ml$|^\.github/dependabot\.ya?ml$ priority: 30 + ## RENOVATE + - repo: https://github.com/renovatebot/pre-commit-hooks + rev: 43.150.0 + hooks: + - id: renovate-config-validator + files: ^\.github/renovate\.json$ + priority: 10 + ## BASH - repo: https://github.com/koalaman/shellcheck-precommit rev: v0.11.0 @@ -62,12 +72,7 @@ repos: - id: autoflake name: "SDK - autoflake" files: { glob: ["{prowler,tests,dashboard,util,scripts}/**/*.py"] } - args: - [ - "--in-place", - "--remove-all-unused-imports", - "--remove-unused-variable", - ] + args: ["--in-place", "--remove-all-unused-imports", "--remove-unused-variable"] priority: 20 - repo: https://github.com/pycqa/isort @@ -110,37 +115,30 @@ repos: files: { glob: ["{api,mcp_server}/**/*.py"] } priority: 20 - ## PYTHON — Poetry - - repo: https://github.com/python-poetry/poetry - rev: 2.3.4 + ## PYTHON — uv (API + SDK) + - repo: https://github.com/astral-sh/uv-pre-commit + rev: 0.11.14 hooks: - - id: poetry-check - name: API - poetry-check - args: ["--directory=./api"] - files: { glob: ["api/{pyproject.toml,poetry.lock}"] } + - id: uv-lock + name: API - uv-lock + args: ["--check", "--project=./api"] + files: { glob: ["api/{pyproject.toml,uv.lock}"] } pass_filenames: false priority: 50 - - id: poetry-lock - name: API - poetry-lock - args: ["--directory=./api"] - files: { glob: ["api/{pyproject.toml,poetry.lock}"] } + - id: uv-lock + name: SDK - uv-lock + args: ["--check", "--project=./"] + files: { glob: ["{pyproject.toml,uv.lock}"] } pass_filenames: false priority: 50 - - id: poetry-check - name: SDK - poetry-check - args: ["--directory=./"] - files: { glob: ["{pyproject.toml,poetry.lock}"] } - pass_filenames: false - priority: 50 - - - id: poetry-lock - name: SDK - poetry-lock - args: ["--directory=./"] - files: { glob: ["{pyproject.toml,poetry.lock}"] } - pass_filenames: false - priority: 50 + ## MARKDOWN + - repo: https://github.com/igorshubovych/markdownlint-cli + rev: v0.45.0 + hooks: + - id: markdownlint + priority: 30 ## CONTAINERS - repo: https://github.com/hadolint/hadolint @@ -179,27 +177,7 @@ repos: language: system types: [python] files: '.*\.py' - exclude: - { glob: ["{contrib,skills}/**", "**/.venv/**", "**/*_test.py"] } - priority: 40 - - - id: safety - name: safety - description: "Safety is a tool that checks your installed dependencies for known security vulnerabilities" - # Accepted CVEs, severity threshold, and ignore expirations live in .safety-policy.yml - entry: safety check --policy-file .safety-policy.yml - language: system - pass_filenames: false - files: - { - glob: - [ - "**/pyproject.toml", - "**/poetry.lock", - "**/requirements*.txt", - ".safety-policy.yml", - ], - } + exclude: { glob: ["{contrib,skills}/**", "**/.venv/**", "**/*_test.py"] } priority: 40 - id: vulture diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 5d375cd0df..fabae0afa5 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -11,15 +11,11 @@ build: python: "3.11" jobs: post_create_environment: - # Install poetry - # https://python-poetry.org/docs/#installing-manually - - python -m pip install poetry==2.3.4 + - python -m pip install uv==0.11.14 post_install: - # Install dependencies with 'docs' dependency group - # https://python-poetry.org/docs/managing-dependencies/#dependency-groups # VIRTUAL_ENV needs to be set manually for now. # See https://github.com/readthedocs/readthedocs.org/pull/11152/ - - VIRTUAL_ENV=${READTHEDOCS_VIRTUALENV_PATH} python -m poetry install --only=docs + - VIRTUAL_ENV=${READTHEDOCS_VIRTUALENV_PATH} uv sync --group docs --no-install-project mkdocs: configuration: mkdocs.yml diff --git a/.safety-policy.yml b/.safety-policy.yml deleted file mode 100644 index fec97e2fb9..0000000000 --- a/.safety-policy.yml +++ /dev/null @@ -1,58 +0,0 @@ -# Safety policy for `safety check` (Safety CLI 3.x, v2 schema). -# Applied in: .pre-commit-config.yaml, .github/workflows/api-security.yml, -# .github/workflows/sdk-security.yml via `--policy-file`. -# -# Validate: poetry run safety validate policy_file --path .safety-policy.yml - -security: - # Scan unpinned requirements too. Prowler pins via poetry.lock, so this is - # defensive against accidental unpinned entries. - ignore-unpinned-requirements: False - - # CVSS severity filter. 7 = report only HIGH (7.0–8.9) and CRITICAL (9.0–10.0). - # Reference: 9=CRITICAL only, 7=CRITICAL+HIGH, 4=CRITICAL+HIGH+MEDIUM. - ignore-cvss-severity-below: 7 - - # Unknown severity is unrated, not safe. Keep False so unrated CVEs still fail - # the build and get a human eye. Flip to True only if noise is unmanageable. - ignore-cvss-unknown-severity: False - - # Fail the build when a non-ignored vulnerability is found. - continue-on-vulnerability-error: False - - # Explicit accepted vulnerabilities. Each entry MUST have a reason and an - # expiry. Expired entries fail the scan, forcing re-audit. - ignore-vulnerabilities: - 77744: - reason: "Botocore requires urllib3 1.X. Remove once upgraded to urllib3 2.X." - expires: '2026-10-22' - 77745: - reason: "Botocore requires urllib3 1.X. Remove once upgraded to urllib3 2.X." - expires: '2026-10-22' - 79023: - reason: "knack ReDoS; blocked until azure-cli-core (via cartography) allows knack >=0.13.0." - expires: '2026-10-22' - 79027: - reason: "knack ReDoS; blocked until azure-cli-core (via cartography) allows knack >=0.13.0." - expires: '2026-10-22' - 86217: - reason: "alibabacloud-tea-openapi==0.4.3 blocks upgrade to cryptography >=46.0.0." - expires: '2026-10-22' - 71600: - reason: "CVE-2024-1135 false positive. Fixed in gunicorn 22.0.0; project uses 23.0.0." - expires: '2026-10-22' - 70612: - reason: "TBD - audit required. Reason not documented in prior --ignore list." - expires: '2026-07-22' - 66963: - reason: "TBD - audit required. Reason not documented in prior --ignore list." - expires: '2026-07-22' - 74429: - reason: "TBD - audit required. Reason not documented in prior --ignore list." - expires: '2026-07-22' - 76352: - reason: "TBD - audit required. Reason not documented in prior --ignore list." - expires: '2026-07-22' - 76353: - reason: "TBD - audit required. Reason not documented in prior --ignore list." - expires: '2026-07-22' diff --git a/.worktreeinclude b/.worktreeinclude index a9fce75e0f..b2828287f7 100644 --- a/.worktreeinclude +++ b/.worktreeinclude @@ -1,2 +1,3 @@ .envrc ui/.env.local +openspec/ diff --git a/AGENTS.md b/AGENTS.md index 1d7a36c667..1cf023889c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,11 +11,12 @@ Use these skills for detailed patterns on-demand: ### Generic Skills (Any Project) + | Skill | Description | URL | |-------|-------------|-----| | `typescript` | Const types, flat interfaces, utility types | [SKILL.md](skills/typescript/SKILL.md) | | `react-19` | No useMemo/useCallback, React Compiler | [SKILL.md](skills/react-19/SKILL.md) | -| `nextjs-15` | App Router, Server Actions, streaming | [SKILL.md](skills/nextjs-15/SKILL.md) | +| `nextjs-16` | App Router, Server Actions, proxy.ts, streaming | [SKILL.md](skills/nextjs-16/SKILL.md) | | `tailwind-4` | cn() utility, no var() in className | [SKILL.md](skills/tailwind-4/SKILL.md) | | `playwright` | Page Object Model, MCP workflow, selectors | [SKILL.md](skills/playwright/SKILL.md) | | `pytest` | Fixtures, mocking, markers, parametrize | [SKILL.md](skills/pytest/SKILL.md) | @@ -28,6 +29,7 @@ Use these skills for detailed patterns on-demand: | `tdd` | Test-Driven Development workflow | [SKILL.md](skills/tdd/SKILL.md) | ### Prowler-Specific Skills + | Skill | Description | URL | |-------|-------------|-----| | `prowler` | Project overview, component navigation | [SKILL.md](skills/prowler/SKILL.md) | @@ -60,11 +62,14 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST: |--------|-------| | Add changelog entry for a PR or feature | `prowler-changelog` | | Adding DRF pagination or permissions | `django-drf` | +| Adding a compliance output formatter (per-provider class + table dispatcher) | `prowler-compliance` | +| Adding indexes or constraints to database tables | `django-migration-psql` | | Adding new providers | `prowler-provider` | | Adding privilege escalation detection queries | `prowler-attack-paths-query` | | Adding services to existing providers | `prowler-provider` | | After creating/modifying a skill | `skill-sync` | -| App Router / Server Actions | `nextjs-15` | +| App Router / Server Actions | `nextjs-16` | +| Auditing check-to-requirement mappings as a cloud auditor | `prowler-compliance` | | Building AI chat features | `ai-sdk-5` | | Committing changes | `prowler-commit` | | Configuring MCP servers in agentic workflows | `gh-aw` | @@ -78,6 +83,7 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST: | Creating a git commit | `prowler-commit` | | Creating new checks | `prowler-sdk-check` | | Creating new skills | `skill-creator` | +| Creating or reviewing Django migrations | `django-migration-psql` | | Creating/modifying Prowler UI components | `prowler-ui` | | Creating/modifying models, views, serializers | `prowler-api` | | Creating/updating compliance frameworks | `prowler-compliance` | @@ -85,6 +91,7 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST: | Debugging gh-aw compilation errors | `gh-aw` | | Fill .github/pull_request_template.md (Context/Description/Steps to review/Checklist) | `prowler-pr` | | Fixing bug | `tdd` | +| Fixing compliance JSON bugs (duplicate IDs, empty Section, stale refs) | `prowler-compliance` | | General Prowler development questions | `prowler` | | Implementing JSON:API endpoints | `django-drf` | | Implementing feature | `tdd` | @@ -102,6 +109,8 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST: | Review changelog format and conventions | `prowler-changelog` | | Reviewing JSON:API compliance | `jsonapi` | | Reviewing compliance framework PRs | `prowler-compliance-review` | +| Running makemigrations or pgmakemigrations | `django-migration-psql` | +| Syncing compliance framework with upstream catalog | `prowler-compliance` | | Testing RLS tenant isolation | `prowler-test-api` | | Testing hooks or utilities | `vitest` | | Troubleshoot why a skill is missing from AGENTS.md auto-invoke | `skill-sync` | @@ -129,6 +138,7 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST: | Writing React components | `react-19` | | Writing TypeScript types/interfaces | `typescript` | | Writing Vitest tests | `vitest` | +| Writing data backfill or data migration | `django-migration-psql` | | Writing documentation | `prowler-docs` | | Writing unit tests for UI | `vitest` | @@ -140,9 +150,9 @@ Prowler is an open-source cloud security assessment tool supporting AWS, Azure, | Component | Location | Tech Stack | |-----------|----------|------------| -| SDK | `prowler/` | Python 3.10+, Poetry 2.3+ | +| SDK | `prowler/` | Python 3.10+, uv | | API | `api/` | Django 5.1, DRF, Celery | -| UI | `ui/` | Next.js 15, React 19, Tailwind 4 | +| UI | `ui/` | Next.js 16, React 19, Tailwind 4 | | MCP Server | `mcp_server/` | FastMCP, Python 3.12+ | | Dashboard | `dashboard/` | Dash, Plotly | @@ -152,13 +162,13 @@ Prowler is an open-source cloud security assessment tool supporting AWS, Azure, ```bash # Setup -poetry install --with dev -poetry run prek install +uv sync +uv run prek install # Code quality -poetry run make lint -poetry run make format -poetry run prek run --all-files +uv run make lint +uv run make format +uv run prek run --all-files ``` --- diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f530feb99a..90c1ed1954 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,13 +1,37 @@ -# Do you want to learn on how to... +# Do you want to learn on how to -- Contribute with your code or fixes to Prowler -- Create a new check for a provider -- Create a new security compliance framework -- Add a custom output format -- Add a new integration -- Contribute with documentation +- [Contribute with your code or fixes to Prowler](https://docs.prowler.com/developer-guide/introduction) +- [Create a new provider](https://docs.prowler.com/developer-guide/provider) +- [Create a new service](https://docs.prowler.com/developer-guide/services) +- [Create a new check for a provider](https://docs.prowler.com/developer-guide/checks) +- [Create a new security compliance framework](https://docs.prowler.com/developer-guide/security-compliance-framework) +- [Add a custom output format](https://docs.prowler.com/developer-guide/outputs) +- [Add a new integration](https://docs.prowler.com/developer-guide/integrations) +- [Contribute with documentation](https://docs.prowler.com/developer-guide/documentation) +- [Write unit tests](https://docs.prowler.com/developer-guide/unit-testing) +- [Write integration tests](https://docs.prowler.com/developer-guide/integration-testing) +- [Write end-to-end tests](https://docs.prowler.com/developer-guide/end2end-testing) +- [Debug Prowler](https://docs.prowler.com/developer-guide/debugging) +- [Configure checks](https://docs.prowler.com/developer-guide/configurable-checks) +- [Rename checks](https://docs.prowler.com/developer-guide/renaming-checks) +- [Follow the check metadata guidelines](https://docs.prowler.com/developer-guide/check-metadata-guidelines) +- [Extend the MCP server](https://docs.prowler.com/developer-guide/mcp-server) +- [Extend Lighthouse AI](https://docs.prowler.com/developer-guide/lighthouse-architecture) +- [Add AI skills](https://docs.prowler.com/developer-guide/ai-skills) + +Provider-specific developer notes: + +- [AWS](https://docs.prowler.com/developer-guide/aws-details) +- [Azure](https://docs.prowler.com/developer-guide/azure-details) +- [Google Cloud](https://docs.prowler.com/developer-guide/gcp-details) +- [Alibaba Cloud](https://docs.prowler.com/developer-guide/alibabacloud-details) +- [Kubernetes](https://docs.prowler.com/developer-guide/kubernetes-details) +- [Microsoft 365](https://docs.prowler.com/developer-guide/m365-details) +- [GitHub](https://docs.prowler.com/developer-guide/github-details) +- [LLM](https://docs.prowler.com/developer-guide/llm-details) Want some swag as appreciation for your contribution? -# Prowler Developer Guide -https://goto.prowler.com/devguide +## Prowler Developer Guide + + diff --git a/Dockerfile b/Dockerfile index 49fbc5356e..0ab4458fad 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ LABEL org.opencontainers.image.source="https://github.com/prowler-cloud/prowler" ARG POWERSHELL_VERSION=7.5.0 ENV POWERSHELL_VERSION=${POWERSHELL_VERSION} -ARG TRIVY_VERSION=0.69.2 +ARG TRIVY_VERSION=0.70.0 ENV TRIVY_VERSION=${TRIVY_VERSION} ARG ZIZMOR_VERSION=1.24.1 @@ -76,28 +76,28 @@ USER prowler WORKDIR /home/prowler # Copy necessary files -COPY prowler/ /home/prowler/prowler/ -COPY dashboard/ /home/prowler/dashboard/ -COPY pyproject.toml /home/prowler -COPY README.md /home/prowler/ -COPY prowler/providers/m365/lib/powershell/m365_powershell.py /home/prowler/prowler/providers/m365/lib/powershell/m365_powershell.py +COPY --chown=prowler:prowler prowler/ /home/prowler/prowler/ +COPY --chown=prowler:prowler dashboard/ /home/prowler/dashboard/ +COPY --chown=prowler:prowler pyproject.toml uv.lock /home/prowler/ +COPY --chown=prowler:prowler README.md /home/prowler/ +COPY --chown=prowler:prowler prowler/providers/m365/lib/powershell/m365_powershell.py /home/prowler/prowler/providers/m365/lib/powershell/m365_powershell.py # Install Python dependencies ENV HOME='/home/prowler' ENV PATH="${HOME}/.local/bin:${PATH}" #hadolint ignore=DL3013 RUN pip install --no-cache-dir --upgrade pip && \ - pip install --no-cache-dir poetry==2.3.4 + pip install --no-cache-dir uv==0.11.14 -RUN poetry install --compile && \ - rm -rf ~/.cache/pip +RUN uv sync --locked --compile-bytecode && \ + rm -rf ~/.cache/uv # Install PowerShell modules -RUN poetry run python prowler/providers/m365/lib/powershell/m365_powershell.py +RUN .venv/bin/python prowler/providers/m365/lib/powershell/m365_powershell.py # Remove deprecated dash dependencies RUN pip uninstall dash-html-components -y && \ pip uninstall dash-core-components -y USER prowler -ENTRYPOINT ["poetry", "run", "prowler"] +ENTRYPOINT ["/home/prowler/.venv/bin/prowler"] diff --git a/Makefile b/Makefile index 2000d6d5bd..8bdb9bf156 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ format: ## Format Code lint: ## Lint Code @echo "Running flake8..." - flake8 . --ignore=E266,W503,E203,E501,W605,E128 --exclude contrib + flake8 . --ignore=E266,W503,E203,E501,W605,E128 --exclude .venv,contrib @echo "Running black... " black --check . @echo "Running pylint..." @@ -35,7 +35,7 @@ pypi-clean: ## Delete the distribution files pypi-build: ## Build package $(MAKE) pypi-clean && \ - poetry build + uv build pypi-upload: ## Upload package python3 -m twine upload --repository pypi dist/* @@ -56,4 +56,3 @@ run-api-dev: ## Start development environment with API, PostgreSQL, Valkey, MCP, ##@ Development Environment build-and-run-api-dev: build-no-cache-dev run-api-dev - diff --git a/README.md b/README.md index 8f4565481d..ad28c81e3d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

- - + Prowler logo + Prowler logo

Prowler is the Open Cloud Security Platform trusted by thousands to automate security and compliance in any cloud environment. With hundreds of ready-to-use checks and compliance frameworks, Prowler delivers real-time, customizable monitoring and seamless integrations, making cloud security simple, scalable, and cost-effective for organizations of any size. @@ -22,8 +22,8 @@ PyPI Downloads Docker Pulls AWS ECR Gallery - - + Codecov coverage + Linux Foundation insights health score

Version @@ -36,7 +36,7 @@


- + Prowler Cloud demo

# Description @@ -104,22 +104,24 @@ Every AWS provider scan will enqueue an Attack Paths ingestion job automatically | Provider | Checks | Services | [Compliance Frameworks](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/compliance/) | [Categories](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/misc/#categories) | Support | Interface | |---|---|---|---|---|---|---| -| AWS | 595 | 84 | 43 | 17 | Official | UI, API, CLI | +| AWS | 600 | 84 | 44 | 18 | Official | UI, API, CLI | | Azure | 167 | 22 | 19 | 16 | Official | UI, API, CLI | | GCP | 102 | 18 | 17 | 12 | Official | UI, API, CLI | | Kubernetes | 83 | 7 | 7 | 11 | Official | UI, API, CLI | | GitHub | 24 | 3 | 1 | 5 | Official | UI, API, CLI | -| M365 | 101 | 10 | 4 | 10 | Official | UI, API, CLI | +| M365 | 102 | 10 | 4 | 10 | Official | UI, API, CLI | | OCI | 51 | 14 | 4 | 10 | Official | UI, API, CLI | -| Alibaba Cloud | 61 | 9 | 4 | 9 | Official | UI, API, CLI | +| Alibaba Cloud | 63 | 9 | 4 | 9 | Official | UI, API, CLI | | Cloudflare | 29 | 3 | 0 | 5 | Official | UI, API, CLI | | IaC | [See `trivy` docs.](https://trivy.dev/latest/docs/coverage/iac/) | N/A | N/A | N/A | Official | UI, API, CLI | | MongoDB Atlas | 10 | 3 | 0 | 8 | Official | UI, API, CLI | | LLM | [See `promptfoo` docs.](https://www.promptfoo.dev/docs/red-team/plugins/) | N/A | N/A | N/A | Official | CLI | | Image | N/A | N/A | N/A | N/A | Official | CLI, API | -| Google Workspace | 25 | 4 | 2 | 4 | Official | CLI | +| Google Workspace | 39 | 5 | 2 | 5 | Official | UI, API, CLI | | OpenStack | 34 | 5 | 0 | 9 | Official | UI, API, CLI | -| Vercel | 26 | 6 | 0 | 5 | Official | CLI | +| Vercel | 26 | 6 | 0 | 8 | Official | UI, API, CLI | +| Okta | 1 | 1 | 0 | 1 | Official | CLI | +| Scaleway [Contact us](https://prowler.com/contact) | 1 | 1 | 0 | 1 | Unofficial | CLI | | NHN | 6 | 2 | 1 | 0 | Unofficial | CLI | > [!Note] @@ -144,11 +146,11 @@ Prowler App offers flexible installation methods tailored to various environment ### Docker Compose -**Requirements** +#### Requirements -* `Docker Compose` installed: https://docs.docker.com/compose/install/. +- `Docker Compose` installed: https://docs.docker.com/compose/install/. -**Commands** +#### Commands ``` console VERSION=$(curl -s https://api.github.com/repos/prowler-cloud/prowler/releases/latest | jq -r .tag_name) @@ -173,20 +175,20 @@ You can find more information in the [Troubleshooting](./docs/troubleshooting.md ### From GitHub -**Requirements** +#### Requirements -* `git` installed. -* `poetry` v2 installed: [poetry installation](https://python-poetry.org/docs/#installation). -* `pnpm` installed: [pnpm installation](https://pnpm.io/installation). -* `Docker Compose` installed: https://docs.docker.com/compose/install/. +- `git` installed. +- `uv` installed: [uv installation](https://docs.astral.sh/uv/getting-started/installation/). +- `pnpm` installed: [pnpm installation](https://pnpm.io/installation). +- `Docker Compose` installed: https://docs.docker.com/compose/install/. -**Commands to run the API** +#### Commands to run the API ``` console git clone https://github.com/prowler-cloud/prowler cd prowler/api -poetry install -eval $(poetry env activate) +uv sync +source .venv/bin/activate set -a source .env docker compose up postgres valkey -d @@ -194,41 +196,36 @@ cd src/backend python manage.py migrate --database admin gunicorn -c config/guniconf.py config.wsgi:application ``` -> [!IMPORTANT] -> As of Poetry v2.0.0, the `poetry shell` command has been deprecated. Use `poetry env activate` instead for environment activation. -> -> If your Poetry version is below v2.0.0, continue using `poetry shell` to activate your environment. -> For further guidance, refer to the Poetry Environment Activation Guide https://python-poetry.org/docs/managing-environments/#activating-the-environment. > After completing the setup, access the API documentation at http://localhost:8080/api/v1/docs. -**Commands to run the API Worker** +#### Commands to run the API Worker ``` console git clone https://github.com/prowler-cloud/prowler cd prowler/api -poetry install -eval $(poetry env activate) +uv sync +source .venv/bin/activate set -a source .env cd src/backend python -m celery -A config.celery worker -l info -E ``` -**Commands to run the API Scheduler** +#### Commands to run the API Scheduler ``` console git clone https://github.com/prowler-cloud/prowler cd prowler/api -poetry install -eval $(poetry env activate) +uv sync +source .venv/bin/activate set -a source .env cd src/backend python -m celery -A config.celery beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler ``` -**Commands to run the UI** +#### Commands to run the UI ``` console git clone https://github.com/prowler-cloud/prowler @@ -240,7 +237,7 @@ pnpm start > Once configured, access the Prowler App at http://localhost:3000. Sign up using your email and password to get started. -**Pre-commit Hooks Setup** +#### Pre-commit Hooks Setup Some pre-commit hooks require tools installed on your system: @@ -260,14 +257,14 @@ prowler -v ### Containers -**Available Versions of Prowler CLI** +#### Available Versions of Prowler CLI The following versions of Prowler CLI are available, depending on your requirements: - `latest`: Synchronizes with the `master` branch. Note that this version is not stable. - `v4-latest`: Synchronizes with the `v4` branch. Note that this version is not stable. - `v3-latest`: Synchronizes with the `v3` branch. Note that this version is not stable. -- `` (release): Stable releases corresponding to specific versions. You can find the complete list of releases [here](https://github.com/prowler-cloud/prowler/releases). +- `` (release): Stable releases corresponding to specific versions. See the [complete list of Prowler releases](https://github.com/prowler-cloud/prowler/releases). - `stable`: Always points to the latest release. - `v4-stable`: Always points to the latest release for v4. - `v3-stable`: Always points to the latest release for v3. @@ -282,24 +279,18 @@ The container images are available here: ### From GitHub -Python >=3.10, <3.13 is required with pip and Poetry: +Python >=3.10, <3.13 is required with [uv](https://docs.astral.sh/uv/): ``` console git clone https://github.com/prowler-cloud/prowler cd prowler -eval $(poetry env activate) -poetry install +uv sync +source .venv/bin/activate python prowler-cli.py -v ``` > [!IMPORTANT] > To clone Prowler on Windows, configure Git to support long file paths by running the following command: `git config core.longpaths true`. -> [!IMPORTANT] -> As of Poetry v2.0.0, the `poetry shell` command has been deprecated. Use `poetry env activate` instead for environment activation. -> -> If your Poetry version is below v2.0.0, continue using `poetry shell` to activate your environment. -> For further guidance, refer to the Poetry Environment Activation Guide https://python-poetry.org/docs/managing-environments/#activating-the-environment. - # 🛡️ GitHub Action The official **Prowler GitHub Action** runs Prowler scans in your GitHub workflows using the official [`prowlercloud/prowler`](https://hub.docker.com/r/prowlercloud/prowler) Docker image. Scans run on any [supported provider](https://docs.prowler.com/user-guide/providers/), with optional [`--push-to-cloud`](https://docs.prowler.com/user-guide/tutorials/prowler-app-import-findings) to send findings to Prowler Cloud and optional SARIF upload so findings show up in the repo's **Security → Code scanning** tab and as inline PR annotations. @@ -347,7 +338,7 @@ Full configuration, per-provider authentication, and SARIF examples: [Prowler Gi ## Prowler CLI -**Running Prowler** +### Running Prowler Prowler can be executed across various environments, offering flexibility to meet your needs. It can be run from: diff --git a/action.yml b/action.yml index 3b5b8d8bdf..a93c67d4e8 100644 --- a/action.yml +++ b/action.yml @@ -167,7 +167,7 @@ runs: - name: Upload SARIF to GitHub Code Scanning if: always() && inputs.upload-sarif == 'true' && steps.find-sarif.outputs.sarif_path != '' - uses: github/codeql-action/upload-sarif@d4b3ca9fa7f69d38bfcd667bdc45bc373d16277e # v4 + uses: github/codeql-action/upload-sarif@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 with: sarif_file: ${{ steps.find-sarif.outputs.sarif_path }} category: ${{ inputs.sarif-category }} diff --git a/api/AGENTS.md b/api/AGENTS.md index 2bdee1b28c..0483e55c04 100644 --- a/api/AGENTS.md +++ b/api/AGENTS.md @@ -10,7 +10,7 @@ > - [`jsonapi`](../skills/jsonapi/SKILL.md) - Strict JSON:API v1.1 spec compliance > - [`pytest`](../skills/pytest/SKILL.md) - Generic pytest patterns -### Auto-invoke Skills +## Auto-invoke Skills When performing these actions, ALWAYS invoke the corresponding skill FIRST: @@ -81,7 +81,7 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST: ## DECISION TREES ### Serializer Selection -``` +```text Read → Serializer Create → CreateSerializer Update → UpdateSerializer @@ -89,7 +89,7 @@ Nested read → IncludeSerializer ``` ### Task vs View -``` +```text < 100ms → View > 100ms or external API → Celery task Needs retry → Celery task @@ -105,7 +105,7 @@ Django 5.1.x | DRF 3.15.x | djangorestframework-jsonapi 7.x | Celery 5.4.x | Pos ## PROJECT STRUCTURE -``` +```text api/src/backend/ ├── api/ # Main Django app │ ├── v1/ # API version 1 (views, serializers, urls) @@ -124,24 +124,24 @@ api/src/backend/ ```bash # Development -poetry run python src/backend/manage.py runserver -poetry run celery -A config.celery worker -l INFO +uv run python src/backend/manage.py runserver +uv run celery -A config.celery worker -l INFO # Database -poetry run python src/backend/manage.py makemigrations -poetry run python src/backend/manage.py migrate +uv run python src/backend/manage.py makemigrations +uv run python src/backend/manage.py migrate # Testing & Linting -poetry run pytest -x --tb=short -poetry run make lint +uv run pytest -x --tb=short +uv run make lint ``` --- ## QA CHECKLIST -- [ ] `poetry run pytest` passes -- [ ] `poetry run make lint` passes +- [ ] `uv run pytest` passes +- [ ] `uv run make lint` passes - [ ] Migrations created if models changed - [ ] New endpoints have `@extend_schema` decorators - [ ] RLS properly applied for tenant data diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index c03906d7e8..f93208f40a 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -2,12 +2,73 @@ All notable changes to the **Prowler API** are documented in this file. -## [1.27.0] (Prowler UNRELEASED) +## [1.30.0] (Prowler UNRELEASED) + +### 🔄 Changed + +- Scan finding ingestion: bulk-resolve `Resource`/`ResourceTag` rows, replace per-mapping `SELECT FOR UPDATE` with deferred `ResourceTagMapping.bulk_create(ignore_conflicts=True)`, wrap each micro-batch in a single `rls_transaction`, and raise `SCAN_DB_BATCH_SIZE` to 1000 [(#11249)](https://github.com/prowler-cloud/prowler/pull/11249) + +--- + +## [1.29.1] (Prowler v5.28.1) + +### 🐞 Fixed + +- `finding-groups` slow response with finding-level filters such as `region`; check title and description are now read from the daily summaries, which drops sorting by `check_title` [(#11326)](https://github.com/prowler-cloud/prowler/pull/11326) + +--- + +## [1.29.0] (Prowler v5.28.0) + +### 🚀 Added + +- `okta` provider support [(#11184)](https://github.com/prowler-cloud/prowler/pull/11184) +- `resource.metadata` attribute included in `/api/v1/findings?include=resources` [(#11187)](https://github.com/prowler-cloud/prowler/pull/11187) + +--- + +## [1.28.0] (Prowler v5.27.0) + +### 🚀 Added + +- GIN index on `findings(categories, resource_services, resource_regions, resource_types)` to speed up `/api/v1/finding-groups` array filters [(#11001)](https://github.com/prowler-cloud/prowler/pull/11001) +- `GET /health/live` and `GET /health/ready` Kubernetes-style probe endpoints following the IETF Health Check Response Format (`application/health+json`). Readiness verifies PostgreSQL, Valkey and Neo4j connectivity and returns 503 with per-dependency detail when any is unreachable [(#11200)](https://github.com/prowler-cloud/prowler/pull/11200) + +### 🔄 Changed + +- Replace `poetry` with `uv` as package manager [(#10775)](https://github.com/prowler-cloud/prowler/pull/10775) +- Remove orphaned `gin_resources_search_idx` declaration from `Resource.Meta.indexes` (DB index dropped in `0072_drop_unused_indexes`) [(#11001)](https://github.com/prowler-cloud/prowler/pull/11001) +- PDF compliance reports cap detail tables at 100 failed findings per check (configurable via `DJANGO_PDF_MAX_FINDINGS_PER_CHECK`) to bound worker memory on large scans [(#11160)](https://github.com/prowler-cloud/prowler/pull/11160) + +### 🐞 Fixed + +- `perform_scan_task` and `perform_scheduled_scan_task` now short-circuit with a warning and `return None` when the target provider no longer exists, instead of letting `handle_provider_deletion` raise `ProviderDeletedException`. `perform_scheduled_scan_task` also removes any orphan `PeriodicTask` it finds so beat stops re-firing scans for deleted providers. Prevents queued messages for deleted providers from being recorded as `FAILURE` [(#11185)](https://github.com/prowler-cloud/prowler/pull/11185) +- Attack Paths: `BEDROCK-001` and `BEDROCK-002` now target roles trusting `bedrock-agentcore.amazonaws.com` instead of `bedrock.amazonaws.com`, eliminating false positives against regular Bedrock service roles (Agents, Knowledge Bases, model invocation) [(#11141)](https://github.com/prowler-cloud/prowler/pull/11141) + +--- + +## [1.27.1] (Prowler v5.26.1) + +### 🐞 Fixed + +- `POST /api/v1/scans` was intermittently failing with `Scan matching query does not exist` in the `scan-perform` worker; the Celery task is now published via `transaction.on_commit` so the worker cannot read the Scan before the dispatch-wide transaction commits [(#11122)](https://github.com/prowler-cloud/prowler/pull/11122) + +--- + +## [1.27.0] (Prowler v5.26.0) ### 🚀 Added - `scan-reset-ephemeral-resources` post-scan task zeroes `failed_findings_count` for resources missing from the latest full-scope scan, keeping ephemeral resources from polluting the Resources page sort [(#10929)](https://github.com/prowler-cloud/prowler/pull/10929) +### 🔄 Changed + +- ASD Essential Eight (AWS) compliance framework support [(#10982)](https://github.com/prowler-cloud/prowler/pull/10982) + +### 🔐 Security + +- `trivy` binary from 0.69.2 to 0.70.0 and `cryptography` from 46.0.6 to 46.0.7 (transitive via prowler SDK) in the API image for CVE-2026-33186 and CVE-2026-39892 [(#10978)](https://github.com/prowler-cloud/prowler/pull/10978) + --- ## [1.26.1] (Prowler v5.25.1) diff --git a/api/Dockerfile b/api/Dockerfile index 6f8385934d..259492cb08 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -5,7 +5,7 @@ LABEL maintainer="https://github.com/prowler-cloud/api" ARG POWERSHELL_VERSION=7.5.0 ENV POWERSHELL_VERSION=${POWERSHELL_VERSION} -ARG TRIVY_VERSION=0.69.2 +ARG TRIVY_VERSION=0.70.0 ENV TRIVY_VERSION=${TRIVY_VERSION} ARG ZIZMOR_VERSION=1.24.1 @@ -14,6 +14,7 @@ ENV ZIZMOR_VERSION=${ZIZMOR_VERSION} # hadolint ignore=DL3008 RUN apt-get update && apt-get install -y --no-install-recommends \ wget \ + git \ libicu72 \ gcc \ g++ \ @@ -88,21 +89,21 @@ WORKDIR /home/prowler # Ensure output directory exists RUN mkdir -p /tmp/prowler_api_output -COPY pyproject.toml ./ +COPY --chown=prowler:prowler pyproject.toml uv.lock ./ RUN pip install --no-cache-dir --upgrade pip && \ - pip install --no-cache-dir poetry==2.3.4 + pip install --no-cache-dir uv==0.11.14 ENV PATH="/home/prowler/.local/bin:$PATH" -# Add `--no-root` to avoid installing the current project as a package -RUN poetry install --no-root && \ - rm -rf ~/.cache/pip +# Add `--no-install-project` to avoid installing the current project as a package +RUN uv sync --locked --no-install-project && \ + rm -rf ~/.cache/uv -RUN poetry run python "$(poetry env info --path)/src/prowler/prowler/providers/m365/lib/powershell/m365_powershell.py" +RUN .venv/bin/python .venv/lib/python3.12/site-packages/prowler/providers/m365/lib/powershell/m365_powershell.py -COPY src/backend/ ./backend/ -COPY docker-entrypoint.sh ./docker-entrypoint.sh +COPY --chown=prowler:prowler src/backend/ ./backend/ +COPY --chown=prowler:prowler docker-entrypoint.sh ./docker-entrypoint.sh WORKDIR /home/prowler/backend diff --git a/api/README.md b/api/README.md index 60a2669718..ea9661366f 100644 --- a/api/README.md +++ b/api/README.md @@ -2,7 +2,7 @@ This repository contains the JSON API and Task Runner components for Prowler, which facilitate a complete backend that interacts with the Prowler SDK and is used by the Prowler UI. -# Components +## Components The Prowler API is composed of the following components: - The JSON API, which is an API built with Django Rest Framework. @@ -10,13 +10,13 @@ The Prowler API is composed of the following components: - The PostgreSQL database, which is used to store the data. - The Valkey database, which is an in-memory database which is used as a message broker for the Celery workers. -## Note about Valkey +### Note about Valkey [Valkey](https://valkey.io/) is an open source (BSD) high performance key/value datastore. Valkey exposes a Redis 7.2 compliant API. Any service that exposes the Redis API can be used with Prowler API. -# Modify environment variables +## Modify environment variables Under the root path of the project, you can find a file called `.env`. This file shows all the environment variables that the project uses. You should review it and set the values for the variables you want to change. @@ -24,23 +24,22 @@ If you don’t set `DJANGO_TOKEN_SIGNING_KEY` or `DJANGO_TOKEN_VERIFYING_KEY`, t **Important note**: Every Prowler version (or repository branches and tags) could have different variables set in its `.env` file. Please use the `.env` file that corresponds with each version. -## Local deployment -Keep in mind if you export the `.env` file to use it with local deployment that you will have to do it within the context of the Poetry interpreter, not before. Otherwise, variables will not be loaded properly. +### Local deployment +Keep in mind if you export the `.env` file to use it with local deployment that you will have to do it within the context of the virtual environment, not before. Otherwise, variables will not be loaded properly. To do this, you can run: ```console -poetry shell set -a source .env ``` -# 🚀 Production deployment -## Docker deployment +## 🚀 Production deployment +### Docker deployment This method requires `docker` and `docker compose`. -### Clone the repository +#### Clone the repository ```console # HTTPS @@ -51,13 +50,13 @@ git clone git@github.com:prowler-cloud/api.git ``` -### Build the base image +#### Build the base image ```console docker compose --profile prod build ``` -### Run the production service +#### Run the production service This command will start the Django production server and the Celery worker and also the Valkey and PostgreSQL databases. @@ -69,7 +68,7 @@ You can access the server in `http://localhost:8080`. > **NOTE:** notice how the port is different. When developing using docker, the port will be `8080` to prevent conflicts. -### View the Production Server Logs +#### View the Production Server Logs To view the logs for any component (e.g., Django, Celery worker), you can use the following command with a wildcard. This command will follow logs for any container that matches the specified pattern: @@ -78,7 +77,7 @@ docker logs -f $(docker ps --format "{{.Names}}" | grep 'api-') ## Local deployment -To use this method, you'll need to set up a Python virtual environment (version ">=3.11,<3.13") and keep dependencies updated. Additionally, ensure that `poetry` and `docker compose` are installed. +To use this method, you'll need to set up a Python virtual environment (version ">=3.11,<3.13") and keep dependencies updated. Additionally, ensure that `uv` and `docker compose` are installed. ### Clone the repository @@ -90,11 +89,10 @@ git clone https://github.com/prowler-cloud/api.git git clone git@github.com:prowler-cloud/api.git ``` -### Install all dependencies with Poetry +### Install all dependencies with uv ```console -poetry install -poetry shell +uv sync ``` ## Start the PostgreSQL Database and Valkey @@ -135,13 +133,13 @@ gunicorn -c config/guniconf.py config.wsgi:application > By default, the Gunicorn server will try to use as many workers as your machine can handle. You can manually change that in the `src/backend/config/guniconf.py` file. -# 🧪 Development guide +## 🧪 Development guide -## Local deployment +### Local deployment -To use this method, you'll need to set up a Python virtual environment (version ">=3.11,<3.13") and keep dependencies updated. Additionally, ensure that `poetry` and `docker compose` are installed. +To use this method, you'll need to set up a Python virtual environment (version ">=3.11,<3.13") and keep dependencies updated. Additionally, ensure that `uv` and `docker compose` are installed. -### Clone the repository +#### Clone the repository ```console # HTTPS @@ -152,7 +150,7 @@ git clone git@github.com:prowler-cloud/api.git ``` -### Start the PostgreSQL Database and Valkey +#### Start the PostgreSQL Database and Valkey The PostgreSQL database (version 16.3) and Valkey (version 7) are required for the development environment. To make development easier, we have provided a `docker-compose` file that will start these components for you. @@ -163,16 +161,15 @@ The PostgreSQL database (version 16.3) and Valkey (version 7) are required for t docker compose up postgres valkey -d ``` -### Install the Python dependencies +#### Install the Python dependencies -> You must have Poetry installed +> You must have uv installed ```console -poetry install -poetry shell +uv sync ``` -### Apply migrations +#### Apply migrations For migrations, you need to force the `admin` database router. Assuming you have the correct environment variables and Python virtual environment, run: @@ -181,7 +178,7 @@ cd src/backend python manage.py migrate --database admin ``` -### Run the Django development server +#### Run the Django development server ```console cd src/backend @@ -191,7 +188,7 @@ python manage.py runserver You can access the server in `http://localhost:8000`. All changes in the code will be automatically reloaded in the server. -### Run the Celery worker +#### Run the Celery worker ```console python -m celery -A config.celery worker -l info -E @@ -199,11 +196,11 @@ python -m celery -A config.celery worker -l info -E The Celery worker does not detect and reload changes in the code, so you need to restart it manually when you make changes. -## Docker deployment +### Docker deployment This method requires `docker` and `docker compose`. -### Clone the repository +#### Clone the repository ```console # HTTPS @@ -214,13 +211,13 @@ git clone git@github.com:prowler-cloud/api.git ``` -### Build the base image +#### Build the base image ```console docker compose --profile dev build ``` -### Run the development service +#### Run the development service This command will start the Django development server and the Celery worker and also the Valkey and PostgreSQL databases. @@ -233,7 +230,7 @@ All changes in the code will be automatically reloaded in the server. > **NOTE:** notice how the port is different. When developing using docker, the port will be `8080` to prevent conflicts. -### View the development server logs +#### View the development server logs To view the logs for any component (e.g., Django, Celery worker), you can use the following command with a wildcard. This command will follow logs for any container that matches the specified pattern: @@ -241,41 +238,38 @@ To view the logs for any component (e.g., Django, Celery worker), you can use th docker logs -f $(docker ps --format "{{.Names}}" | grep 'api-') ``` -## Applying migrations +### Applying migrations For migrations, you need to force the `admin` database router. Assuming you have the correct environment variables and Python virtual environment, run: ```console -poetry shell cd src/backend -python manage.py migrate --database admin +uv run python manage.py migrate --database admin ``` -## Apply fixtures +### Apply fixtures Fixtures are used to populate the database with initial development data. ```console -poetry shell cd src/backend -python manage.py loaddata api/fixtures/0_dev_users.json --database admin +uv run python manage.py loaddata api/fixtures/0_dev_users.json --database admin ``` > The default credentials are `dev@prowler.com:Thisisapassword123@` or `dev2@prowler.com:Thisisapassword123@` -## Run tests +### Run tests Note that the tests will fail if you use the same `.env` file as the development environment. For best results, run in a new shell with no environment variables set. ```console -poetry shell cd src/backend -pytest +uv run pytest ``` -# Custom commands +## Custom commands Django provides a way to create custom commands that can be run from the command line. @@ -284,11 +278,10 @@ Django provides a way to create custom commands that can be run from the command To run a custom command, you need to be in the `prowler/api/src/backend` directory and run: ```console -poetry shell -python manage.py +uv run python manage.py ``` -## Generate dummy data +### Generate dummy data ```console python manage.py findings --tenant @@ -305,10 +298,10 @@ This command creates, for a given tenant, a provider, scan and a set of findings > > The last step is required to access the findings details, since the UI needs that to print all the information. -### Example +#### Example ```console -~/backend $ poetry run python manage.py findings --tenant +~/backend $ uv run python manage.py findings --tenant fffb1893-3fc7-4623-a5d9-fae47da1c528 --findings 25000 --re sources 1000 --batch 5000 --alias test-script diff --git a/api/docker-entrypoint.sh b/api/docker-entrypoint.sh index fb1e1693b4..4535fb34e2 100755 --- a/api/docker-entrypoint.sh +++ b/api/docker-entrypoint.sh @@ -5,9 +5,9 @@ apply_migrations() { echo "Applying database migrations..." # Fix Inconsistent migration history after adding sites app - poetry run python manage.py check_and_fix_socialaccount_sites_migration --database admin + uv run python manage.py check_and_fix_socialaccount_sites_migration --database admin - poetry run python manage.py migrate --database admin + uv run python manage.py migrate --database admin } apply_fixtures() { @@ -15,19 +15,19 @@ apply_fixtures() { for fixture in api/fixtures/dev/*.json; do if [ -f "$fixture" ]; then echo "Loading $fixture" - poetry run python manage.py loaddata "$fixture" --database admin + uv run python manage.py loaddata "$fixture" --database admin fi done } start_dev_server() { echo "Starting the development server..." - poetry run python manage.py runserver 0.0.0.0:"${DJANGO_PORT:-8080}" + uv run python manage.py runserver 0.0.0.0:"${DJANGO_PORT:-8080}" } start_prod_server() { echo "Starting the Gunicorn server..." - poetry run gunicorn -c config/guniconf.py config.wsgi:application + uv run gunicorn -c config/guniconf.py config.wsgi:application } resolve_worker_hostname() { @@ -47,7 +47,7 @@ resolve_worker_hostname() { start_worker() { echo "Starting the worker..." - poetry run python -m celery -A config.celery worker \ + uv run python -m celery -A config.celery worker \ -n "$(resolve_worker_hostname)" \ -l "${DJANGO_LOGGING_LEVEL:-info}" \ -Q celery,scans,scan-reports,deletion,backfill,overview,integrations,compliance,attack-paths-scans \ @@ -56,7 +56,7 @@ start_worker() { start_worker_beat() { echo "Starting the worker-beat..." - poetry run python -m celery -A config.celery beat -l "${DJANGO_LOGGING_LEVEL:-info}" --scheduler django_celery_beat.schedulers:DatabaseScheduler + uv run python -m celery -A config.celery beat -l "${DJANGO_LOGGING_LEVEL:-info}" --scheduler django_celery_beat.schedulers:DatabaseScheduler } manage_db_partitions() { @@ -64,7 +64,7 @@ manage_db_partitions() { echo "Managing DB partitions..." # For now we skip the deletion of partitions until we define the data retention policy # --yes auto approves the operation without the need of an interactive terminal - poetry run python manage.py pgpartition --using admin --skip-delete --yes + uv run python manage.py pgpartition --using admin --skip-delete --yes fi } diff --git a/api/poetry.lock b/api/poetry.lock deleted file mode 100644 index f93e0d21e6..0000000000 --- a/api/poetry.lock +++ /dev/null @@ -1,9427 +0,0 @@ -# This file is automatically @generated by Poetry 2.3.4 and should not be changed by hand. - -[[package]] -name = "about-time" -version = "4.2.1" -description = "Easily measure timing and throughput of code blocks, with beautiful human friendly representations." -optional = false -python-versions = ">=3.7, <4" -groups = ["main"] -files = [ - {file = "about-time-4.2.1.tar.gz", hash = "sha256:6a538862d33ce67d997429d14998310e1dbfda6cb7d9bbfbf799c4709847fece"}, - {file = "about_time-4.2.1-py3-none-any.whl", hash = "sha256:8bbf4c75fe13cbd3d72f49a03b02c5c7dca32169b6d49117c257e7eb3eaee341"}, -] - -[[package]] -name = "adal" -version = "1.2.7" -description = "Note: This library is already replaced by MSAL Python, available here: https://pypi.org/project/msal/ .ADAL Python remains available here as a legacy. The ADAL for Python library makes it easy for python application to authenticate to Azure Active Directory (AAD) in order to access AAD protected web resources." -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "adal-1.2.7-py2.py3-none-any.whl", hash = "sha256:2a7451ed7441ddbc57703042204a3e30ef747478eea022c70f789fc7f084bc3d"}, - {file = "adal-1.2.7.tar.gz", hash = "sha256:d74f45b81317454d96e982fd1c50e6fb5c99ac2223728aea8764433a39f566f1"}, -] - -[package.dependencies] -cryptography = ">=1.1.0" -PyJWT = ">=1.0.0,<3" -python-dateutil = ">=2.1.0,<3" -requests = ">=2.0.0,<3" - -[[package]] -name = "aioboto3" -version = "15.5.0" -description = "Async boto3 wrapper" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "aioboto3-15.5.0-py3-none-any.whl", hash = "sha256:cc880c4d6a8481dd7e05da89f41c384dbd841454fc1998ae25ca9c39201437a6"}, - {file = "aioboto3-15.5.0.tar.gz", hash = "sha256:ea8d8787d315594842fbfcf2c4dce3bac2ad61be275bc8584b2ce9a3402a6979"}, -] - -[package.dependencies] -aiobotocore = {version = "2.25.1", extras = ["boto3"]} -aiofiles = ">=23.2.1" - -[package.extras] -chalice = ["chalice (>=1.24.0)"] -s3cse = ["cryptography (>=44.0.1)"] - -[[package]] -name = "aiobotocore" -version = "2.25.1" -description = "Async client for aws services using botocore and aiohttp" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "aiobotocore-2.25.1-py3-none-any.whl", hash = "sha256:eb6daebe3cbef5b39a0bb2a97cffbe9c7cb46b2fcc399ad141f369f3c2134b1f"}, - {file = "aiobotocore-2.25.1.tar.gz", hash = "sha256:ea9be739bfd7ece8864f072ec99bb9ed5c7e78ebb2b0b15f29781fbe02daedbc"}, -] - -[package.dependencies] -aiohttp = ">=3.9.2,<4.0.0" -aioitertools = ">=0.5.1,<1.0.0" -boto3 = {version = ">=1.40.46,<1.40.62", optional = true, markers = "extra == \"boto3\""} -botocore = ">=1.40.46,<1.40.62" -jmespath = ">=0.7.1,<2.0.0" -multidict = ">=6.0.0,<7.0.0" -python-dateutil = ">=2.1,<3.0.0" -wrapt = ">=1.10.10,<2.0.0" - -[package.extras] -awscli = ["awscli (>=1.42.46,<1.42.62)"] -boto3 = ["boto3 (>=1.40.46,<1.40.62)"] -httpx = ["httpx (>=0.25.1,<0.29)"] - -[[package]] -name = "aiofiles" -version = "24.1.0" -description = "File support for asyncio." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"}, - {file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"}, -] - -[[package]] -name = "aiohappyeyeballs" -version = "2.6.1" -description = "Happy Eyeballs for asyncio" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}, - {file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"}, -] - -[[package]] -name = "aiohttp" -version = "3.13.5" -description = "Async http client/server framework (asyncio)" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "aiohttp-3.13.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:02222e7e233295f40e011c1b00e3b0bd451f22cf853a0304c3595633ee47da4b"}, - {file = "aiohttp-3.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bace460460ed20614fa6bc8cb09966c0b8517b8c58ad8046828c6078d25333b5"}, - {file = "aiohttp-3.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f546a4dc1e6a5edbb9fd1fd6ad18134550e096a5a43f4ad74acfbd834fc6670"}, - {file = "aiohttp-3.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c86969d012e51b8e415a8c6ce96f7857d6a87d6207303ab02d5d11ef0cad2274"}, - {file = "aiohttp-3.13.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b6f6cd1560c5fa427e3b6074bb24d2c64e225afbb7165008903bd42e4e33e28a"}, - {file = "aiohttp-3.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:636bc362f0c5bbc7372bc3ae49737f9e3030dbce469f0f422c8f38079780363d"}, - {file = "aiohttp-3.13.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6a7cbeb06d1070f1d14895eeeed4dac5913b22d7b456f2eb969f11f4b3993796"}, - {file = "aiohttp-3.13.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca9ef7517fd7874a1a08970ae88f497bf5c984610caa0bf40bd7e8450852b95"}, - {file = "aiohttp-3.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:019a67772e034a0e6b9b17c13d0a8fe56ad9fb150fc724b7f3ffd3724288d9e5"}, - {file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f34ecee82858e41dd217734f0c41a532bd066bcaab636ad830f03a30b2a96f2a"}, - {file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4eac02d9af4813ee289cd63a361576da36dba57f5a1ab36377bc2600db0cbb73"}, - {file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4beac52e9fe46d6abf98b0176a88154b742e878fdf209d2248e99fcdf73cd297"}, - {file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c180f480207a9b2475f2b8d8bd7204e47aec952d084b2a2be58a782ffcf96074"}, - {file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2837fb92951564d6339cedae4a7231692aa9f73cbc4fb2e04263b96844e03b4e"}, - {file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d9010032a0b9710f58012a1e9c222528763d860ba2ee1422c03473eab47703e7"}, - {file = "aiohttp-3.13.5-cp310-cp310-win32.whl", hash = "sha256:7c4b6668b2b2b9027f209ddf647f2a4407784b5d88b8be4efcc72036f365baf9"}, - {file = "aiohttp-3.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:cd3db5927bf9167d5a6157ddb2f036f6b6b0ad001ac82355d43e97a4bde76d76"}, - {file = "aiohttp-3.13.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ab7229b6f9b5c1ba4910d6c41a9eb11f543eadb3f384df1b4c293f4e73d44d6"}, - {file = "aiohttp-3.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f14c50708bb156b3a3ca7230b3d820199d56a48e3af76fa21c2d6087190fe3d"}, - {file = "aiohttp-3.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7d2f8616f0ff60bd332022279011776c3ac0faa0f1b463f7bb12326fbc97a1c"}, - {file = "aiohttp-3.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2567b72e1ffc3ab25510db43f355b29eeada56c0a622e58dcdb19530eb0a3cb"}, - {file = "aiohttp-3.13.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fb0540c854ac9c0c5ad495908fdfd3e332d553ec731698c0e29b1877ba0d2ec6"}, - {file = "aiohttp-3.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9883051c6972f58bfc4ebb2116345ee2aa151178e99c3f2b2bbe2af712abd13"}, - {file = "aiohttp-3.13.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2294172ce08a82fb7c7273485895de1fa1186cc8294cfeb6aef4af42ad261174"}, - {file = "aiohttp-3.13.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a807cabd5115fb55af198b98178997a5e0e57dead43eb74a93d9c07d6d4a7dc"}, - {file = "aiohttp-3.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6d0d932e0f39c02b80744273cd5c388a2d9bc07760a03164f229c8e02662f6"}, - {file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60869c7ac4aaabe7110f26499f3e6e5696eae98144735b12a9c3d9eae2b51a49"}, - {file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:26d2f8546f1dfa75efa50c3488215a903c0168d253b75fba4210f57ab77a0fb8"}, - {file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1162a1492032c82f14271e831c8f4b49f2b6078f4f5fc74de2c912fa225d51d"}, - {file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8b14eb3262fad0dc2f89c1a43b13727e709504972186ff6a99a3ecaa77102b6c"}, - {file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ca9ac61ac6db4eb6c2a0cd1d0f7e1357647b638ccc92f7e9d8d133e71ed3c6ac"}, - {file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7996023b2ed59489ae4762256c8516df9820f751cf2c5da8ed2fb20ee50abab3"}, - {file = "aiohttp-3.13.5-cp311-cp311-win32.whl", hash = "sha256:77dfa48c9f8013271011e51c00f8ada19851f013cde2c48fca1ba5e0caf5bb06"}, - {file = "aiohttp-3.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:d3a4834f221061624b8887090637db9ad4f61752001eae37d56c52fddade2dc8"}, - {file = "aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9"}, - {file = "aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416"}, - {file = "aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2"}, - {file = "aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4"}, - {file = "aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9"}, - {file = "aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5"}, - {file = "aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e"}, - {file = "aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1"}, - {file = "aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286"}, - {file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9"}, - {file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88"}, - {file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3"}, - {file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b"}, - {file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe"}, - {file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14"}, - {file = "aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3"}, - {file = "aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1"}, - {file = "aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61"}, - {file = "aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832"}, - {file = "aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9"}, - {file = "aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090"}, - {file = "aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b"}, - {file = "aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a"}, - {file = "aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8"}, - {file = "aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665"}, - {file = "aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540"}, - {file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb"}, - {file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46"}, - {file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8"}, - {file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d"}, - {file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6"}, - {file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c"}, - {file = "aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc"}, - {file = "aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83"}, - {file = "aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c"}, - {file = "aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be"}, - {file = "aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25"}, - {file = "aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56"}, - {file = "aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2"}, - {file = "aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a"}, - {file = "aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be"}, - {file = "aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b"}, - {file = "aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94"}, - {file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d"}, - {file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7"}, - {file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772"}, - {file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5"}, - {file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1"}, - {file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b"}, - {file = "aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3"}, - {file = "aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162"}, - {file = "aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a"}, - {file = "aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254"}, - {file = "aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36"}, - {file = "aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f"}, - {file = "aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800"}, - {file = "aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf"}, - {file = "aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b"}, - {file = "aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a"}, - {file = "aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8"}, - {file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be"}, - {file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b"}, - {file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6"}, - {file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037"}, - {file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500"}, - {file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9"}, - {file = "aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8"}, - {file = "aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9"}, - {file = "aiohttp-3.13.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:347542f0ea3f95b2a955ee6656461fa1c776e401ac50ebce055a6c38454a0adf"}, - {file = "aiohttp-3.13.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:178c7b5e62b454c2bc790786e6058c3cc968613b4419251b478c153a4aec32b1"}, - {file = "aiohttp-3.13.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af545c2cffdb0967a96b6249e6f5f7b0d92cdfd267f9d5238d5b9ca63e8edb10"}, - {file = "aiohttp-3.13.5-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:206b7b3ef96e4ce211754f0cd003feb28b7d81f0ad26b8d077a5d5161436067f"}, - {file = "aiohttp-3.13.5-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ee5e86776273de1795947d17bddd6bb19e0365fd2af4289c0d2c5454b6b1d36b"}, - {file = "aiohttp-3.13.5-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:95d14ca7abefde230f7639ec136ade282655431fd5db03c343b19dda72dd1643"}, - {file = "aiohttp-3.13.5-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:912d4b6af530ddb1338a66229dac3a25ff11d4448be3ec3d6340583995f56031"}, - {file = "aiohttp-3.13.5-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e999f0c88a458c836d5fb521814e92ed2172c649200336a6df514987c1488258"}, - {file = "aiohttp-3.13.5-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39380e12bd1f2fdab4285b6e055ad48efbaed5c836433b142ed4f5b9be71036a"}, - {file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9efcc0f11d850cefcafdd9275b9576ad3bfb539bed96807663b32ad99c4d4b88"}, - {file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:147b4f501d0292077f29d5268c16bb7c864a1f054d7001c4c1812c0421ea1ed0"}, - {file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d147004fede1b12f6013a6dbb2a26a986a671a03c6ea740ddc76500e5f1c399f"}, - {file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:9277145d36a01653863899c665243871434694bcc3431922c3b35c978061bdb8"}, - {file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4e704c52438f66fdd89588346183d898bb42167cf88f8b7ff1c0f9fc957c348f"}, - {file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a8a4d3427e8de1312ddf309cc482186466c79895b3a139fed3259fc01dfa9a5b"}, - {file = "aiohttp-3.13.5-cp39-cp39-win32.whl", hash = "sha256:6f497a6876aa4b1a102b04996ce4c1170c7040d83faa9387dd921c16e30d5c83"}, - {file = "aiohttp-3.13.5-cp39-cp39-win_amd64.whl", hash = "sha256:cb979826071c0986a5f08333a36104153478ce6018c58cba7f9caddaf63d5d67"}, - {file = "aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1"}, -] - -[package.dependencies] -aiohappyeyeballs = ">=2.5.0" -aiosignal = ">=1.4.0" -attrs = ">=17.3.0" -frozenlist = ">=1.1.1" -multidict = ">=4.5,<7.0" -propcache = ">=0.2.0" -yarl = ">=1.17.0,<2.0" - -[package.extras] -speedups = ["Brotli (>=1.2) ; platform_python_implementation == \"CPython\"", "aiodns (>=3.3.0)", "backports.zstd ; platform_python_implementation == \"CPython\" and python_version < \"3.14\"", "brotlicffi (>=1.2) ; platform_python_implementation != \"CPython\""] - -[[package]] -name = "aioitertools" -version = "0.13.0" -description = "itertools and builtins for AsyncIO and mixed iterables" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "aioitertools-0.13.0-py3-none-any.whl", hash = "sha256:0be0292b856f08dfac90e31f4739432f4cb6d7520ab9eb73e143f4f2fa5259be"}, - {file = "aioitertools-0.13.0.tar.gz", hash = "sha256:620bd241acc0bbb9ec819f1ab215866871b4bbd1f73836a55f799200ee86950c"}, -] - -[[package]] -name = "aiosignal" -version = "1.4.0" -description = "aiosignal: a list of registered asynchronous callbacks" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e"}, - {file = "aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7"}, -] - -[package.dependencies] -frozenlist = ">=1.1.0" -typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} - -[[package]] -name = "alibabacloud-actiontrail20200706" -version = "2.4.1" -description = "Alibaba Cloud ActionTrail (20200706) SDK Library for Python" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "alibabacloud_actiontrail20200706-2.4.1-py3-none-any.whl", hash = "sha256:5dee0009db9b7cba182fbac742820f6a949287a8faafb843b5107f7dc89136da"}, - {file = "alibabacloud_actiontrail20200706-2.4.1.tar.gz", hash = "sha256:b65c6b37a96443fbe625dd5a4dd1be52a7476006a411db75206908b11588ffa8"}, -] - -[package.dependencies] -alibabacloud-endpoint-util = ">=0.0.4,<1.0.0" -alibabacloud-openapi-util = ">=0.2.2,<1.0.0" -alibabacloud-tea-openapi = ">=0.3.16,<1.0.0" -alibabacloud-tea-util = ">=0.3.13,<1.0.0" - -[[package]] -name = "alibabacloud-credentials" -version = "1.0.3" -description = "The alibabacloud credentials module of alibabaCloud Python SDK." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "alibabacloud-credentials-1.0.3.tar.gz", hash = "sha256:9d8707e96afc6f348e23f5677ed15a21c2dfce7cfe6669776548ee4c80e1dfaf"}, - {file = "alibabacloud_credentials-1.0.3-py3-none-any.whl", hash = "sha256:30c8302f204b663c655d97e1c283ee9f9f84a6257d7901b931477d6cf34445a8"}, -] - -[package.dependencies] -aiofiles = ">=22.1.0,<25.0.0" -alibabacloud-credentials-api = ">=1.0.0,<2.0.0" -alibabacloud-tea = ">=0.4.0" -APScheduler = ">=3.10.0,<4.0.0" - -[[package]] -name = "alibabacloud-credentials-api" -version = "1.0.0" -description = "Alibaba Cloud Gateway SPI SDK Library for Python" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "alibabacloud-credentials-api-1.0.0.tar.gz", hash = "sha256:8c340038d904f0218d7214a8f4088c31912bfcf279af2cbc7d9be4897a97dd2f"}, -] - -[[package]] -name = "alibabacloud-cs20151215" -version = "6.1.0" -description = "Alibaba Cloud CS (20151215) SDK Library for Python" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "alibabacloud_cs20151215-6.1.0-py3-none-any.whl", hash = "sha256:75e90b1bb9acca2236244bb0e44234ca4805d456ea4303ba4225ac15152a458e"}, - {file = "alibabacloud_cs20151215-6.1.0.tar.gz", hash = "sha256:5b3d99306701bf499ddd57cd9f2905b7721cb1bb4bb38ffe4d051f7b4e80e355"}, -] - -[package.dependencies] -alibabacloud-endpoint-util = ">=0.0.4,<1.0.0" -alibabacloud-openapi-util = ">=0.2.2,<1.0.0" -alibabacloud-tea-openapi = ">=0.3.16,<1.0.0" -alibabacloud-tea-util = ">=0.3.13,<1.0.0" - -[[package]] -name = "alibabacloud-darabonba-array" -version = "0.1.0" -description = "Alibaba Cloud Darabonba Array SDK Library for Python" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "alibabacloud_darabonba_array-0.1.0.tar.gz", hash = "sha256:7f9a7c632518ff4f0cebb0d4e825a48c12e7cf0b9016ea25054dd73732e155aa"}, -] - -[[package]] -name = "alibabacloud-darabonba-encode-util" -version = "0.0.2" -description = "Darabonba Util Library for Alibaba Cloud Python SDK" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "alibabacloud_darabonba_encode_util-0.0.2.tar.gz", hash = "sha256:f1c484f276d60450fa49b4b2987194e741fcb2f7faae7f287c0ae65abc85fd4d"}, -] - -[[package]] -name = "alibabacloud-darabonba-map" -version = "0.0.1" -description = "Alibaba Cloud Darabonba Map SDK Library for Python" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "alibabacloud_darabonba_map-0.0.1.tar.gz", hash = "sha256:adb17384658a1a8f72418f1838d4b6a5fd2566bfd392a3ef06d9dbb0a595a23f"}, -] - -[[package]] -name = "alibabacloud-darabonba-signature-util" -version = "0.0.4" -description = "Darabonba Util Library for Alibaba Cloud Python SDK" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "alibabacloud_darabonba_signature_util-0.0.4.tar.gz", hash = "sha256:71d79b2ae65957bcfbf699ced894fda782b32f9635f1616635533e5a90d5feb0"}, -] - -[package.dependencies] -cryptography = ">=3.0.0" - -[[package]] -name = "alibabacloud-darabonba-string" -version = "0.0.4" -description = "Alibaba Cloud Darabonba String Library for Python" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "alibabacloud-darabonba-string-0.0.4.tar.gz", hash = "sha256:ec6614c0448dadcbc5e466485838a1f8cfdd911135bea739e20b14511270c6f7"}, -] - -[[package]] -name = "alibabacloud-darabonba-time" -version = "0.0.1" -description = "Alibaba Cloud Darabonba Time SDK Library for Python" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "alibabacloud_darabonba_time-0.0.1.tar.gz", hash = "sha256:0ad9c7b0696570d1a3f40106cc7777f755fd92baa0d1dcab5b7df78dde5b922d"}, -] - -[[package]] -name = "alibabacloud-ecs20140526" -version = "7.2.5" -description = "Alibaba Cloud Elastic Compute Service (20140526) SDK Library for Python" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "alibabacloud_ecs20140526-7.2.5-py3-none-any.whl", hash = "sha256:10bda5e185f6ba899e7d51477373595c629d66db7530a8a37433fb4e9034a96f"}, - {file = "alibabacloud_ecs20140526-7.2.5.tar.gz", hash = "sha256:2abbe630ce42d69061821f38950b938c5982cc31902ccd7132d05be328765a55"}, -] - -[package.dependencies] -alibabacloud-endpoint-util = ">=0.0.4,<1.0.0" -alibabacloud-openapi-util = ">=0.2.2,<1.0.0" -alibabacloud-tea-openapi = ">=0.3.16,<1.0.0" -alibabacloud-tea-util = ">=0.3.13,<1.0.0" - -[[package]] -name = "alibabacloud-endpoint-util" -version = "0.0.4" -description = "The endpoint-util module of alibabaCloud Python SDK." -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "alibabacloud_endpoint_util-0.0.4.tar.gz", hash = "sha256:a593eb8ddd8168d5dc2216cd33111b144f9189fcd6e9ca20e48f358a739bbf90"}, -] - -[[package]] -name = "alibabacloud-gateway-oss" -version = "0.0.17" -description = "Alibaba Cloud OSS SDK Library for Python" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "alibabacloud_gateway_oss-0.0.17.tar.gz", hash = "sha256:8c4b66c8c7dd285fc210ee232ab3f062b5573258752804d19382000746531e29"}, -] - -[package.dependencies] -alibabacloud_credentials = ">=0.3.5" -alibabacloud_darabonba_array = ">=0.1.0,<1.0.0" -alibabacloud_darabonba_encode_util = ">=0.0.2,<1.0.0" -alibabacloud_darabonba_map = ">=0.0.1,<1.0.0" -alibabacloud_darabonba_signature_util = ">=0.0.4,<1.0.0" -alibabacloud_darabonba_string = ">=0.0.4,<1.0.0" -alibabacloud_darabonba_time = ">=0.0.1,<1.0.0" -alibabacloud_gateway_oss_util = ">=0.0.3,<1.0.0" -alibabacloud_gateway_spi = ">=0.0.1,<1.0.0" -alibabacloud_openapi_util = ">=0.2.1,<1.0.0" -alibabacloud_oss_util = ">=0.0.5,<1.0.0" -alibabacloud_tea_util = ">=0.3.11,<1.0.0" -alibabacloud_tea_xml = ">=0.0.2,<1.0.0" - -[[package]] -name = "alibabacloud-gateway-oss-util" -version = "0.0.3" -description = "Alibaba Cloud OSS Util Library for Python" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "alibabacloud_gateway_oss_util-0.0.3.tar.gz", hash = "sha256:5eb7fa450dc7350d5c71577974b9d7f489479e5c5ec7efc1c5376385e8c1c0a5"}, -] - -[[package]] -name = "alibabacloud-gateway-sls" -version = "0.4.0" -description = "Alibaba Cloud SLS Gateway Library for Python" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "alibabacloud_gateway_sls-0.4.0-py3-none-any.whl", hash = "sha256:a0299a83a5528025983b42b7533a28028461bced5e180a66f97999e0134760a6"}, - {file = "alibabacloud_gateway_sls-0.4.0.tar.gz", hash = "sha256:9d2aceb377c9b3ed0558149fda16fe39fa114cc0a22e22a88dc76efdda34633b"}, -] - -[package.dependencies] -alibabacloud-credentials = ">=1.0.2,<2.0.0" -alibabacloud-darabonba-array = ">=0.1.0,<1.0.0" -alibabacloud-darabonba-encode-util = ">=0.0.2,<1.0.0" -alibabacloud-darabonba-map = ">=0.0.1,<1.0.0" -alibabacloud-darabonba-signature-util = ">=0.0.4,<1.0.0" -alibabacloud-darabonba-string = ">=0.0.4,<1.0.0" -alibabacloud-gateway-sls-util = ">=0.4.0,<1.0.0" -alibabacloud-gateway-spi = ">=0.0.2,<1.0.0" -alibabacloud-openapi-util = ">=0.2.2,<1.0.0" -alibabacloud-tea-util = ">=0.3.13,<1.0.0" - -[[package]] -name = "alibabacloud-gateway-sls-util" -version = "0.4.0" -description = "Alibaba Cloud SLS Util Library for Python" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "alibabacloud_gateway_sls_util-0.4.0-py3-none-any.whl", hash = "sha256:c91ab7fe55af526a01d25b0d431088c4d241b160db055da3d8cb7330bd74595a"}, - {file = "alibabacloud_gateway_sls_util-0.4.0.tar.gz", hash = "sha256:f8b683a36a2ae3fe9a8225d3d97773ea769bdf9cdf4f4d033eab2eb6062ddd1f"}, -] - -[package.dependencies] -aliyun-log-fastpb = ">=0.2.0" -lz4 = ">=4.3.2" -zstd = ">=1.5.5.1" - -[[package]] -name = "alibabacloud-gateway-spi" -version = "0.0.3" -description = "Alibaba Cloud Gateway SPI SDK Library for Python" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "alibabacloud_gateway_spi-0.0.3.tar.gz", hash = "sha256:10d1c53a3fc5f87915fbd6b4985b98338a776e9b44a0263f56643c5048223b8b"}, -] - -[package.dependencies] -alibabacloud_credentials = ">=0.3.4" - -[[package]] -name = "alibabacloud-openapi-util" -version = "0.2.4" -description = "Aliyun Tea OpenApi Library for Python" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "alibabacloud_openapi_util-0.2.4-py3-none-any.whl", hash = "sha256:a2474f230b5965ae9a8c286e0dc86132a887928d02d20b8182656cf6b1b6c5bd"}, - {file = "alibabacloud_openapi_util-0.2.4.tar.gz", hash = "sha256:87022b9dcb7593a601f7a40ca698227ac3ccb776b58cb7b06b8dc7f510995c34"}, -] - -[package.dependencies] -alibabacloud-tea-util = ">=0.3.13,<1.0.0" -cryptography = ">=3.0.0" - -[[package]] -name = "alibabacloud-oss-util" -version = "0.0.6" -description = "The oss util module of alibabaCloud Python SDK." -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "alibabacloud_oss_util-0.0.6.tar.gz", hash = "sha256:d3ecec36632434bd509a113e8cf327dc23e830ac8d9dd6949926f4e334c8b5d6"}, -] - -[package.dependencies] -alibabacloud-tea = "*" - -[[package]] -name = "alibabacloud-oss20190517" -version = "1.0.6" -description = "Alibaba Cloud Object Storage Service (20190517) SDK Library for Python" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "alibabacloud_oss20190517-1.0.6-py3-none-any.whl", hash = "sha256:365fda353de6658a1a289f4d70dcd0394e2a8e2921b6b5834ba6d9772121d2f6"}, - {file = "alibabacloud_oss20190517-1.0.6.tar.gz", hash = "sha256:7cd0fb16af613ceb38d2e0e529aa1f58038c7cf59eb67c8c8775ae44ea717852"}, -] - -[package.dependencies] -alibabacloud-gateway-oss = ">=0.0.9,<1.0.0" -alibabacloud-gateway-spi = ">=0.0.1,<1.0.0" -alibabacloud-openapi-util = ">=0.2.1,<1.0.0" -alibabacloud-tea-openapi = ">=0.3.6,<1.0.0" -alibabacloud-tea-util = ">=0.3.11,<1.0.0" - -[[package]] -name = "alibabacloud-ram20150501" -version = "1.2.0" -description = "Alibaba Cloud Resource Access Management (20150501) SDK Library for Python" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "alibabacloud_ram20150501-1.2.0-py3-none-any.whl", hash = "sha256:03a0f2a0259848787c1f74e802b486184a88e04183486bd9398766971e5eb00a"}, - {file = "alibabacloud_ram20150501-1.2.0.tar.gz", hash = "sha256:6253513c8880769f4fd5b36fedddb362a9ca628ad9ae9c05c0eeacf5fbc95b42"}, -] - -[package.dependencies] -alibabacloud-endpoint-util = ">=0.0.4,<1.0.0" -alibabacloud-openapi-util = ">=0.2.2,<1.0.0" -alibabacloud-tea-openapi = ">=0.3.15,<1.0.0" -alibabacloud-tea-util = ">=0.3.13,<1.0.0" - -[[package]] -name = "alibabacloud-rds20140815" -version = "12.0.0" -description = "Alibaba Cloud rds (20140815) SDK Library for Python" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "alibabacloud_rds20140815-12.0.0-py3-none-any.whl", hash = "sha256:0bd7e2018a428d86b1b0681087336e74665b48fc3eb0a13c4f4377ed5eab2b08"}, - {file = "alibabacloud_rds20140815-12.0.0.tar.gz", hash = "sha256:e7421d94f18a914c0a06b0e7fad0daff557713f1c97d415d463a78c1270e9b98"}, -] - -[package.dependencies] -alibabacloud-endpoint-util = ">=0.0.4,<1.0.0" -alibabacloud-openapi-util = ">=0.2.2,<1.0.0" -alibabacloud-tea-openapi = ">=0.3.15,<1.0.0" -alibabacloud-tea-util = ">=0.3.13,<1.0.0" - -[[package]] -name = "alibabacloud-sas20181203" -version = "6.1.0" -description = "Alibaba Cloud Threat Detection (20181203) SDK Library for Python" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "alibabacloud_sas20181203-6.1.0-py3-none-any.whl", hash = "sha256:1ad735332c50c7961be036b17420d56b5ec3b5557e3aea1daa19491e8b75da20"}, - {file = "alibabacloud_sas20181203-6.1.0.tar.gz", hash = "sha256:e49ffd53e630274a8bf5a8299ca753023ad118510c80f6d9c6fb018b7479bf37"}, -] - -[package.dependencies] -alibabacloud-endpoint-util = ">=0.0.4,<1.0.0" -alibabacloud-openapi-util = ">=0.2.2,<1.0.0" -alibabacloud-tea-openapi = ">=0.3.16,<1.0.0" -alibabacloud-tea-util = ">=0.3.13,<1.0.0" - -[[package]] -name = "alibabacloud-sls20201230" -version = "5.9.0" -description = "Alibaba Cloud Log Service (20201230) SDK Library for Python" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "alibabacloud_sls20201230-5.9.0-py3-none-any.whl", hash = "sha256:c4ae14096817a9686af5a0ae2389f1f6a8781e60b9edb8643445250cf15c26f1"}, - {file = "alibabacloud_sls20201230-5.9.0.tar.gz", hash = "sha256:bea830b64fbc7ed1719ba386ceeefb120f08d705f03eb0e02409dc6f12a291da"}, -] - -[package.dependencies] -alibabacloud-gateway-sls = ">=0.3.0,<1.0.0" -alibabacloud-openapi-util = ">=0.2.2,<1.0.0" -alibabacloud-tea-openapi = ">=0.3.16,<1.0.0" -alibabacloud-tea-util = ">=0.3.13,<1.0.0" - -[[package]] -name = "alibabacloud-sts20150401" -version = "1.1.6" -description = "Alibaba Cloud Sts (20150401) SDK Library for Python" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "alibabacloud_sts20150401-1.1.6-py3-none-any.whl", hash = "sha256:627f5ca1f86e19b0bf8ce0e99071a36fb65579fad9256fbee38fdc8d500598e9"}, - {file = "alibabacloud_sts20150401-1.1.6.tar.gz", hash = "sha256:c2529b41e0e4531e21cb393e4df346e19fd6d54cc6337d1138dbcd2191438d4c"}, -] - -[package.dependencies] -alibabacloud-endpoint-util = ">=0.0.4,<1.0.0" -alibabacloud-openapi-util = ">=0.2.2,<1.0.0" -alibabacloud-tea-openapi = ">=0.3.15,<1.0.0" -alibabacloud-tea-util = ">=0.3.13,<1.0.0" - -[[package]] -name = "alibabacloud-tea" -version = "0.4.3" -description = "The tea module of alibabaCloud Python SDK." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "alibabacloud-tea-0.4.3.tar.gz", hash = "sha256:ec8053d0aa8d43ebe1deb632d5c5404339b39ec9a18a0707d57765838418504a"}, -] - -[package.dependencies] -aiohttp = ">=3.7.0,<4.0.0" -requests = ">=2.21.0,<3.0.0" - -[[package]] -name = "alibabacloud-tea-openapi" -version = "0.4.4" -description = "Alibaba Cloud openapi SDK Library for Python" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "alibabacloud_tea_openapi-0.4.4-py3-none-any.whl", hash = "sha256:cea6bc1fe35b0319a8752cb99eb0ecb0dab7ca1a71b99c12970ba0867410995f"}, - {file = "alibabacloud_tea_openapi-0.4.4.tar.gz", hash = "sha256:1b0917bc03cd49417da64945e92731716d53e2eb8707b235f54e45b7473221ce"}, -] - -[package.dependencies] -alibabacloud-credentials = ">=1.0.2,<2.0.0" -alibabacloud-gateway-spi = ">=0.0.2,<1.0.0" -alibabacloud-tea-util = ">=0.3.13,<1.0.0" -cryptography = {version = ">=3.0.0,<47.0.0", markers = "python_version >= \"3.8\""} -darabonba-core = ">=1.0.3,<2.0.0" - -[[package]] -name = "alibabacloud-tea-util" -version = "0.3.14" -description = "The tea-util module of alibabaCloud Python SDK." -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "alibabacloud_tea_util-0.3.14-py3-none-any.whl", hash = "sha256:10d3e5c340d8f7ec69dd27345eb2fc5a1dab07875742525edf07bbe86db93bfe"}, - {file = "alibabacloud_tea_util-0.3.14.tar.gz", hash = "sha256:708e7c9f64641a3c9e0e566365d2f23675f8d7c2a3e2971d9402ceede0408cdb"}, -] - -[package.dependencies] -alibabacloud-tea = ">=0.3.3" - -[[package]] -name = "alibabacloud-tea-xml" -version = "0.0.3" -description = "The tea-xml module of alibabaCloud Python SDK." -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "alibabacloud_tea_xml-0.0.3.tar.gz", hash = "sha256:979cb51fadf43de77f41c69fc69c12529728919f849723eb0cd24eb7b048a90c"}, -] - -[package.dependencies] -alibabacloud-tea = ">=0.4.0" - -[[package]] -name = "alibabacloud-vpc20160428" -version = "6.13.0" -description = "Alibaba Cloud Virtual Private Cloud (20160428) SDK Library for Python" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "alibabacloud_vpc20160428-6.13.0-py3-none-any.whl", hash = "sha256:933cf1e74322a20a2df27ca6323760d857744a4246eeadc9fb3eae01322fb1c6"}, - {file = "alibabacloud_vpc20160428-6.13.0.tar.gz", hash = "sha256:daf00679a83d422799f9fcf263739fe1f360641675843cbfbe623833fc8b1681"}, -] - -[package.dependencies] -alibabacloud-endpoint-util = ">=0.0.4,<1.0.0" -alibabacloud-openapi-util = ">=0.2.2,<1.0.0" -alibabacloud-tea-openapi = ">=0.3.16,<1.0.0" -alibabacloud-tea-util = ">=0.3.13,<1.0.0" - -[[package]] -name = "alive-progress" -version = "3.3.0" -description = "A new kind of Progress Bar, with real-time throughput, ETA, and very cool animations!" -optional = false -python-versions = "<4,>=3.9" -groups = ["main"] -files = [ - {file = "alive-progress-3.3.0.tar.gz", hash = "sha256:457dd2428b48dacd49854022a46448d236a48f1b7277874071c39395307e830c"}, - {file = "alive_progress-3.3.0-py3-none-any.whl", hash = "sha256:63dd33bb94cde15ad9e5b666dbba8fedf71b72a4935d6fb9a92931e69402c9ff"}, -] - -[package.dependencies] -about-time = "4.2.1" -graphemeu = "0.7.2" - -[[package]] -name = "aliyun-log-fastpb" -version = "0.2.0" -description = "Fast protobuf serialization for Aliyun Log using PyO3 and quick-protobuf" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "aliyun_log_fastpb-0.2.0-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:51633d92d2b349aed4843c0b503454fb4f7d73eeaaa54f82aa5a36c10c064ef5"}, - {file = "aliyun_log_fastpb-0.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d2984aafc61ccbbf1db2589ce90b6d5a26e72dba137fb1fdf7f61ce3faa967c0"}, - {file = "aliyun_log_fastpb-0.2.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:181fc61ac9934f58b0880fa5617a4a4dc709dba09f8be95b5a71e828f2e48053"}, - {file = "aliyun_log_fastpb-0.2.0-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12b8bfddf0bc5450f16f1954c6387a73da124fae10d1205a17a0117e66bb56db"}, - {file = "aliyun_log_fastpb-0.2.0-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8fbc83cbaa51d332e5e68871c1200014f1f3de54a8cba4fb55a634ee145cd4e4"}, - {file = "aliyun_log_fastpb-0.2.0-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42a86a6e11dd227d595fa23f69d30588446af19d045d1003bd1b66b5c9a55485"}, - {file = "aliyun_log_fastpb-0.2.0-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd92c0b84ba300c1d1c227204c5f2fff243cea80bc3f9399293385e87c82ee3e"}, - {file = "aliyun_log_fastpb-0.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7c07a6d81a3eab6666949240da305236ed2350c305154d7e39fcc121fc52291"}, - {file = "aliyun_log_fastpb-0.2.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2cff4fbdd0edff94adcee1dcabf16daacb5d336a12fc897887aa6e4f0ad25152"}, - {file = "aliyun_log_fastpb-0.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5a451809e2a062accbb8dae8750e507e58806e4a8da48d69215cdeef428e9d63"}, - {file = "aliyun_log_fastpb-0.2.0-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:61f09df30232f1f5628d13310cf0e175171399ea1c75a8470e9f9d97b045bfb5"}, - {file = "aliyun_log_fastpb-0.2.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:a5fbf0d41d8c0c964a3dc8dd0ee2e732f876b803e0ed3432550ef3b84dde84f1"}, - {file = "aliyun_log_fastpb-0.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ae2f84ed0777e00045791044a56413f370afbd5b061505f5ded540c04b19c58e"}, - {file = "aliyun_log_fastpb-0.2.0-cp37-abi3-win32.whl", hash = "sha256:967f9656c805602fd9be07d8c2756ad89204c852c99689c3c71aa035416ef42a"}, - {file = "aliyun_log_fastpb-0.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:bbdcf7b85f0f3437c2a8e8a1db0ef5584d21468b7c7a358269a4c651c84f4a54"}, - {file = "aliyun_log_fastpb-0.2.0.tar.gz", hash = "sha256:91c714e76fb941c9a0db6b1aa1f4c56cb1626254ff5444c1179860f5e5b63d93"}, -] - -[[package]] -name = "amqp" -version = "5.3.1" -description = "Low-level AMQP client for Python (fork of amqplib)." -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2"}, - {file = "amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432"}, -] - -[package.dependencies] -vine = ">=5.0.0,<6.0.0" - -[[package]] -name = "annotated-types" -version = "0.7.0" -description = "Reusable constraint types to use with typing.Annotated" -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, - {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, -] - -[[package]] -name = "anyio" -version = "4.12.1" -description = "High-level concurrency and networking framework on top of asyncio or Trio" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c"}, - {file = "anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703"}, -] - -[package.dependencies] -idna = ">=2.8" -typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} - -[package.extras] -trio = ["trio (>=0.31.0) ; python_version < \"3.10\"", "trio (>=0.32.0) ; python_version >= \"3.10\""] - -[[package]] -name = "applicationinsights" -version = "0.11.10" -description = "This project extends the Application Insights API surface to support Python." -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "applicationinsights-0.11.10-py2.py3-none-any.whl", hash = "sha256:e89a890db1c6906b6a7d0bcfd617dac83974773c64573147c8d6654f9cf2a6ea"}, - {file = "applicationinsights-0.11.10.tar.gz", hash = "sha256:0b761f3ef0680acf4731906dfc1807faa6f2a57168ae74592db0084a6099f7b3"}, -] - -[[package]] -name = "apscheduler" -version = "3.11.2" -description = "In-process task scheduler with Cron-like capabilities" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "apscheduler-3.11.2-py3-none-any.whl", hash = "sha256:ce005177f741409db4e4dd40a7431b76feb856b9dd69d57e0da49d6715bfd26d"}, - {file = "apscheduler-3.11.2.tar.gz", hash = "sha256:2a9966b052ec805f020c8c4c3ae6e6a06e24b1bf19f2e11d91d8cca0473eef41"}, -] - -[package.dependencies] -tzlocal = ">=3.0" - -[package.extras] -doc = ["packaging", "sphinx", "sphinx-rtd-theme (>=1.3.0)"] -etcd = ["etcd3", "protobuf (<=3.21.0)"] -gevent = ["gevent"] -mongodb = ["pymongo (>=3.0)"] -redis = ["redis (>=3.0)"] -rethinkdb = ["rethinkdb (>=2.4.0)"] -sqlalchemy = ["sqlalchemy (>=1.4)"] -test = ["APScheduler[etcd,mongodb,redis,rethinkdb,sqlalchemy,tornado,zookeeper]", "PySide6 ; platform_python_implementation == \"CPython\" and python_version < \"3.14\"", "anyio (>=4.5.2)", "gevent ; python_version < \"3.14\"", "pytest", "pytest-timeout", "pytz", "twisted ; python_version < \"3.14\""] -tornado = ["tornado (>=4.3)"] -twisted = ["twisted"] -zookeeper = ["kazoo"] - -[[package]] -name = "argcomplete" -version = "3.5.3" -description = "Bash tab completion for argparse" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "argcomplete-3.5.3-py3-none-any.whl", hash = "sha256:2ab2c4a215c59fd6caaff41a869480a23e8f6a5f910b266c1808037f4e375b61"}, - {file = "argcomplete-3.5.3.tar.gz", hash = "sha256:c12bf50eded8aebb298c7b7da7a5ff3ee24dffd9f5281867dfe1424b58c55392"}, -] - -[package.extras] -test = ["coverage", "mypy", "pexpect", "ruff", "wheel"] - -[[package]] -name = "asgiref" -version = "3.11.0" -description = "ASGI specs, helper code, and adapters" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "asgiref-3.11.0-py3-none-any.whl", hash = "sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d"}, - {file = "asgiref-3.11.0.tar.gz", hash = "sha256:13acff32519542a1736223fb79a715acdebe24286d98e8b164a73085f40da2c4"}, -] - -[package.extras] -tests = ["mypy (>=1.14.0)", "pytest", "pytest-asyncio"] - -[[package]] -name = "astroid" -version = "3.2.4" -description = "An abstract syntax tree for Python with inference support." -optional = false -python-versions = ">=3.8.0" -groups = ["dev"] -files = [ - {file = "astroid-3.2.4-py3-none-any.whl", hash = "sha256:413658a61eeca6202a59231abb473f932038fbcbf1666587f66d482083413a25"}, - {file = "astroid-3.2.4.tar.gz", hash = "sha256:0e14202810b30da1b735827f78f5157be2bbd4a7a59b7707ca0bfc2fb4c0063a"}, -] - -[[package]] -name = "async-timeout" -version = "5.0.1" -description = "Timeout context manager for asyncio programs" -optional = false -python-versions = ">=3.8" -groups = ["main"] -markers = "python_version == \"3.11\" and python_full_version < \"3.11.3\"" -files = [ - {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, - {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, -] - -[[package]] -name = "attrs" -version = "25.4.0" -description = "Classes Without Boilerplate" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"}, - {file = "attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11"}, -] - -[[package]] -name = "authlib" -version = "1.6.9" -description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "authlib-1.6.9-py2.py3-none-any.whl", hash = "sha256:f08b4c14e08f0861dc18a32357b33fbcfd2ea86cfe3fe149484b4d764c4a0ac3"}, - {file = "authlib-1.6.9.tar.gz", hash = "sha256:d8f2421e7e5980cc1ddb4e32d3f5fa659cfaf60d8eaf3281ebed192e4ab74f04"}, -] - -[package.dependencies] -cryptography = "*" - -[[package]] -name = "autopep8" -version = "2.3.2" -description = "A tool that automatically formats Python code to conform to the PEP 8 style guide" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "autopep8-2.3.2-py2.py3-none-any.whl", hash = "sha256:ce8ad498672c845a0c3de2629c15b635ec2b05ef8177a6e7c91c74f3e9b51128"}, - {file = "autopep8-2.3.2.tar.gz", hash = "sha256:89440a4f969197b69a995e4ce0661b031f455a9f776d2c5ba3dbd83466931758"}, -] - -[package.dependencies] -pycodestyle = ">=2.12.0" - -[[package]] -name = "awsipranges" -version = "0.3.3" -description = "Work with the AWS IP address ranges in native Python." -optional = false -python-versions = ">=3.7,<4.0" -groups = ["main"] -files = [ - {file = "awsipranges-0.3.3-py3-none-any.whl", hash = "sha256:f3d7a54aeaf7fe310beb5d377a4034a63a51b72677ae6af3e0967bc4de7eedaf"}, - {file = "awsipranges-0.3.3.tar.gz", hash = "sha256:4f0b3f22a9dc1163c85b513bed812b6c92bdacd674e6a7b68252a3c25b99e2c0"}, -] - -[[package]] -name = "azure-cli-core" -version = "2.83.0" -description = "Microsoft Azure Command-Line Tools Core Module" -optional = false -python-versions = ">=3.10.0" -groups = ["main"] -files = [ - {file = "azure_cli_core-2.83.0-py3-none-any.whl", hash = "sha256:3136f1434cb6fbd2f5b1d7f82b15cff3d4ba4a638808a86584376a829fd26b8a"}, - {file = "azure_cli_core-2.83.0.tar.gz", hash = "sha256:ac59ae4307a961891587d746984a3349b7afe9759ed8267e1cdd614aeeeabbf9"}, -] - -[package.dependencies] -argcomplete = ">=3.5.2,<3.6.0" -azure-cli-telemetry = "==1.1.0.*" -azure-core = ">=1.38.0,<1.39.0" -azure-mgmt-core = ">=1.2.0,<2" -cryptography = "*" -distro = {version = "*", markers = "sys_platform == \"linux\""} -humanfriendly = ">=10.0,<11.0" -jmespath = "*" -knack = ">=0.11.0,<0.12.0" -microsoft-security-utilities-secret-masker = ">=1.0.0b4,<1.1.0" -msal = [ - {version = "1.35.0b1", extras = ["broker"], markers = "sys_platform == \"win32\""}, - {version = "1.35.0b1", markers = "sys_platform != \"win32\""}, -] -msal-extensions = "1.2.0" -packaging = ">=20.9" -pkginfo = ">=1.5.0.1" -psutil = {version = ">=5.9", markers = "sys_platform != \"cygwin\""} -py-deviceid = "*" -PyJWT = ">=2.1.0" -pyopenssl = ">=17.1.0" -requests = {version = "*", extras = ["socks"]} - -[[package]] -name = "azure-cli-telemetry" -version = "1.1.0" -description = "Microsoft Azure CLI Telemetry Package" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "azure-cli-telemetry-1.1.0.tar.gz", hash = "sha256:d922379cda1b48952be75fb3bd2ac5e7ceecf569492a6088bab77894c624a278"}, - {file = "azure_cli_telemetry-1.1.0-py3-none-any.whl", hash = "sha256:2fc12608c0cf0ea6e69b392af9cab92f1249340b8caff7e9674cf91b3becb337"}, -] - -[package.dependencies] -applicationinsights = ">=0.11.1,<0.12" -portalocker = ">=1.6,<3" - -[[package]] -name = "azure-common" -version = "1.1.28" -description = "Microsoft Azure Client Library for Python (Common)" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "azure-common-1.1.28.zip", hash = "sha256:4ac0cd3214e36b6a1b6a442686722a5d8cc449603aa833f3f0f40bda836704a3"}, - {file = "azure_common-1.1.28-py2.py3-none-any.whl", hash = "sha256:5c12d3dcf4ec20599ca6b0d3e09e86e146353d443e7fcc050c9a19c1f9df20ad"}, -] - -[[package]] -name = "azure-core" -version = "1.38.1" -description = "Microsoft Azure Core Library for Python" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "azure_core-1.38.1-py3-none-any.whl", hash = "sha256:69f08ee3d55136071b7100de5b198994fc1c5f89d2b91f2f43156d20fcf200a4"}, - {file = "azure_core-1.38.1.tar.gz", hash = "sha256:9317db1d838e39877eb94a2240ce92fa607db68adf821817b723f0d679facbf6"}, -] - -[package.dependencies] -requests = ">=2.21.0" -typing-extensions = ">=4.6.0" - -[package.extras] -aio = ["aiohttp (>=3.0)"] -tracing = ["opentelemetry-api (>=1.26,<2.0)"] - -[[package]] -name = "azure-identity" -version = "1.21.0" -description = "Microsoft Azure Identity Library for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "azure_identity-1.21.0-py3-none-any.whl", hash = "sha256:258ea6325537352440f71b35c3dffe9d240eae4a5126c1b7ce5efd5766bd9fd9"}, - {file = "azure_identity-1.21.0.tar.gz", hash = "sha256:ea22ce6e6b0f429bc1b8d9212d5b9f9877bd4c82f1724bfa910760612c07a9a6"}, -] - -[package.dependencies] -azure-core = ">=1.31.0" -cryptography = ">=2.5" -msal = ">=1.30.0" -msal-extensions = ">=1.2.0" -typing-extensions = ">=4.0.0" - -[[package]] -name = "azure-keyvault-certificates" -version = "4.10.0" -description = "Microsoft Corporation Key Vault Certificates Client Library for Python" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "azure_keyvault_certificates-4.10.0-py3-none-any.whl", hash = "sha256:fa76cbc329274cb5f4ab61b0ed7d209d44377df4b4d6be2fd01e741c2fbb83a9"}, - {file = "azure_keyvault_certificates-4.10.0.tar.gz", hash = "sha256:004ff47a73152f9f40f678e5a07719b753a3ca86f0460bfeaaf6a23304872e05"}, -] - -[package.dependencies] -azure-core = ">=1.31.0" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[[package]] -name = "azure-keyvault-keys" -version = "4.10.0" -description = "Microsoft Azure Key Vault Keys Client Library for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "azure_keyvault_keys-4.10.0-py3-none-any.whl", hash = "sha256:210227e0061f641a79755f0e0bcbcf27bbfb4df630a933c43a99a29962283d0d"}, - {file = "azure_keyvault_keys-4.10.0.tar.gz", hash = "sha256:511206ae90aec1726a4d6ff5a92d754bd0c0f1e8751891368d30fb70b62955f1"}, -] - -[package.dependencies] -azure-core = ">=1.31.0" -cryptography = ">=2.1.4" -isodate = ">=0.6.1" -typing-extensions = ">=4.0.1" - -[[package]] -name = "azure-keyvault-secrets" -version = "4.10.0" -description = "Microsoft Corporation Key Vault Secrets Client Library for Python" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "azure_keyvault_secrets-4.10.0-py3-none-any.whl", hash = "sha256:9dbde256077a4ee1a847646671580692e3f9bea36bcfc189c3cf2b9a94eb38b9"}, - {file = "azure_keyvault_secrets-4.10.0.tar.gz", hash = "sha256:666fa42892f9cee749563e551a90f060435ab878977c95265173a8246d546a36"}, -] - -[package.dependencies] -azure-core = ">=1.31.0" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[[package]] -name = "azure-mgmt-apimanagement" -version = "5.0.0" -description = "Microsoft Azure API Management Client Library for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "azure_mgmt_apimanagement-5.0.0-py3-none-any.whl", hash = "sha256:b88c42a392333b60722fb86f15d092dfc19a8d67510dccd15c217381dff4e6ec"}, - {file = "azure_mgmt_apimanagement-5.0.0.tar.gz", hash = "sha256:0ab7fe17e70fe3154cd840ff47d19d7a4610217003eaa7c21acf3511a6e57999"}, -] - -[package.dependencies] -azure-common = ">=1.1" -azure-mgmt-core = ">=1.3.2" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[[package]] -name = "azure-mgmt-applicationinsights" -version = "4.1.0" -description = "Microsoft Azure Application Insights Management Client Library for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "azure_mgmt_applicationinsights-4.1.0-py3-none-any.whl", hash = "sha256:9e71f29b01e505a773501451d12fd6a10482cf4b13e9ac2bff72f5380496d979"}, - {file = "azure_mgmt_applicationinsights-4.1.0.tar.gz", hash = "sha256:15531390f12ce3d767cd3f1949af36aa39077c145c952fec4d80303c86ec7b6c"}, -] - -[package.dependencies] -azure-common = ">=1.1" -azure-mgmt-core = ">=1.3.2" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[[package]] -name = "azure-mgmt-authorization" -version = "4.0.0" -description = "Microsoft Azure Authorization Management Client Library for Python" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "azure-mgmt-authorization-4.0.0.zip", hash = "sha256:69b85abc09ae64fc72975bd43431170d8c7eb5d166754b98aac5f3845de57dc4"}, - {file = "azure_mgmt_authorization-4.0.0-py3-none-any.whl", hash = "sha256:d8feeb3842e6ddf1a370963ca4f61fb6edc124e8997b807dd025bc9b2379cd1a"}, -] - -[package.dependencies] -azure-common = ">=1.1,<2.0" -azure-mgmt-core = ">=1.3.2,<2.0.0" -isodate = ">=0.6.1,<1.0.0" - -[[package]] -name = "azure-mgmt-compute" -version = "34.0.0" -description = "Microsoft Azure Compute Management Client Library for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "azure_mgmt_compute-34.0.0-py3-none-any.whl", hash = "sha256:f8f7b1c5c187a26fae4d1f099adf93561244242f28899484d9a42747bf0d5af4"}, - {file = "azure_mgmt_compute-34.0.0.tar.gz", hash = "sha256:58cd01d025efa02870b84dbfb69834a3b23501a135658c03854d2434e8dfee1e"}, -] - -[package.dependencies] -azure-common = ">=1.1" -azure-mgmt-core = ">=1.3.2" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[[package]] -name = "azure-mgmt-containerinstance" -version = "10.1.0" -description = "Microsoft Azure Container Instance Client Library for Python" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "azure-mgmt-containerinstance-10.1.0.zip", hash = "sha256:78d437adb28574f448c838ed5f01f9ced378196098061deb59d9f7031704c17e"}, - {file = "azure_mgmt_containerinstance-10.1.0-py3-none-any.whl", hash = "sha256:ee7977b7b70f2233e44ec6ce8c99027f3f7892bb3452b4bad46df340d9f98959"}, -] - -[package.dependencies] -azure-common = ">=1.1,<2.0" -azure-mgmt-core = ">=1.3.2,<2.0.0" -isodate = ">=0.6.1,<1.0.0" - -[[package]] -name = "azure-mgmt-containerregistry" -version = "12.0.0" -description = "Microsoft Azure Container Registry Client Library for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "azure_mgmt_containerregistry-12.0.0-py3-none-any.whl", hash = "sha256:464abd4d3d9ecc0456ed8f63a6b9b93afc2e3e194f2d34f26a758afb67ad3b5c"}, - {file = "azure_mgmt_containerregistry-12.0.0.tar.gz", hash = "sha256:f19f8faa7881deaf2b5015c0eb050a92e2380cd9d18dee33cdb5f27d44a06c03"}, -] - -[package.dependencies] -azure-common = ">=1.1" -azure-mgmt-core = ">=1.3.2" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[[package]] -name = "azure-mgmt-containerservice" -version = "34.1.0" -description = "Microsoft Azure Container Service Management Client Library for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "azure_mgmt_containerservice-34.1.0-py3-none-any.whl", hash = "sha256:1faa1714e0100c6ee4cfb8d2eadb1c270b548a84b0070c74e9fe646056a5cb12"}, - {file = "azure_mgmt_containerservice-34.1.0.tar.gz", hash = "sha256:637a6cf8f06636c016ad151d76f9c7ba75bd05d4334b3dd7837eb8b517f30dbe"}, -] - -[package.dependencies] -azure-common = ">=1.1" -azure-mgmt-core = ">=1.3.2" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[[package]] -name = "azure-mgmt-core" -version = "1.6.0" -description = "Microsoft Azure Management Core Library for Python" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "azure_mgmt_core-1.6.0-py3-none-any.whl", hash = "sha256:0460d11e85c408b71c727ee1981f74432bc641bb25dfcf1bb4e90a49e776dbc4"}, - {file = "azure_mgmt_core-1.6.0.tar.gz", hash = "sha256:b26232af857b021e61d813d9f4ae530465255cb10b3dde945ad3743f7a58e79c"}, -] - -[package.dependencies] -azure-core = ">=1.32.0" - -[[package]] -name = "azure-mgmt-cosmosdb" -version = "9.7.0" -description = "Microsoft Azure Cosmos DB Management Client Library for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "azure_mgmt_cosmosdb-9.7.0-py3-none-any.whl", hash = "sha256:be735a554d16995c8cefe413e62119985f8fabae1cb45a6f6ad2c3958bed14da"}, - {file = "azure_mgmt_cosmosdb-9.7.0.tar.gz", hash = "sha256:b5072d319f11953d8f12e22459aded1912d5f27e442e1d8b49596a85005410a1"}, -] - -[package.dependencies] -azure-common = ">=1.1" -azure-mgmt-core = ">=1.3.2" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[[package]] -name = "azure-mgmt-databricks" -version = "2.0.0" -description = "Microsoft Azure Data Bricks Management Client Library for Python" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "azure-mgmt-databricks-2.0.0.zip", hash = "sha256:70d11362dc2d17f5fb1db0cfe65c1af55b8f136f1a0db9a5b51e7acf760cf5b9"}, - {file = "azure_mgmt_databricks-2.0.0-py3-none-any.whl", hash = "sha256:0c29434a7339e74231bd171a6c08dcdf8153abaebd332658d7f66b8ea143fa17"}, -] - -[package.dependencies] -azure-common = ">=1.1,<2.0" -azure-mgmt-core = ">=1.3.2,<2.0.0" -isodate = ">=0.6.1,<1.0.0" - -[[package]] -name = "azure-mgmt-datafactory" -version = "9.2.0" -description = "Microsoft Azure Data Factory Management Client Library for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "azure_mgmt_datafactory-9.2.0-py3-none-any.whl", hash = "sha256:d870a7a6099227e91d1c258a956c2aa32c2ea4c0a4409913d8f215887349f128"}, - {file = "azure_mgmt_datafactory-9.2.0.tar.gz", hash = "sha256:5132e9c24c441ac225f2a60225924baa55079ca81eff7db99a70d661d64bb0d7"}, -] - -[package.dependencies] -azure-common = ">=1.1" -azure-mgmt-core = ">=1.3.2" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[[package]] -name = "azure-mgmt-eventgrid" -version = "10.4.0" -description = "Microsoft Azure Event Grid Management Client Library for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "azure_mgmt_eventgrid-10.4.0-py3-none-any.whl", hash = "sha256:5e4637245bbff33298d5f427971b870dbb03d873a3ef68f328190a7b7a38c56f"}, - {file = "azure_mgmt_eventgrid-10.4.0.tar.gz", hash = "sha256:303e5e27cf4bb5ec833ba4e5a9ef70b5bc410e190412ec47cde59d82e413fb7e"}, -] - -[package.dependencies] -azure-common = ">=1.1" -azure-mgmt-core = ">=1.3.2" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[[package]] -name = "azure-mgmt-eventhub" -version = "11.2.0" -description = "Microsoft Azure Event Hub Management Client Library for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "azure_mgmt_eventhub-11.2.0-py3-none-any.whl", hash = "sha256:a7e2618eca58d8e52c7ff7d4a04a4fae12685351746e6d01b933b43e7ea3b906"}, - {file = "azure_mgmt_eventhub-11.2.0.tar.gz", hash = "sha256:31c47f18f73d2d83345cde5909568e28858c2548a35b10e23194b4767a9ce7e3"}, -] - -[package.dependencies] -azure-common = ">=1.1" -azure-mgmt-core = ">=1.3.2" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[[package]] -name = "azure-mgmt-keyvault" -version = "10.3.1" -description = "Microsoft Azure Key Vault Management Client Library for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "azure-mgmt-keyvault-10.3.1.tar.gz", hash = "sha256:34b92956aefbdd571cae5a03f7078e037d8087b2c00cfa6748835dc73abb5a30"}, - {file = "azure_mgmt_keyvault-10.3.1-py3-none-any.whl", hash = "sha256:a18a27a06551482d31f92bc43ac8b0846af02cd69511f80090865b4c5caa3c21"}, -] - -[package.dependencies] -azure-common = ">=1.1" -azure-mgmt-core = ">=1.3.2" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[[package]] -name = "azure-mgmt-loganalytics" -version = "12.0.0" -description = "Microsoft Azure Log Analytics Management Client Library for Python" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "azure-mgmt-loganalytics-12.0.0.zip", hash = "sha256:da128a7e0291be7fa2063848df92a9180cf5c16d42adc09d2bc2efd711536bfb"}, - {file = "azure_mgmt_loganalytics-12.0.0-py2.py3-none-any.whl", hash = "sha256:75ac1d47dd81179905c40765be8834643d8994acff31056ddc1863017f3faa02"}, -] - -[package.dependencies] -azure-common = ">=1.1,<2.0" -azure-mgmt-core = ">=1.2.0,<2.0.0" -msrest = ">=0.6.21" - -[[package]] -name = "azure-mgmt-logic" -version = "10.0.0" -description = "Microsoft Azure Logic Apps Management Client Library for Python" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "azure-mgmt-logic-10.0.0.zip", hash = "sha256:b3fa4864f14aaa7af41d778d925f051ed29b6016f46344765ecd0f49d0f04dd6"}, - {file = "azure_mgmt_logic-10.0.0-py3-none-any.whl", hash = "sha256:525c78afedf3edb35eb0a16152c8beba89769ee1bc6af01bcdc42842a551e443"}, -] - -[package.dependencies] -azure-common = ">=1.1,<2.0" -azure-mgmt-core = ">=1.3.0,<2.0.0" -msrest = ">=0.6.21" - -[[package]] -name = "azure-mgmt-monitor" -version = "6.0.2" -description = "Microsoft Azure Monitor Client Library for Python" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "azure-mgmt-monitor-6.0.2.tar.gz", hash = "sha256:5ffbf500e499ab7912b1ba6d26cef26480d9ae411532019bb78d72562196e07b"}, - {file = "azure_mgmt_monitor-6.0.2-py3-none-any.whl", hash = "sha256:fe4cf41e6680b74a228f81451dc5522656d599c6f343ecf702fc790fda9a357b"}, -] - -[package.dependencies] -azure-common = ">=1.1,<2.0" -azure-mgmt-core = ">=1.3.2,<2.0.0" -isodate = ">=0.6.1,<1.0.0" - -[[package]] -name = "azure-mgmt-network" -version = "28.1.0" -description = "Microsoft Azure Network Management Client Library for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "azure_mgmt_network-28.1.0-py3-none-any.whl", hash = "sha256:8ddb0e9ec8f10c9c152d60fc945908d113e4591f397ea3e40b92290ec2b01658"}, - {file = "azure_mgmt_network-28.1.0.tar.gz", hash = "sha256:8c84bffb5ec75c6e0244e58ecf07c00d5fc421d616b0cb369c6fe585af33cf87"}, -] - -[package.dependencies] -azure-common = ">=1.1" -azure-mgmt-core = ">=1.3.2" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[[package]] -name = "azure-mgmt-postgresqlflexibleservers" -version = "1.1.0" -description = "Microsoft Azure Postgresqlflexibleservers Management Client Library for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "azure_mgmt_postgresqlflexibleservers-1.1.0-py3-none-any.whl", hash = "sha256:87ddb5a5e6d12c45769485d234cfe0322140e3a0a7636d0e61fb00ac544b5d20"}, - {file = "azure_mgmt_postgresqlflexibleservers-1.1.0.tar.gz", hash = "sha256:9ede9d8ba63e9d2879cb74adc903c649af3bc5460a02787287b0cd18d754af14"}, -] - -[package.dependencies] -azure-common = ">=1.1" -azure-mgmt-core = ">=1.3.2" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[[package]] -name = "azure-mgmt-rdbms" -version = "10.1.0" -description = "Microsoft Azure RDBMS Management Client Library for Python" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "azure-mgmt-rdbms-10.1.0.zip", hash = "sha256:a87d401c876c84734cdd4888af551e4a1461b4b328d9816af60cb8ac5979f035"}, - {file = "azure_mgmt_rdbms-10.1.0-py3-none-any.whl", hash = "sha256:8eac17d1341a91d7ed914435941ba917b5ef1568acabc3e65653603966a7cc88"}, -] - -[package.dependencies] -azure-common = ">=1.1,<2.0" -azure-mgmt-core = ">=1.3.0,<2.0.0" -msrest = ">=0.6.21" - -[[package]] -name = "azure-mgmt-recoveryservices" -version = "3.1.0" -description = "Microsoft Azure Recovery Services Client Library for Python" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "azure_mgmt_recoveryservices-3.1.0-py3-none-any.whl", hash = "sha256:21c58afdf4ae66806783e95f8cd17e3bec31be7178c48784db21f0b05de7fa66"}, - {file = "azure_mgmt_recoveryservices-3.1.0.tar.gz", hash = "sha256:7f2db98401708cf145322f50bc491caf7967bec4af3bf7b0984b9f07d3092687"}, -] - -[package.dependencies] -azure-common = ">=1.1" -azure-mgmt-core = ">=1.5.0" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[[package]] -name = "azure-mgmt-recoveryservicesbackup" -version = "9.2.0" -description = "Microsoft Azure Recovery Services Backup Management Client Library for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "azure_mgmt_recoveryservicesbackup-9.2.0-py3-none-any.whl", hash = "sha256:c0002858d0166b6a10189a1fd580a49c83dc31b111e98010a5b2ea0f767dfff1"}, - {file = "azure_mgmt_recoveryservicesbackup-9.2.0.tar.gz", hash = "sha256:c402b3e22a6c3879df56bc37e0063142c3352c5102599ff102d19824f1b32b29"}, -] - -[package.dependencies] -azure-common = ">=1.1" -azure-mgmt-core = ">=1.3.2" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[[package]] -name = "azure-mgmt-resource" -version = "24.0.0" -description = "Microsoft Azure Resource Management Client Library for Python" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "azure_mgmt_resource-24.0.0-py3-none-any.whl", hash = "sha256:27b32cd223e2784269f5a0db3c282042886ee4072d79cedc638438ece7cd0df4"}, - {file = "azure_mgmt_resource-24.0.0.tar.gz", hash = "sha256:cf6b8995fcdd407ac9ff1dd474087129429a1d90dbb1ac77f97c19b96237b265"}, -] - -[package.dependencies] -azure-common = ">=1.1" -azure-mgmt-core = ">=1.5.0" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[[package]] -name = "azure-mgmt-search" -version = "9.1.0" -description = "Microsoft Azure Search Management Client Library for Python" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "azure-mgmt-search-9.1.0.tar.gz", hash = "sha256:53bc6eeadb0974d21f120bb21bb5e6827df6d650e17347460fd83e2d68883599"}, - {file = "azure_mgmt_search-9.1.0-py3-none-any.whl", hash = "sha256:488ff81477e980e2b7abf0b857387c74ebbad419e6f6126044e3e6fad2da72b6"}, -] - -[package.dependencies] -azure-common = ">=1.1,<2.0" -azure-mgmt-core = ">=1.3.2,<2.0.0" -isodate = ">=0.6.1,<1.0.0" - -[[package]] -name = "azure-mgmt-security" -version = "7.0.0" -description = "Microsoft Azure Security Center Management Client Library for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "azure-mgmt-security-7.0.0.tar.gz", hash = "sha256:5912eed7e9d3758fdca8d26e1dc26b41943dc4703208a1184266e2c252e1ad66"}, - {file = "azure_mgmt_security-7.0.0-py3-none-any.whl", hash = "sha256:85a6d8b7a5cd74884a548ed53fed034449f54a9989edd64e9020c5837db96933"}, -] - -[package.dependencies] -azure-common = ">=1.1" -azure-mgmt-core = ">=1.3.2" -isodate = ">=0.6.1" - -[[package]] -name = "azure-mgmt-sql" -version = "3.0.1" -description = "Microsoft Azure SQL Management Client Library for Python" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "azure-mgmt-sql-3.0.1.zip", hash = "sha256:129042cc011225e27aee6ef2697d585fa5722e5d1aeb0038af6ad2451a285457"}, - {file = "azure_mgmt_sql-3.0.1-py2.py3-none-any.whl", hash = "sha256:1d1dd940d4d41be4ee319aad626341251572a5bf4a2addec71779432d9a1381f"}, -] - -[package.dependencies] -azure-common = ">=1.1,<2.0" -azure-mgmt-core = ">=1.2.0,<2.0.0" -msrest = ">=0.6.21" - -[[package]] -name = "azure-mgmt-storage" -version = "22.1.1" -description = "Microsoft Azure Storage Management Client Library for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "azure_mgmt_storage-22.1.1-py3-none-any.whl", hash = "sha256:a4a4064918dcfa4f1cbebada5bf064935d66f2a3647a2f46a1f1c9348736f5d9"}, - {file = "azure_mgmt_storage-22.1.1.tar.gz", hash = "sha256:25aaa5ae8c40c30e2f91f8aae6f52906b0557e947d5c1b9817d4ff9decc11340"}, -] - -[package.dependencies] -azure-common = ">=1.1" -azure-mgmt-core = ">=1.3.2" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[[package]] -name = "azure-mgmt-subscription" -version = "3.1.1" -description = "Microsoft Azure Subscription Management Client Library for Python" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "azure-mgmt-subscription-3.1.1.zip", hash = "sha256:4e255b4ce9b924357bb8c5009b3c88a2014d3203b2495e2256fa027bf84e800e"}, - {file = "azure_mgmt_subscription-3.1.1-py3-none-any.whl", hash = "sha256:38d4574a8d47fa17e3587d756e296cb63b82ad8fb21cd8543bcee443a502bf48"}, -] - -[package.dependencies] -azure-common = ">=1.1,<2.0" -azure-mgmt-core = ">=1.3.2,<2.0.0" -msrest = ">=0.7.1" - -[[package]] -name = "azure-mgmt-synapse" -version = "2.0.0" -description = "Microsoft Azure Synapse Management Client Library for Python" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "azure-mgmt-synapse-2.0.0.zip", hash = "sha256:bec6bdfaeb55b4fdd159f2055e8875bf50a720bb0fce80a816e92a2359b898c8"}, - {file = "azure_mgmt_synapse-2.0.0-py2.py3-none-any.whl", hash = "sha256:e901274009be843a7bf2eedeab32c0941fabb2addea9a1ad1560395073965f0f"}, -] - -[package.dependencies] -azure-common = ">=1.1,<2.0" -azure-mgmt-core = ">=1.2.0,<2.0.0" -msrest = ">=0.6.21" - -[[package]] -name = "azure-mgmt-web" -version = "8.0.0" -description = "Microsoft Azure Web Apps Management Client Library for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "azure_mgmt_web-8.0.0-py3-none-any.whl", hash = "sha256:0536aac05bfc673b56ed930f2966b77856e84df675d376e782a7af6bb92449af"}, - {file = "azure_mgmt_web-8.0.0.tar.gz", hash = "sha256:c8d9c042c09db7aacb20270a9effed4d4e651e365af32d80897b84dc7bf35098"}, -] - -[package.dependencies] -azure-common = ">=1.1" -azure-mgmt-core = ">=1.3.2" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[[package]] -name = "azure-monitor-query" -version = "2.0.0" -description = "Microsoft Corporation Azure Monitor Query Client Library for Python" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "azure_monitor_query-2.0.0-py3-none-any.whl", hash = "sha256:8f52d581271d785e12f49cd5aaa144b8910fb843db2373855a7ef94c7fc462ea"}, - {file = "azure_monitor_query-2.0.0.tar.gz", hash = "sha256:7b05f2fcac4fb67fc9f77a7d4c5d98a0f3099fb73b57c69ec1b080773994671b"}, -] - -[package.dependencies] -azure-core = ">=1.30.0" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[[package]] -name = "azure-storage-blob" -version = "12.24.1" -description = "Microsoft Azure Blob Storage Client Library for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "azure_storage_blob-12.24.1-py3-none-any.whl", hash = "sha256:77fb823fdbac7f3c11f7d86a5892e2f85e161e8440a7489babe2195bf248f09e"}, - {file = "azure_storage_blob-12.24.1.tar.gz", hash = "sha256:052b2a1ea41725ba12e2f4f17be85a54df1129e13ea0321f5a2fcc851cbf47d4"}, -] - -[package.dependencies] -azure-core = ">=1.30.0" -cryptography = ">=2.1.4" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[package.extras] -aio = ["azure-core[aio] (>=1.30.0)"] - -[[package]] -name = "azure-synapse-artifacts" -version = "0.21.0" -description = "Microsoft Azure Synapse Artifacts Client Library for Python" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "azure_synapse_artifacts-0.21.0-py3-none-any.whl", hash = "sha256:3311919df13a2b42f1fb9debf5d512080c35d64d02b9f84ff944848835289a8d"}, - {file = "azure_synapse_artifacts-0.21.0.tar.gz", hash = "sha256:d7e37516cf8569e03c604d921e3407d7140cf7523b67b67f757caf999e3c8ee7"}, -] - -[package.dependencies] -azure-common = ">=1.1" -azure-mgmt-core = ">=1.6.0" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[[package]] -name = "backoff" -version = "2.2.1" -description = "Function decoration for backoff and retry" -optional = false -python-versions = ">=3.7,<4.0" -groups = ["main"] -files = [ - {file = "backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8"}, - {file = "backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba"}, -] - -[[package]] -name = "bandit" -version = "1.7.9" -description = "Security oriented static analyser for python code." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "bandit-1.7.9-py3-none-any.whl", hash = "sha256:52077cb339000f337fb25f7e045995c4ad01511e716e5daac37014b9752de8ec"}, - {file = "bandit-1.7.9.tar.gz", hash = "sha256:7c395a436743018f7be0a4cbb0a4ea9b902b6d87264ddecf8cfdc73b4f78ff61"}, -] - -[package.dependencies] -colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} -PyYAML = ">=5.3.1" -rich = "*" -stevedore = ">=1.20.0" - -[package.extras] -baseline = ["GitPython (>=3.1.30)"] -sarif = ["jschema-to-python (>=1.2.3)", "sarif-om (>=1.0.4)"] -test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "pylint (==1.9.4)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)"] -toml = ["tomli (>=1.1.0) ; python_version < \"3.11\""] -yaml = ["PyYAML"] - -[[package]] -name = "billiard" -version = "4.2.4" -description = "Python multiprocessing fork with improvements and bugfixes" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "billiard-4.2.4-py3-none-any.whl", hash = "sha256:525b42bdec68d2b983347ac312f892db930858495db601b5836ac24e6477cde5"}, - {file = "billiard-4.2.4.tar.gz", hash = "sha256:55f542c371209e03cd5862299b74e52e4fbcba8250ba611ad94276b369b6a85f"}, -] - -[[package]] -name = "blinker" -version = "1.9.0" -description = "Fast, simple object-to-object and broadcast signaling" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"}, - {file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"}, -] - -[[package]] -name = "boto3" -version = "1.40.61" -description = "The AWS SDK for Python" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "boto3-1.40.61-py3-none-any.whl", hash = "sha256:6b9c57b2a922b5d8c17766e29ed792586a818098efe84def27c8f582b33f898c"}, - {file = "boto3-1.40.61.tar.gz", hash = "sha256:d6c56277251adf6c2bdd25249feae625abe4966831676689ff23b4694dea5b12"}, -] - -[package.dependencies] -botocore = ">=1.40.61,<1.41.0" -jmespath = ">=0.7.1,<2.0.0" -s3transfer = ">=0.14.0,<0.15.0" - -[package.extras] -crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] - -[[package]] -name = "botocore" -version = "1.40.61" -description = "Low-level, data-driven core of boto 3." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "botocore-1.40.61-py3-none-any.whl", hash = "sha256:17ebae412692fd4824f99cde0f08d50126dc97954008e5ba2b522eb049238aa7"}, - {file = "botocore-1.40.61.tar.gz", hash = "sha256:a2487ad69b090f9cccd64cf07c7021cd80ee9c0655ad974f87045b02f3ef52cd"}, -] - -[package.dependencies] -jmespath = ">=0.7.1,<2.0.0" -python-dateutil = ">=2.1,<3.0.0" -urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""} - -[package.extras] -crt = ["awscrt (==0.27.6)"] - -[[package]] -name = "cartography" -version = "0.135.0" -description = "Explore assets and their relationships across your technical infrastructure." -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "cartography-0.135.0-py3-none-any.whl", hash = "sha256:c62c32a6917b8f23a8b98fe2b6c7c4a918b50f55918482966c4dae1cf5f538e1"}, - {file = "cartography-0.135.0.tar.gz", hash = "sha256:3f500cd22c3b392d00e8b49f62acc95fd4dcd559ce514aafe2eb8101133c7a49"}, -] - -[package.dependencies] -adal = ">=1.2.4" -aioboto3 = ">=15.0.0" -azure-cli-core = ">=2.26.0" -azure-identity = ">=1.5.0" -azure-keyvault-certificates = ">=4.0.0" -azure-keyvault-keys = ">=4.0.0" -azure-keyvault-secrets = ">=4.0.0" -azure-mgmt-authorization = ">=0.60.0" -azure-mgmt-compute = ">=5.0.0" -azure-mgmt-containerinstance = ">=10.0.0" -azure-mgmt-containerservice = ">=30.0.0" -azure-mgmt-cosmosdb = ">=6.0.0" -azure-mgmt-datafactory = ">=8.0.0" -azure-mgmt-eventgrid = ">=10.0.0" -azure-mgmt-eventhub = ">=10.1.0" -azure-mgmt-keyvault = ">=10.0.0" -azure-mgmt-logic = ">=10.0.0" -azure-mgmt-monitor = ">=3.0.0" -azure-mgmt-network = ">=25.0.0" -azure-mgmt-resource = ">=24.0.0,<25" -azure-mgmt-security = ">=5.0.0" -azure-mgmt-sql = ">=3.0.1" -azure-mgmt-storage = ">=16.0.0" -azure-mgmt-synapse = ">=2.0.0" -azure-mgmt-web = ">=7.0.0" -azure-synapse-artifacts = ">=0.17.0" -backoff = ">=2.1.2" -boto3 = ">=1.15.1" -botocore = ">=1.18.1" -cloudflare = ">=4.1.0" -crowdstrike-falconpy = ">=0.5.1" -cryptography = ">=45.0.0" -dnspython = ">=2.0.0" -duo-client = ">=5.5.0" -google-api-python-client = ">=2.0.0" -google-auth = ">=2.37.0" -google-cloud-asset = ">=1.0.0" -google-cloud-resource-manager = ">=1.14.2" -httpx = ">=0.24.0" -kubernetes = ">=22.6.0" -marshmallow = ">=4.0.0" -msgraph-sdk = ">=1.53.0" -msrestazure = ">=0.6.4" -neo4j = ">=6.0.0" -oci = ">=2.71.0" -okta = "<1.0.0" -packageurl-python = ">=0.17.0" -packaging = ">=26.0.0" -pagerduty = ">=4.0.1" -policyuniverse = ">=1.1.0.0" -PyJWT = {version = ">=2.0.0", extras = ["crypto"]} -python-dateutil = ">=2.9.0" -python-digitalocean = ">=1.16.0" -pyyaml = ">=5.3.1" -requests = ">=2.22.0" -scaleway = ">=2.10.0" -slack-sdk = ">=3.37.0" -statsd = ">=4.0.0" -typer = ">=0.9.0" -types-aiobotocore-ecr = ">=3.1.0" -workos = ">=5.44.0" -xmltodict = ">=1.0.0" - -[[package]] -name = "celery" -version = "5.6.2" -description = "Distributed Task Queue." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "celery-5.6.2-py3-none-any.whl", hash = "sha256:3ffafacbe056951b629c7abcf9064c4a2366de0bdfc9fdba421b97ebb68619a5"}, - {file = "celery-5.6.2.tar.gz", hash = "sha256:4a8921c3fcf2ad76317d3b29020772103581ed2454c4c042cc55dcc43585009b"}, -] - -[package.dependencies] -billiard = ">=4.2.1,<5.0" -click = ">=8.1.2,<9.0" -click-didyoumean = ">=0.3.0" -click-plugins = ">=1.1.1" -click-repl = ">=0.2.0" -kombu = ">=5.6.0" -python-dateutil = ">=2.8.2" -tzlocal = "*" -vine = ">=5.1.0,<6.0" - -[package.extras] -arangodb = ["pyArango (>=2.0.2)"] -auth = ["cryptography (==46.0.3)"] -azureblockblob = ["azure-identity (>=1.19.0)", "azure-storage-blob (>=12.15.0)"] -brotli = ["brotli (>=1.0.0) ; platform_python_implementation == \"CPython\"", "brotlipy (>=0.7.0) ; platform_python_implementation == \"PyPy\""] -cassandra = ["cassandra-driver (>=3.25.0,<4)"] -consul = ["python-consul2 (==0.1.5)"] -cosmosdbsql = ["pydocumentdb (==2.3.5)"] -couchbase = ["couchbase (>=3.0.0) ; platform_python_implementation != \"PyPy\" and (platform_system != \"Windows\" or python_version < \"3.10\")"] -couchdb = ["pycouchdb (==1.16.0)"] -django = ["Django (>=2.2.28)"] -dynamodb = ["boto3 (>=1.26.143)"] -elasticsearch = ["elastic-transport (<=9.1.0)", "elasticsearch (<=9.1.2)"] -eventlet = ["eventlet (>=0.32.0) ; python_version < \"3.10\""] -gcs = ["google-cloud-firestore (==2.22.0)", "google-cloud-storage (>=2.10.0)", "grpcio (==1.75.1)"] -gevent = ["gevent (>=1.5.0)"] -librabbitmq = ["librabbitmq (>=2.0.0) ; python_version < \"3.11\""] -memcache = ["pylibmc (==1.6.3) ; platform_system != \"Windows\""] -mongodb = ["kombu[mongodb]"] -msgpack = ["kombu[msgpack]"] -pydantic = ["pydantic (>=2.12.0a1) ; python_version >= \"3.14\"", "pydantic (>=2.4) ; python_version < \"3.14\""] -pymemcache = ["python-memcached (>=1.61)"] -pyro = ["pyro4 (==4.82) ; python_version < \"3.11\""] -pytest = ["pytest-celery[all] (>=1.2.0,<1.3.0)"] -redis = ["kombu[redis]"] -s3 = ["boto3 (>=1.26.143)"] -slmq = ["softlayer_messaging (>=1.0.3)"] -solar = ["ephem (==4.2) ; platform_python_implementation != \"PyPy\""] -sqlalchemy = ["kombu[sqlalchemy]"] -sqs = ["boto3 (>=1.26.143)", "kombu[sqs] (>=5.5.0)", "pycurl (>=7.43.0.5,<7.45.4) ; sys_platform != \"win32\" and platform_python_implementation == \"CPython\" and python_version < \"3.9\"", "pycurl (>=7.45.4) ; sys_platform != \"win32\" and platform_python_implementation == \"CPython\" and python_version >= \"3.9\"", "urllib3 (>=1.26.16)"] -tblib = ["tblib (==3.2.2)"] -yaml = ["kombu[yaml]"] -zookeeper = ["kazoo (>=1.3.1)"] -zstd = ["zstandard (==0.23.0)"] - -[[package]] -name = "certifi" -version = "2026.1.4" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -files = [ - {file = "certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c"}, - {file = "certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"}, -] - -[[package]] -name = "cffi" -version = "2.0.0" -description = "Foreign Function Interface for Python calling C code." -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -markers = "platform_python_implementation != \"PyPy\"" -files = [ - {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, - {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, - {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, - {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, - {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, - {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, - {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, - {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, - {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, - {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, - {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, - {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, - {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, - {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, - {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, - {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, - {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, - {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, - {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, - {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, - {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, - {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, - {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, - {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, - {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, - {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, - {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, - {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, - {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, - {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, - {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, - {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, - {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, - {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, - {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, - {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, - {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, - {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, - {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, - {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, - {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, - {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, - {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, - {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, - {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, - {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, - {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, - {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, - {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, - {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, - {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, -] - -[package.dependencies] -pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} - -[[package]] -name = "charset-normalizer" -version = "3.4.4" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -files = [ - {file = "charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50"}, - {file = "charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f"}, - {file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}, -] - -[[package]] -name = "circuitbreaker" -version = "2.1.3" -description = "Python Circuit Breaker pattern implementation" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "circuitbreaker-2.1.3-py3-none-any.whl", hash = "sha256:87ba6a3ed03fdc7032bc175561c2b04d52ade9d5faf94ca2b035fbdc5e6b1dd1"}, - {file = "circuitbreaker-2.1.3.tar.gz", hash = "sha256:1a4baee510f7bea3c91b194dcce7c07805fe96c4423ed5594b75af438531d084"}, -] - -[[package]] -name = "click" -version = "8.3.1" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.10" -groups = ["main", "dev"] -files = [ - {file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"}, - {file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "click-didyoumean" -version = "0.3.1" -description = "Enables git-like *did-you-mean* feature in click" -optional = false -python-versions = ">=3.6.2" -groups = ["main"] -files = [ - {file = "click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c"}, - {file = "click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463"}, -] - -[package.dependencies] -click = ">=7" - -[[package]] -name = "click-plugins" -version = "1.1.1.2" -description = "An extension module for click to enable registering CLI commands via setuptools entry-points." -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "click_plugins-1.1.1.2-py2.py3-none-any.whl", hash = "sha256:008d65743833ffc1f5417bf0e78e8d2c23aab04d9745ba817bd3e71b0feb6aa6"}, - {file = "click_plugins-1.1.1.2.tar.gz", hash = "sha256:d7af3984a99d243c131aa1a828331e7630f4a88a9741fd05c927b204bcf92261"}, -] - -[package.dependencies] -click = ">=4.0" - -[package.extras] -dev = ["coveralls", "pytest (>=3.6)", "pytest-cov", "wheel"] - -[[package]] -name = "click-repl" -version = "0.3.0" -description = "REPL plugin for Click" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9"}, - {file = "click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812"}, -] - -[package.dependencies] -click = ">=7.0" -prompt-toolkit = ">=3.0.36" - -[package.extras] -testing = ["pytest (>=7.2.1)", "pytest-cov (>=4.0.0)", "tox (>=4.4.3)"] - -[[package]] -name = "cloudflare" -version = "4.3.1" -description = "The official Python library for the cloudflare API" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "cloudflare-4.3.1-py3-none-any.whl", hash = "sha256:6927135a5ee5633d6e2e1952ca0484745e933727aeeb189996d2ad9d292071c6"}, - {file = "cloudflare-4.3.1.tar.gz", hash = "sha256:b1e1c6beeb8d98f63bfe0a1cba874fc4e22e000bcc490544f956c689b3b5b258"}, -] - -[package.dependencies] -anyio = ">=3.5.0,<5" -distro = ">=1.7.0,<2" -httpx = ">=0.23.0,<1" -pydantic = ">=1.9.0,<3" -sniffio = "*" -typing-extensions = ">=4.10,<5" - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["main", "dev"] -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] -markers = {dev = "sys_platform == \"win32\" or platform_system == \"Windows\""} - -[[package]] -name = "contextlib2" -version = "21.6.0" -description = "Backports and enhancements for the contextlib module" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "contextlib2-21.6.0-py2.py3-none-any.whl", hash = "sha256:3fbdb64466afd23abaf6c977627b75b6139a5a3e8ce38405c5b413aed7a0471f"}, - {file = "contextlib2-21.6.0.tar.gz", hash = "sha256:ab1e2bfe1d01d968e1b7e8d9023bc51ef3509bba217bb730cee3827e1ee82869"}, -] - -[[package]] -name = "contourpy" -version = "1.3.3" -description = "Python library for calculating contours of 2D quadrilateral grids" -optional = false -python-versions = ">=3.11" -groups = ["main"] -files = [ - {file = "contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1"}, - {file = "contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381"}, - {file = "contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7"}, - {file = "contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1"}, - {file = "contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a"}, - {file = "contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db"}, - {file = "contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620"}, - {file = "contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f"}, - {file = "contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff"}, - {file = "contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42"}, - {file = "contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470"}, - {file = "contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb"}, - {file = "contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6"}, - {file = "contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7"}, - {file = "contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8"}, - {file = "contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea"}, - {file = "contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1"}, - {file = "contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7"}, - {file = "contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411"}, - {file = "contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69"}, - {file = "contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b"}, - {file = "contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc"}, - {file = "contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5"}, - {file = "contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1"}, - {file = "contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286"}, - {file = "contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5"}, - {file = "contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67"}, - {file = "contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9"}, - {file = "contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659"}, - {file = "contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7"}, - {file = "contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d"}, - {file = "contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263"}, - {file = "contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9"}, - {file = "contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d"}, - {file = "contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216"}, - {file = "contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae"}, - {file = "contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20"}, - {file = "contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99"}, - {file = "contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b"}, - {file = "contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a"}, - {file = "contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e"}, - {file = "contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3"}, - {file = "contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8"}, - {file = "contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301"}, - {file = "contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a"}, - {file = "contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77"}, - {file = "contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5"}, - {file = "contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4"}, - {file = "contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36"}, - {file = "contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3"}, - {file = "contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b"}, - {file = "contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36"}, - {file = "contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d"}, - {file = "contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd"}, - {file = "contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339"}, - {file = "contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772"}, - {file = "contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77"}, - {file = "contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13"}, - {file = "contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe"}, - {file = "contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f"}, - {file = "contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0"}, - {file = "contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4"}, - {file = "contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f"}, - {file = "contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae"}, - {file = "contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc"}, - {file = "contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b"}, - {file = "contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497"}, - {file = "contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8"}, - {file = "contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e"}, - {file = "contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989"}, - {file = "contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77"}, - {file = "contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880"}, -] - -[package.dependencies] -numpy = ">=1.25" - -[package.extras] -bokeh = ["bokeh", "selenium"] -docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"] -mypy = ["bokeh", "contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.17.0)", "types-Pillow"] -test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] -test-no-images = ["pytest", "pytest-cov", "pytest-rerunfailures", "pytest-xdist", "wurlitzer"] - -[[package]] -name = "coverage" -version = "7.5.4" -description = "Code coverage measurement for Python" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "coverage-7.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6cfb5a4f556bb51aba274588200a46e4dd6b505fb1a5f8c5ae408222eb416f99"}, - {file = "coverage-7.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2174e7c23e0a454ffe12267a10732c273243b4f2d50d07544a91198f05c48f47"}, - {file = "coverage-7.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2214ee920787d85db1b6a0bd9da5f8503ccc8fcd5814d90796c2f2493a2f4d2e"}, - {file = "coverage-7.5.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1137f46adb28e3813dec8c01fefadcb8c614f33576f672962e323b5128d9a68d"}, - {file = "coverage-7.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b385d49609f8e9efc885790a5a0e89f2e3ae042cdf12958b6034cc442de428d3"}, - {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b4a474f799456e0eb46d78ab07303286a84a3140e9700b9e154cfebc8f527016"}, - {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5cd64adedf3be66f8ccee418473c2916492d53cbafbfcff851cbec5a8454b136"}, - {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e564c2cf45d2f44a9da56f4e3a26b2236504a496eb4cb0ca7221cd4cc7a9aca9"}, - {file = "coverage-7.5.4-cp310-cp310-win32.whl", hash = "sha256:7076b4b3a5f6d2b5d7f1185fde25b1e54eb66e647a1dfef0e2c2bfaf9b4c88c8"}, - {file = "coverage-7.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:018a12985185038a5b2bcafab04ab833a9a0f2c59995b3cec07e10074c78635f"}, - {file = "coverage-7.5.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:db14f552ac38f10758ad14dd7b983dbab424e731588d300c7db25b6f89e335b5"}, - {file = "coverage-7.5.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3257fdd8e574805f27bb5342b77bc65578e98cbc004a92232106344053f319ba"}, - {file = "coverage-7.5.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a6612c99081d8d6134005b1354191e103ec9705d7ba2754e848211ac8cacc6b"}, - {file = "coverage-7.5.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d45d3cbd94159c468b9b8c5a556e3f6b81a8d1af2a92b77320e887c3e7a5d080"}, - {file = "coverage-7.5.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed550e7442f278af76d9d65af48069f1fb84c9f745ae249c1a183c1e9d1b025c"}, - {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a892be37ca35eb5019ec85402c3371b0f7cda5ab5056023a7f13da0961e60da"}, - {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8192794d120167e2a64721d88dbd688584675e86e15d0569599257566dec9bf0"}, - {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:820bc841faa502e727a48311948e0461132a9c8baa42f6b2b84a29ced24cc078"}, - {file = "coverage-7.5.4-cp311-cp311-win32.whl", hash = "sha256:6aae5cce399a0f065da65c7bb1e8abd5c7a3043da9dceb429ebe1b289bc07806"}, - {file = "coverage-7.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:d2e344d6adc8ef81c5a233d3a57b3c7d5181f40e79e05e1c143da143ccb6377d"}, - {file = "coverage-7.5.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:54317c2b806354cbb2dc7ac27e2b93f97096912cc16b18289c5d4e44fc663233"}, - {file = "coverage-7.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:042183de01f8b6d531e10c197f7f0315a61e8d805ab29c5f7b51a01d62782747"}, - {file = "coverage-7.5.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6bb74ed465d5fb204b2ec41d79bcd28afccf817de721e8a807d5141c3426638"}, - {file = "coverage-7.5.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3d45ff86efb129c599a3b287ae2e44c1e281ae0f9a9bad0edc202179bcc3a2e"}, - {file = "coverage-7.5.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5013ed890dc917cef2c9f765c4c6a8ae9df983cd60dbb635df8ed9f4ebc9f555"}, - {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1014fbf665fef86cdfd6cb5b7371496ce35e4d2a00cda501cf9f5b9e6fced69f"}, - {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3684bc2ff328f935981847082ba4fdc950d58906a40eafa93510d1b54c08a66c"}, - {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:581ea96f92bf71a5ec0974001f900db495488434a6928a2ca7f01eee20c23805"}, - {file = "coverage-7.5.4-cp312-cp312-win32.whl", hash = "sha256:73ca8fbc5bc622e54627314c1a6f1dfdd8db69788f3443e752c215f29fa87a0b"}, - {file = "coverage-7.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:cef4649ec906ea7ea5e9e796e68b987f83fa9a718514fe147f538cfeda76d7a7"}, - {file = "coverage-7.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdd31315fc20868c194130de9ee6bfd99755cc9565edff98ecc12585b90be882"}, - {file = "coverage-7.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:02ff6e898197cc1e9fa375581382b72498eb2e6d5fc0b53f03e496cfee3fac6d"}, - {file = "coverage-7.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d05c16cf4b4c2fc880cb12ba4c9b526e9e5d5bb1d81313d4d732a5b9fe2b9d53"}, - {file = "coverage-7.5.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5986ee7ea0795a4095ac4d113cbb3448601efca7f158ec7f7087a6c705304e4"}, - {file = "coverage-7.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5df54843b88901fdc2f598ac06737f03d71168fd1175728054c8f5a2739ac3e4"}, - {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ab73b35e8d109bffbda9a3e91c64e29fe26e03e49addf5b43d85fc426dde11f9"}, - {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:aea072a941b033813f5e4814541fc265a5c12ed9720daef11ca516aeacd3bd7f"}, - {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:16852febd96acd953b0d55fc842ce2dac1710f26729b31c80b940b9afcd9896f"}, - {file = "coverage-7.5.4-cp38-cp38-win32.whl", hash = "sha256:8f894208794b164e6bd4bba61fc98bf6b06be4d390cf2daacfa6eca0a6d2bb4f"}, - {file = "coverage-7.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:e2afe743289273209c992075a5a4913e8d007d569a406ffed0bd080ea02b0633"}, - {file = "coverage-7.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b95c3a8cb0463ba9f77383d0fa8c9194cf91f64445a63fc26fb2327e1e1eb088"}, - {file = "coverage-7.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3d7564cc09dd91b5a6001754a5b3c6ecc4aba6323baf33a12bd751036c998be4"}, - {file = "coverage-7.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44da56a2589b684813f86d07597fdf8a9c6ce77f58976727329272f5a01f99f7"}, - {file = "coverage-7.5.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e16f3d6b491c48c5ae726308e6ab1e18ee830b4cdd6913f2d7f77354b33f91c8"}, - {file = "coverage-7.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbc5958cb471e5a5af41b0ddaea96a37e74ed289535e8deca404811f6cb0bc3d"}, - {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a04e990a2a41740b02d6182b498ee9796cf60eefe40cf859b016650147908029"}, - {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ddbd2f9713a79e8e7242d7c51f1929611e991d855f414ca9996c20e44a895f7c"}, - {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b1ccf5e728ccf83acd313c89f07c22d70d6c375a9c6f339233dcf792094bcbf7"}, - {file = "coverage-7.5.4-cp39-cp39-win32.whl", hash = "sha256:56b4eafa21c6c175b3ede004ca12c653a88b6f922494b023aeb1e836df953ace"}, - {file = "coverage-7.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:65e528e2e921ba8fd67d9055e6b9f9e34b21ebd6768ae1c1723f4ea6ace1234d"}, - {file = "coverage-7.5.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:79b356f3dd5b26f3ad23b35c75dbdaf1f9e2450b6bcefc6d0825ea0aa3f86ca5"}, - {file = "coverage-7.5.4.tar.gz", hash = "sha256:a44963520b069e12789d0faea4e9fdb1e410cdc4aab89d94f7f55cbb7fef0353"}, -] - -[package.extras] -toml = ["tomli ; python_full_version <= \"3.11.0a6\""] - -[[package]] -name = "cron-descriptor" -version = "1.4.5" -description = "A Python library that converts cron expressions into human readable strings." -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "cron_descriptor-1.4.5-py3-none-any.whl", hash = "sha256:736b3ae9d1a99bc3dbfc5b55b5e6e7c12031e7ba5de716625772f8b02dcd6013"}, - {file = "cron_descriptor-1.4.5.tar.gz", hash = "sha256:f51ce4ffc1d1f2816939add8524f206c376a42c87a5fca3091ce26725b3b1bca"}, -] - -[package.extras] -dev = ["polib"] - -[[package]] -name = "crowdstrike-falconpy" -version = "1.6.0" -description = "The CrowdStrike Falcon SDK for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "crowdstrike_falconpy-1.6.0-py3-none-any.whl", hash = "sha256:2cae39e42510f473e77f3a647d56aa8f55d9de0a1963bad353b195cb5ae61ea4"}, - {file = "crowdstrike_falconpy-1.6.0.tar.gz", hash = "sha256:663402ac9bc56625478460b4865446371de2d74f5e96cb5d16672119c113346f"}, -] - -[package.dependencies] -requests = "*" -urllib3 = "*" - -[package.extras] -dev = ["bandit", "coverage", "flake8", "pydocstyle", "pylint", "pytest", "pytest-cov"] - -[[package]] -name = "cryptography" -version = "46.0.6" -description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -optional = false -python-versions = "!=3.9.0,!=3.9.1,>=3.8" -groups = ["main", "dev"] -files = [ - {file = "cryptography-46.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8"}, - {file = "cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30"}, - {file = "cryptography-46.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a"}, - {file = "cryptography-46.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175"}, - {file = "cryptography-46.0.6-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463"}, - {file = "cryptography-46.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97"}, - {file = "cryptography-46.0.6-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c"}, - {file = "cryptography-46.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507"}, - {file = "cryptography-46.0.6-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19"}, - {file = "cryptography-46.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738"}, - {file = "cryptography-46.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c"}, - {file = "cryptography-46.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f"}, - {file = "cryptography-46.0.6-cp311-abi3-win32.whl", hash = "sha256:bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2"}, - {file = "cryptography-46.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124"}, - {file = "cryptography-46.0.6-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:2ef9e69886cbb137c2aef9772c2e7138dc581fad4fcbcf13cc181eb5a3ab6275"}, - {file = "cryptography-46.0.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7f417f034f91dcec1cb6c5c35b07cdbb2ef262557f701b4ecd803ee8cefed4f4"}, - {file = "cryptography-46.0.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d24c13369e856b94892a89ddf70b332e0b70ad4a5c43cf3e9cb71d6d7ffa1f7b"}, - {file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:aad75154a7ac9039936d50cf431719a2f8d4ed3d3c277ac03f3339ded1a5e707"}, - {file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3c21d92ed15e9cfc6eb64c1f5a0326db22ca9c2566ca46d845119b45b4400361"}, - {file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:4668298aef7cddeaf5c6ecc244c2302a2b8e40f384255505c22875eebb47888b"}, - {file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8ce35b77aaf02f3b59c90b2c8a05c73bac12cea5b4e8f3fbece1f5fddea5f0ca"}, - {file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c89eb37fae9216985d8734c1afd172ba4927f5a05cfd9bf0e4863c6d5465b013"}, - {file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:ed418c37d095aeddf5336898a132fba01091f0ac5844e3e8018506f014b6d2c4"}, - {file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:69cf0056d6947edc6e6760e5f17afe4bea06b56a9ac8a06de9d2bd6b532d4f3a"}, - {file = "cryptography-46.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e7304c4f4e9490e11efe56af6713983460ee0780f16c63f219984dab3af9d2d"}, - {file = "cryptography-46.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b928a3ca837c77a10e81a814a693f2295200adb3352395fad024559b7be7a736"}, - {file = "cryptography-46.0.6-cp314-cp314t-win32.whl", hash = "sha256:97c8115b27e19e592a05c45d0dd89c57f81f841cc9880e353e0d3bf25b2139ed"}, - {file = "cryptography-46.0.6-cp314-cp314t-win_amd64.whl", hash = "sha256:c797e2517cb7880f8297e2c0f43bb910e91381339336f75d2c1c2cbf811b70b4"}, - {file = "cryptography-46.0.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a"}, - {file = "cryptography-46.0.6-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8"}, - {file = "cryptography-46.0.6-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77"}, - {file = "cryptography-46.0.6-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290"}, - {file = "cryptography-46.0.6-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410"}, - {file = "cryptography-46.0.6-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d"}, - {file = "cryptography-46.0.6-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70"}, - {file = "cryptography-46.0.6-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d"}, - {file = "cryptography-46.0.6-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa"}, - {file = "cryptography-46.0.6-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58"}, - {file = "cryptography-46.0.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb"}, - {file = "cryptography-46.0.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72"}, - {file = "cryptography-46.0.6-cp38-abi3-win32.whl", hash = "sha256:7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c"}, - {file = "cryptography-46.0.6-cp38-abi3-win_amd64.whl", hash = "sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f"}, - {file = "cryptography-46.0.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:2ea0f37e9a9cf0df2952893ad145fd9627d326a59daec9b0802480fa3bcd2ead"}, - {file = "cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a3e84d5ec9ba01f8fd03802b2147ba77f0c8f2617b2aff254cedd551844209c8"}, - {file = "cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:12f0fa16cc247b13c43d56d7b35287ff1569b5b1f4c5e87e92cc4fcc00cd10c0"}, - {file = "cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:50575a76e2951fe7dbd1f56d181f8c5ceeeb075e9ff88e7ad997d2f42af06e7b"}, - {file = "cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:90e5f0a7b3be5f40c3a0a0eafb32c681d8d2c181fc2a1bdabe9b3f611d9f6b1a"}, - {file = "cryptography-46.0.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6728c49e3b2c180ef26f8e9f0a883a2c585638db64cf265b49c9ba10652d430e"}, - {file = "cryptography-46.0.6.tar.gz", hash = "sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759"}, -] - -[package.dependencies] -cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""} - -[package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"] -docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] -nox = ["nox[uv] (>=2024.4.15)"] -pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"] -sdist = ["build (>=1.0.0)"] -ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi (>=2024)", "cryptography-vectors (==46.0.6)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] -test-randomorder = ["pytest-randomly"] - -[[package]] -name = "cycler" -version = "0.12.1" -description = "Composable style cycles" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"}, - {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"}, -] - -[package.extras] -docs = ["ipython", "matplotlib", "numpydoc", "sphinx"] -tests = ["pytest", "pytest-cov", "pytest-xdist"] - -[[package]] -name = "darabonba-core" -version = "1.0.5" -description = "The darabonba module of alibabaCloud Python SDK." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "darabonba_core-1.0.5-py3-none-any.whl", hash = "sha256:671ab8dbc4edc2a8f88013da71646839bb8914f1259efc069353243ef52ea27c"}, -] - -[package.dependencies] -aiohttp = ">=3.7.0,<4.0.0" -alibabacloud-tea = "*" -requests = ">=2.21.0,<3.0.0" - -[[package]] -name = "dash" -version = "3.1.1" -description = "A Python framework for building reactive web-apps. Developed by Plotly." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "dash-3.1.1-py3-none-any.whl", hash = "sha256:66fff37e79c6aa114cd55aea13683d1e9afe0e3f96b35388baca95ff6cfdad23"}, - {file = "dash-3.1.1.tar.gz", hash = "sha256:916b31cec46da0a3339da0e9df9f446126aa7f293c0544e07adf9fe4ba060b18"}, -] - -[package.dependencies] -Flask = ">=1.0.4,<3.2" -importlib-metadata = "*" -nest-asyncio = "*" -plotly = ">=5.0.0" -requests = "*" -retrying = "*" -setuptools = "*" -typing-extensions = ">=4.1.1" -Werkzeug = "<3.2" - -[package.extras] -async = ["flask[async]"] -celery = ["celery[redis] (>=5.1.2,<5.4.0)", "kombu (<5.4.0)", "redis (>=3.5.3,<=5.0.4)"] -ci = ["black (==22.3.0)", "flake8 (==7.0.0)", "flaky (==3.8.1)", "flask-talisman (==1.0.0)", "ipython (<9.0.0)", "jupyterlab (<4.0.0)", "mimesis (<=11.1.0)", "mock (==4.0.3)", "mypy (==1.15.0) ; python_version >= \"3.12\"", "numpy (<=1.26.3)", "openpyxl", "orjson (==3.10.3)", "pandas (>=1.4.0)", "pyarrow", "pylint (==3.0.3)", "pyright (==1.1.398) ; python_version >= \"3.7\"", "pytest-mock", "pytest-rerunfailures", "pytest-sugar (==0.9.6)", "pyzmq (==25.1.2)", "xlrd (>=2.0.1)"] -compress = ["flask-compress"] -dev = ["PyYAML (>=5.4.1)", "coloredlogs (>=15.0.1)", "fire (>=0.4.0)"] -diskcache = ["diskcache (>=5.2.1)", "multiprocess (>=0.70.12)", "psutil (>=5.8.0)"] -testing = ["beautifulsoup4 (>=4.8.2)", "cryptography", "dash-testing-stub (>=0.0.2)", "lxml (>=4.6.2)", "multiprocess (>=0.70.12)", "percy (>=2.0.2)", "psutil (>=5.8.0)", "pytest (>=6.0.2)", "requests[security] (>=2.21.0)", "selenium (>=3.141.0,<=4.2.0)", "waitress (>=1.4.4)"] - -[[package]] -name = "dash-bootstrap-components" -version = "2.0.3" -description = "Bootstrap themed components for use in Plotly Dash" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "dash_bootstrap_components-2.0.3-py3-none-any.whl", hash = "sha256:82754d3d001ad5482b8a82b496c7bf98a1c68d2669d607a89dda7ec627304af5"}, - {file = "dash_bootstrap_components-2.0.3.tar.gz", hash = "sha256:5c161b04a6e7ed19a7d54e42f070c29fd6c385d5a7797e7a82999aa2fc15b1de"}, -] - -[package.dependencies] -dash = ">=3.0.4" - -[package.extras] -pandas = ["numpy (>=2.0.2)", "pandas (>=2.2.3)"] - -[[package]] -name = "debugpy" -version = "1.8.20" -description = "An implementation of the Debug Adapter Protocol for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "debugpy-1.8.20-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:157e96ffb7f80b3ad36d808646198c90acb46fdcfd8bb1999838f0b6f2b59c64"}, - {file = "debugpy-1.8.20-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:c1178ae571aff42e61801a38b007af504ec8e05fde1c5c12e5a7efef21009642"}, - {file = "debugpy-1.8.20-cp310-cp310-win32.whl", hash = "sha256:c29dd9d656c0fbd77906a6e6a82ae4881514aa3294b94c903ff99303e789b4a2"}, - {file = "debugpy-1.8.20-cp310-cp310-win_amd64.whl", hash = "sha256:3ca85463f63b5dd0aa7aaa933d97cbc47c174896dcae8431695872969f981893"}, - {file = "debugpy-1.8.20-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:eada6042ad88fa1571b74bd5402ee8b86eded7a8f7b827849761700aff171f1b"}, - {file = "debugpy-1.8.20-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:7de0b7dfeedc504421032afba845ae2a7bcc32ddfb07dae2c3ca5442f821c344"}, - {file = "debugpy-1.8.20-cp311-cp311-win32.whl", hash = "sha256:773e839380cf459caf73cc533ea45ec2737a5cc184cf1b3b796cd4fd98504fec"}, - {file = "debugpy-1.8.20-cp311-cp311-win_amd64.whl", hash = "sha256:1f7650546e0eded1902d0f6af28f787fa1f1dbdbc97ddabaf1cd963a405930cb"}, - {file = "debugpy-1.8.20-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:4ae3135e2089905a916909ef31922b2d733d756f66d87345b3e5e52b7a55f13d"}, - {file = "debugpy-1.8.20-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:88f47850a4284b88bd2bfee1f26132147d5d504e4e86c22485dfa44b97e19b4b"}, - {file = "debugpy-1.8.20-cp312-cp312-win32.whl", hash = "sha256:4057ac68f892064e5f98209ab582abfee3b543fb55d2e87610ddc133a954d390"}, - {file = "debugpy-1.8.20-cp312-cp312-win_amd64.whl", hash = "sha256:a1a8f851e7cf171330679ef6997e9c579ef6dd33c9098458bd9986a0f4ca52e3"}, - {file = "debugpy-1.8.20-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:5dff4bb27027821fdfcc9e8f87309a28988231165147c31730128b1c983e282a"}, - {file = "debugpy-1.8.20-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:84562982dd7cf5ebebfdea667ca20a064e096099997b175fe204e86817f64eaf"}, - {file = "debugpy-1.8.20-cp313-cp313-win32.whl", hash = "sha256:da11dea6447b2cadbf8ce2bec59ecea87cc18d2c574980f643f2d2dfe4862393"}, - {file = "debugpy-1.8.20-cp313-cp313-win_amd64.whl", hash = "sha256:eb506e45943cab2efb7c6eafdd65b842f3ae779f020c82221f55aca9de135ed7"}, - {file = "debugpy-1.8.20-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9c74df62fc064cd5e5eaca1353a3ef5a5d50da5eb8058fcef63106f7bebe6173"}, - {file = "debugpy-1.8.20-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:077a7447589ee9bc1ff0cdf443566d0ecf540ac8aa7333b775ebcb8ce9f4ecad"}, - {file = "debugpy-1.8.20-cp314-cp314-win32.whl", hash = "sha256:352036a99dd35053b37b7803f748efc456076f929c6a895556932eaf2d23b07f"}, - {file = "debugpy-1.8.20-cp314-cp314-win_amd64.whl", hash = "sha256:a98eec61135465b062846112e5ecf2eebb855305acc1dfbae43b72903b8ab5be"}, - {file = "debugpy-1.8.20-cp38-cp38-macosx_15_0_x86_64.whl", hash = "sha256:b773eb026a043e4d9c76265742bc846f2f347da7e27edf7fe97716ea19d6bfc5"}, - {file = "debugpy-1.8.20-cp38-cp38-manylinux_2_34_x86_64.whl", hash = "sha256:20d6e64ea177ab6732bffd3ce8fc6fb8879c60484ce14c3b3fe183b1761459ca"}, - {file = "debugpy-1.8.20-cp38-cp38-win32.whl", hash = "sha256:0dfd9adb4b3c7005e9c33df430bcdd4e4ebba70be533e0066e3a34d210041b66"}, - {file = "debugpy-1.8.20-cp38-cp38-win_amd64.whl", hash = "sha256:60f89411a6c6afb89f18e72e9091c3dfbcfe3edc1066b2043a1f80a3bbb3e11f"}, - {file = "debugpy-1.8.20-cp39-cp39-macosx_15_0_x86_64.whl", hash = "sha256:bff8990f040dacb4c314864da95f7168c5a58a30a66e0eea0fb85e2586a92cd6"}, - {file = "debugpy-1.8.20-cp39-cp39-manylinux_2_34_x86_64.whl", hash = "sha256:70ad9ae09b98ac307b82c16c151d27ee9d68ae007a2e7843ba621b5ce65333b5"}, - {file = "debugpy-1.8.20-cp39-cp39-win32.whl", hash = "sha256:9eeed9f953f9a23850c85d440bf51e3c56ed5d25f8560eeb29add815bd32f7ee"}, - {file = "debugpy-1.8.20-cp39-cp39-win_amd64.whl", hash = "sha256:760813b4fff517c75bfe7923033c107104e76acfef7bda011ffea8736e9a66f8"}, - {file = "debugpy-1.8.20-py2.py3-none-any.whl", hash = "sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7"}, - {file = "debugpy-1.8.20.tar.gz", hash = "sha256:55bc8701714969f1ab89a6d5f2f3d40c36f91b2cbe2f65d98bf8196f6a6a2c33"}, -] - -[[package]] -name = "decorator" -version = "5.2.1" -description = "Decorators for Humans" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a"}, - {file = "decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360"}, -] - -[[package]] -name = "defusedxml" -version = "0.7.1" -description = "XML bomb protection for Python stdlib modules" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -groups = ["main"] -files = [ - {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, - {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, -] - -[[package]] -name = "detect-secrets" -version = "1.5.0" -description = "Tool for detecting secrets in the codebase" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "detect_secrets-1.5.0-py3-none-any.whl", hash = "sha256:e24e7b9b5a35048c313e983f76c4bd09dad89f045ff059e354f9943bf45aa060"}, - {file = "detect_secrets-1.5.0.tar.gz", hash = "sha256:6bb46dcc553c10df51475641bb30fd69d25645cc12339e46c824c1e0c388898a"}, -] - -[package.dependencies] -pyyaml = "*" -requests = "*" - -[package.extras] -gibberish = ["gibberish-detector"] -word-list = ["pyahocorasick"] - -[[package]] -name = "dill" -version = "0.4.1" -description = "serialize all of Python" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d"}, - {file = "dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa"}, -] - -[package.extras] -graph = ["objgraph (>=1.7.2)"] -profile = ["gprof2dot (>=2022.7.29)"] - -[[package]] -name = "distro" -version = "1.9.0" -description = "Distro - an OS platform information API" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, - {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, -] - -[[package]] -name = "dj-rest-auth" -version = "7.0.1" -description = "Authentication and Registration in Django Rest Framework" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "dj-rest-auth-7.0.1.tar.gz", hash = "sha256:3f8c744cbcf05355ff4bcbef0c8a63645da38e29a0fdef3c3332d4aced52fb90"}, -] - -[package.dependencies] -Django = ">=4.2,<6.0" -django-allauth = {version = ">=64.0.0", extras = ["socialaccount"], optional = true, markers = "extra == \"with-social\""} -djangorestframework = ">=3.13.0" - -[package.extras] -with-social = ["django-allauth[socialaccount] (>=64.0.0)"] - -[[package]] -name = "django" -version = "5.1.15" -description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." -optional = false -python-versions = ">=3.10" -groups = ["main", "dev"] -files = [ - {file = "django-5.1.15-py3-none-any.whl", hash = "sha256:117871e58d6eda37f09870b7d73a3d66567b03aecd515b386b1751177c413432"}, - {file = "django-5.1.15.tar.gz", hash = "sha256:46a356b5ff867bece73fc6365e081f21c569973403ee7e9b9a0316f27d0eb947"}, -] - -[package.dependencies] -asgiref = ">=3.8.1,<4" -sqlparse = ">=0.3.1" -tzdata = {version = "*", markers = "sys_platform == \"win32\""} - -[package.extras] -argon2 = ["argon2-cffi (>=19.1.0)"] -bcrypt = ["bcrypt"] - -[[package]] -name = "django-allauth" -version = "65.15.0" -description = "Integrated set of Django applications addressing authentication, registration, account management as well as 3rd party (social) account authentication." -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "django_allauth-65.15.0-py3-none-any.whl", hash = "sha256:ad9fc49c49a9368eaa5bb95456b76e2a4f377b3c6862ee8443507816578c098d"}, - {file = "django_allauth-65.15.0.tar.gz", hash = "sha256:b404d48cf0c3ee14dacc834c541f30adedba2ff1c433980ecc494d6cb0b395a8"}, -] - -[package.dependencies] -asgiref = ">=3.8.1" -Django = ">=4.2.16" -oauthlib = {version = ">=3.3.0,<4", optional = true, markers = "extra == \"socialaccount\""} -pyjwt = {version = ">=2.0,<3", extras = ["crypto"], optional = true, markers = "extra == \"socialaccount\""} -python3-saml = {version = ">=1.15.0,<2.0.0", optional = true, markers = "extra == \"saml\""} -requests = {version = ">=2.0.0,<3", optional = true, markers = "extra == \"socialaccount\""} - -[package.extras] -headless = ["pyjwt[crypto] (>=2.0,<3)"] -headless-spec = ["PyYAML (>=6,<7)"] -idp-oidc = ["oauthlib (>=3.3.0,<4)", "pyjwt[crypto] (>=2.0,<3)"] -mfa = ["fido2 (>=1.1.2,<3)", "qrcode (>=7.0.0,<9)"] -openid = ["python3-openid (>=3.0.8,<4)"] -saml = ["python3-saml (>=1.15.0,<2.0.0)"] -socialaccount = ["oauthlib (>=3.3.0,<4)", "pyjwt[crypto] (>=2.0,<3)", "requests (>=2.0.0,<3)"] -steam = ["python3-openid (>=3.0.8,<4)"] - -[[package]] -name = "django-celery-beat" -version = "2.9.0" -description = "Database-backed Periodic Tasks." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "django_celery_beat-2.9.0-py3-none-any.whl", hash = "sha256:4a9e5ebe26d6f8d7215e1fc5c46e466016279dc102435a28141108649bdf2157"}, - {file = "django_celery_beat-2.9.0.tar.gz", hash = "sha256:92404650f52fcb44cf08e2b09635cb1558327c54b1a5d570f0e2d3a22130934c"}, -] - -[package.dependencies] -celery = ">=5.2.3,<6.0" -cron-descriptor = ">=1.2.32,<2.0.0" -Django = ">=2.2,<6.1" -django-timezone-field = ">=5.0" -python-crontab = ">=2.3.4" -tzdata = "*" - -[[package]] -name = "django-celery-results" -version = "2.6.0" -description = "Celery result backends for Django." -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "django_celery_results-2.6.0-py3-none-any.whl", hash = "sha256:b9ccdca2695b98c7cbbb8dea742311ba9a92773d71d7b4944a676e69a7df1c73"}, - {file = "django_celery_results-2.6.0.tar.gz", hash = "sha256:9abcd836ae6b61063779244d8887a88fe80bbfaba143df36d3cb07034671277c"}, -] - -[package.dependencies] -celery = ">=5.2.7,<6.0" -Django = ">=3.2.25" - -[[package]] -name = "django-cors-headers" -version = "4.4.0" -description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "django_cors_headers-4.4.0-py3-none-any.whl", hash = "sha256:5c6e3b7fe870876a1efdfeb4f433782c3524078fa0dc9e0195f6706ce7a242f6"}, - {file = "django_cors_headers-4.4.0.tar.gz", hash = "sha256:92cf4633e22af67a230a1456cb1b7a02bb213d6536d2dcb2a4a24092ea9cebc2"}, -] - -[package.dependencies] -asgiref = ">=3.6" -django = ">=3.2" - -[[package]] -name = "django-environ" -version = "0.11.2" -description = "A package that allows you to utilize 12factor inspired environment variables to configure your Django application." -optional = false -python-versions = ">=3.6,<4" -groups = ["main"] -files = [ - {file = "django-environ-0.11.2.tar.gz", hash = "sha256:f32a87aa0899894c27d4e1776fa6b477e8164ed7f6b3e410a62a6d72caaf64be"}, - {file = "django_environ-0.11.2-py2.py3-none-any.whl", hash = "sha256:0ff95ab4344bfeff693836aa978e6840abef2e2f1145adff7735892711590c05"}, -] - -[package.extras] -develop = ["coverage[toml] (>=5.0a4)", "furo (>=2021.8.17b43,<2021.9.dev0)", "pytest (>=4.6.11)", "sphinx (>=3.5.0)", "sphinx-notfound-page"] -docs = ["furo (>=2021.8.17b43,<2021.9.dev0)", "sphinx (>=3.5.0)", "sphinx-notfound-page"] -testing = ["coverage[toml] (>=5.0a4)", "pytest (>=4.6.11)"] - -[[package]] -name = "django-filter" -version = "24.3" -description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "django_filter-24.3-py3-none-any.whl", hash = "sha256:c4852822928ce17fb699bcfccd644b3574f1a2d80aeb2b4ff4f16b02dd49dc64"}, - {file = "django_filter-24.3.tar.gz", hash = "sha256:d8ccaf6732afd21ca0542f6733b11591030fa98669f8d15599b358e24a2cd9c3"}, -] - -[package.dependencies] -Django = ">=4.2" - -[[package]] -name = "django-guid" -version = "3.5.0" -description = "Middleware that enables single request-response cycle tracing by injecting a unique ID into project logs" -optional = false -python-versions = "<4.0,>=3.8" -groups = ["main"] -files = [ - {file = "django_guid-3.5.0-py3-none-any.whl", hash = "sha256:28f52cfeac47e8e22ea889a3845bc2b1c604dd842e495dadd44ad5184db72c76"}, - {file = "django_guid-3.5.0.tar.gz", hash = "sha256:5f32f70287e4f36addc79f29f2a7b2f56fc5f4e4cfb2023141525be8baa35d9e"}, -] - -[package.dependencies] -django = {version = ">=4.0,<6.0", markers = "python_version >= \"3.10\""} - -[[package]] -name = "django-postgres-extra" -version = "2.0.9" -description = "Bringing all of PostgreSQL's awesomeness to Django." -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "django_postgres_extra-2.0.9-py3-none-any.whl", hash = "sha256:0a4f9fd7f843e2ef5cfe7a291fcb883bd72d2ca8e4b369d0070866205e00b404"}, - {file = "django_postgres_extra-2.0.9.tar.gz", hash = "sha256:9e47c436d033712d0e2611b8c1583566dd4f97700e5360001bf3623913a92046"}, -] - -[package.dependencies] -Django = ">=2.0,<6.0" -python-dateutil = ">=2.8.0,<=3.0.0" - -[package.extras] -analysis = ["autoflake (==1.4)", "autopep8 (==1.6.0)", "black (==22.3.0)", "django-stubs (==1.16.0) ; python_version > \"3.6\"", "django-stubs (==1.9.0) ; python_version <= \"3.6\"", "docformatter (==1.4)", "flake8 (==4.0.1)", "isort (==5.10.0)", "mypy (==0.971) ; python_version <= \"3.6\"", "mypy (==1.2.0) ; python_version > \"3.6\"", "types-dj-database-url (==1.3.0.0)", "types-psycopg2 (==2.9.21.9)", "types-python-dateutil (==2.8.19.12)", "typing-extensions (==4.1.0) ; python_version <= \"3.6\"", "typing-extensions (==4.5.0) ; python_version > \"3.6\""] -docs = ["Sphinx (==2.2.0)", "docutils (<0.18)", "sphinx-rtd-theme (==0.4.3)"] -publish = ["build (==0.7.0)", "twine (==3.7.1)"] -test = ["coveralls (==3.3.0)", "dj-database-url (==0.5.0)", "freezegun (==1.1.0)", "psycopg2 (>=2.8.4,<3.0.0)", "pytest (==6.2.5)", "pytest-benchmark (==3.4.1)", "pytest-cov (==3.0.0)", "pytest-django (==4.4.0)", "pytest-freezegun (==0.4.2)", "pytest-lazy-fixture (==0.6.3)", "snapshottest (==0.6.0)", "tox (==3.24.4)"] - -[[package]] -name = "django-silk" -version = "5.3.2" -description = "Silky smooth profiling for the Django Framework" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "django_silk-5.3.2-py3-none-any.whl", hash = "sha256:49f1caebfda28b1707f0cfef524e0476beb82b8c5e40f5ccff7f73a6b4f6d3ac"}, - {file = "django_silk-5.3.2.tar.gz", hash = "sha256:b0db54eebedb8d16f572321bd6daccac0bd3f547ae2618bb45d96fe8fc02229d"}, -] - -[package.dependencies] -autopep8 = "*" -Django = ">=4.2" -gprof2dot = ">=2017.9.19" -sqlparse = "*" - -[[package]] -name = "django-timezone-field" -version = "7.2.1" -description = "A Django app providing DB, form, and REST framework fields for zoneinfo and pytz timezone objects." -optional = false -python-versions = "<4.0,>=3.8" -groups = ["main"] -files = [ - {file = "django_timezone_field-7.2.1-py3-none-any.whl", hash = "sha256:276915b72c5816f57c3baf9e43f816c695ef940d1b21f91ebf6203c09bf4ad44"}, - {file = "django_timezone_field-7.2.1.tar.gz", hash = "sha256:def846f9e7200b7b8f2a28fcce2b78fb2d470f6a9f272b07c4e014f6ba4c6d2e"}, -] - -[package.dependencies] -Django = ">=3.2,<6.1" - -[[package]] -name = "djangorestframework" -version = "3.15.2" -description = "Web APIs for Django, made easy." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "djangorestframework-3.15.2-py3-none-any.whl", hash = "sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20"}, - {file = "djangorestframework-3.15.2.tar.gz", hash = "sha256:36fe88cd2d6c6bec23dca9804bab2ba5517a8bb9d8f47ebc68981b56840107ad"}, -] - -[package.dependencies] -django = ">=4.2" - -[[package]] -name = "djangorestframework-jsonapi" -version = "7.0.2" -description = "A Django REST framework API adapter for the JSON:API spec." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "djangorestframework-jsonapi-7.0.2.tar.gz", hash = "sha256:d6c72a2bee539f1093dd86620e862af2d1a0e60408e38a710146286dbde71d75"}, - {file = "djangorestframework_jsonapi-7.0.2-py2.py3-none-any.whl", hash = "sha256:be457adb50aac77eec8893048bf46ad6926dcd26204aa10965a1430610828d50"}, -] - -[package.dependencies] -django = ">=4.2" -djangorestframework = ">=3.14" -inflection = ">=0.5.0" - -[package.extras] -django-filter = ["django-filter (>=2.4)"] -django-polymorphic = ["django-polymorphic (>=3.0)"] -openapi = ["pyyaml (>=5.4)", "uritemplate (>=3.0.1)"] - -[[package]] -name = "djangorestframework-simplejwt" -version = "5.5.1" -description = "A minimal JSON Web Token authentication plugin for Django REST Framework" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "djangorestframework_simplejwt-5.5.1-py3-none-any.whl", hash = "sha256:2c30f3707053d384e9f315d11c2daccfcb548d4faa453111ca19a542b732e469"}, - {file = "djangorestframework_simplejwt-5.5.1.tar.gz", hash = "sha256:e72c5572f51d7803021288e2057afcbd03f17fe11d484096f40a460abc76e87f"}, -] - -[package.dependencies] -django = ">=4.2" -djangorestframework = ">=3.14" -pyjwt = ">=1.7.1" - -[package.extras] -crypto = ["cryptography (>=3.3.1)"] -dev = ["Sphinx", "cryptography", "freezegun", "ipython", "pre-commit", "pytest", "pytest-cov", "pytest-django", "pytest-watch", "pytest-xdist", "python-jose (==3.3.0)", "pyupgrade", "ruff", "sphinx_rtd_theme (>=0.1.9)", "tox", "twine", "wheel", "yesqa"] -doc = ["Sphinx", "sphinx_rtd_theme (>=0.1.9)"] -lint = ["pre-commit", "pyupgrade", "ruff", "yesqa"] -python-jose = ["python-jose (==3.3.0)"] -test = ["cryptography", "freezegun", "pytest", "pytest-cov", "pytest-django", "pytest-xdist", "tox"] - -[[package]] -name = "dnspython" -version = "2.8.0" -description = "DNS toolkit" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af"}, - {file = "dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f"}, -] - -[package.extras] -dev = ["black (>=25.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "hypercorn (>=0.17.0)", "mypy (>=1.17)", "pylint (>=3)", "pytest (>=8.4)", "pytest-cov (>=6.2.0)", "quart-trio (>=0.12.0)", "sphinx (>=8.2.0)", "sphinx-rtd-theme (>=3.0.0)", "twine (>=6.1.0)", "wheel (>=0.45.0)"] -dnssec = ["cryptography (>=45)"] -doh = ["h2 (>=4.2.0)", "httpcore (>=1.0.0)", "httpx (>=0.28.0)"] -doq = ["aioquic (>=1.2.0)"] -idna = ["idna (>=3.10)"] -trio = ["trio (>=0.30)"] -wmi = ["wmi (>=1.5.1) ; platform_system == \"Windows\""] - -[[package]] -name = "docker" -version = "7.1.0" -description = "A Python library for the Docker Engine API." -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0"}, - {file = "docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c"}, -] - -[package.dependencies] -pywin32 = {version = ">=304", markers = "sys_platform == \"win32\""} -requests = ">=2.26.0" -urllib3 = ">=1.26.0" - -[package.extras] -dev = ["coverage (==7.2.7)", "pytest (==7.4.2)", "pytest-cov (==4.1.0)", "pytest-timeout (==2.1.0)", "ruff (==0.1.8)"] -docs = ["myst-parser (==0.18.0)", "sphinx (==5.1.1)"] -ssh = ["paramiko (>=2.4.3)"] -websockets = ["websocket-client (>=1.3.0)"] - -[[package]] -name = "dogpile-cache" -version = "1.5.0" -description = "A caching front-end based on the Dogpile lock." -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "dogpile_cache-1.5.0-py3-none-any.whl", hash = "sha256:dc7b47d37844db15e8fdc0243c1b58857a2ddc52a5118237a97127bac200e18d"}, - {file = "dogpile_cache-1.5.0.tar.gz", hash = "sha256:849c5573c9a38f155cd4173103c702b637ede0361c12e864876877d0cd125eec"}, -] - -[package.dependencies] -decorator = ">=4.0.0" -stevedore = ">=3.0.0" - -[package.extras] -bmemcached = ["python-binary-memcached"] -memcached = ["python-memcached"] -pifpaf = ["pifpaf (>=3.3.0)"] -pylibmc = ["pylibmc"] -pymemcache = ["pymemcache"] -redis = ["redis"] -valkey = ["valkey"] - -[[package]] -name = "dparse" -version = "0.6.4" -description = "A parser for Python dependency files" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "dparse-0.6.4-py3-none-any.whl", hash = "sha256:fbab4d50d54d0e739fbb4dedfc3d92771003a5b9aa8545ca7a7045e3b174af57"}, - {file = "dparse-0.6.4.tar.gz", hash = "sha256:90b29c39e3edc36c6284c82c4132648eaf28a01863eb3c231c2512196132201a"}, -] - -[package.dependencies] -packaging = "*" - -[package.extras] -all = ["pipenv", "poetry", "pyyaml"] -conda = ["pyyaml"] -pipenv = ["pipenv"] -poetry = ["poetry"] - -[[package]] -name = "drf-extensions" -version = "0.8.0" -description = "Extensions for Django REST Framework" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "drf_extensions-0.8.0-py2.py3-none-any.whl", hash = "sha256:ab7bd854c9061c27ab55233b66d758001e5c2d81aaa9d117cbbe1c9ea49c77ab"}, - {file = "drf_extensions-0.8.0.tar.gz", hash = "sha256:c3f27bca74a2def53e8454a5c7b327595195df51e121743120b2f51ef5a52aaa"}, -] - -[package.dependencies] -Django = ">=2.2,<6.0" -djangorestframework = ">=3.10.3" -packaging = ">=24.1" - -[[package]] -name = "drf-nested-routers" -version = "0.95.0" -description = "Nested resources for the Django Rest Framework" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "drf_nested_routers-0.95.0-py2.py3-none-any.whl", hash = "sha256:dd489c33d667aaa81383ffaa8c74781d2b353d8f0795716ae37fc59ee297b7c4"}, - {file = "drf_nested_routers-0.95.0.tar.gz", hash = "sha256:815978f802e578fd7035c74040c104909cbe97615de89a275d77e928f4029891"}, -] - -[package.dependencies] -Django = ">=4.2" -djangorestframework = ">=3.15.0" - -[[package]] -name = "drf-simple-apikey" -version = "2.2.1" -description = "API Key authentication and permissions for Django REST." -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "drf_simple_apikey-2.2.1-py2.py3-none-any.whl", hash = "sha256:2a60b35676d14f907c47dee179dd0fa7425a84c34d6ff5b48d08d3b87ff32809"}, - {file = "drf_simple_apikey-2.2.1.tar.gz", hash = "sha256:e5a52804bbac12c8db80c10a3d51a8514fc59fc8385b5e751099a2bc944ad25d"}, -] - -[package.dependencies] -cryptography = ">=38.0.4" -django = ">=4.2" -djangorestframework = ">=3.14.0" - -[package.extras] -test = ["coverage", "pytest", "pytest-django"] -tooling = ["black (==22.3.0)", "bump2version", "pylint"] - -[[package]] -name = "drf-spectacular" -version = "0.27.2" -description = "Sane and flexible OpenAPI 3 schema generation for Django REST framework" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "drf-spectacular-0.27.2.tar.gz", hash = "sha256:a199492f2163c4101055075ebdbb037d59c6e0030692fc83a1a8c0fc65929981"}, - {file = "drf_spectacular-0.27.2-py3-none-any.whl", hash = "sha256:b1c04bf8b2fbbeaf6f59414b4ea448c8787aba4d32f76055c3b13335cf7ec37b"}, -] - -[package.dependencies] -Django = ">=2.2" -djangorestframework = ">=3.10.3" -inflection = ">=0.3.1" -jsonschema = ">=2.6.0" -PyYAML = ">=5.1" -uritemplate = ">=2.0.0" - -[package.extras] -offline = ["drf-spectacular-sidecar"] -sidecar = ["drf-spectacular-sidecar"] - -[[package]] -name = "drf-spectacular-jsonapi" -version = "0.5.1" -description = "open api 3 schema generator for drf-json-api package based on drf-spectacular package." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "drf-spectacular-jsonapi-0.5.1.tar.gz", hash = "sha256:e45f87f3cce2692f4f546e0785d8fcc32c6b49770fff858065c267ae8f9cfde5"}, - {file = "drf_spectacular_jsonapi-0.5.1-py3-none-any.whl", hash = "sha256:abac728abd83e2544408cc900d682d532ca2088f2f9321d1c9101bcdfdabca78"}, -] - -[package.dependencies] -Django = ">=3.2" -djangorestframework = ">=3.13" -djangorestframework-jsonapi = ">=6.0.0" -drf-extensions = ">=0.7.1" -drf-spectacular = ">=0.25.0" - -[[package]] -name = "dulwich" -version = "0.23.0" -description = "Python Git Library" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "dulwich-0.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c13b0d5a9009cde23ecb8cb201df6e23e2a7a82c5e2d6ba6443fbb322c9befc6"}, - {file = "dulwich-0.23.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:a68faf8612bf93de1285048d6ad13160f0fb3c5596a86e694e78f4e212886fa5"}, - {file = "dulwich-0.23.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:d971566826f16ec67c70641c1fbdb337323aa5b533799bc5a4641f4750e73b36"}, - {file = "dulwich-0.23.0-cp310-cp310-win32.whl", hash = "sha256:27d970adf539806dfc4fe3e4c9e8dc6ebf0318977a56e24d22f13413535a51ba"}, - {file = "dulwich-0.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:025178533e884ffdb0d9d8db4b8870745d438cbfecb782fd1b56c3b6438e86cf"}, - {file = "dulwich-0.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d68498fdda13ab00791b483daab3bcfe9f9721c037aa458695e6ad81640c57cc"}, - {file = "dulwich-0.23.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:cb7bb930b12471a1cfcea4b3d25a671dc0ad32573f0ad25684684298959a1527"}, - {file = "dulwich-0.23.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a2abbce32fd2bc7902bcc5f69b10bf22576810de21651baaa864b78fd7aec261"}, - {file = "dulwich-0.23.0-cp311-cp311-win32.whl", hash = "sha256:9e3151f10ce2a9ff91bca64c74345217f53bdd947dc958032343822009832f7a"}, - {file = "dulwich-0.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:3ae9f1d9dc92d4e9a3f89ba2c55221f7b6442c5dd93b3f6f539a3c9eb3f37bdd"}, - {file = "dulwich-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:52cdef66a7994d29528ca79ca59452518bbba3fd56a9c61c61f6c467c1c7956e"}, - {file = "dulwich-0.23.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:d473888a6ab9ed5d4a4c3f053cbe5b77f72d54b6efdf5688fed76094316e571e"}, - {file = "dulwich-0.23.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:19fcf20224c641a61c774da92f098fbaae9938c7e17a52841e64092adf7e78f9"}, - {file = "dulwich-0.23.0-cp312-cp312-win32.whl", hash = "sha256:7fc8b76b704ef35cd001e993e3aa4e1d666a2064bf467c07c560f12b2959dcaf"}, - {file = "dulwich-0.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:cb0566b888b578325350b4d67c61a0de35d417e9877560e3a6df88cae4576a59"}, - {file = "dulwich-0.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:624e2223c8b705b3a217f9c8d3bfed3a573093be0b0ba033c46cba8411fb9630"}, - {file = "dulwich-0.23.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:b4eaf326d15bb3fc5316c777b0312f0fe02f6f82a4368cd971d0ce2167b7ec34"}, - {file = "dulwich-0.23.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:d754afaf7c133a015c75cc2be11703138b4be932e0eeeb2c70add56083f31109"}, - {file = "dulwich-0.23.0-cp313-cp313-win32.whl", hash = "sha256:ac53ec438bde3c1f479782c34240479b36cd47230d091979137b7ecc12c0242e"}, - {file = "dulwich-0.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:50d3b4ba45671fb8b7d2afbd02c10b4edbc3290a1f92260e64098b409e9ca35c"}, - {file = "dulwich-0.23.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d8e18ea3fa49f10932077f39c0b960b5045870c550c3d7c74f3cfaac09457cd6"}, - {file = "dulwich-0.23.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:3e6df0eb8cca21f210e3ddce2ccb64482646893dbec2fee9f3411d037595bf7b"}, - {file = "dulwich-0.23.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:90c0064d7df8e7fe83d3a03c7d60b9e07a92698b18442f926199b2c3f0bf34d4"}, - {file = "dulwich-0.23.0-cp39-cp39-win32.whl", hash = "sha256:84eef513aba501cbc1f223863f3b4b351fe732d3fb590cab9bdf5d33eb1a1248"}, - {file = "dulwich-0.23.0-cp39-cp39-win_amd64.whl", hash = "sha256:dce943da48217c26e15790fd6df62d27a7f1d067102780351ebf2635fc0ba482"}, - {file = "dulwich-0.23.0-py3-none-any.whl", hash = "sha256:d8da6694ca332bb48775e35ee2215aa4673821164a91b83062f699c69f7cd135"}, - {file = "dulwich-0.23.0.tar.gz", hash = "sha256:0aa6c2489dd5e978b27e9b75983b7331a66c999f0efc54ebe37cab808ed322ae"}, -] - -[package.dependencies] -urllib3 = ">=1.25" - -[package.extras] -dev = ["dissolve (>=0.1.1)", "mypy (==1.16.0)", "ruff (==0.11.13)"] -fastimport = ["fastimport"] -https = ["urllib3 (>=1.24.1)"] -merge = ["merge3"] -paramiko = ["paramiko"] -pgp = ["gpg"] - -[[package]] -name = "duo-client" -version = "5.5.0" -description = "Reference client for Duo Security APIs" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "duo_client-5.5.0-py3-none-any.whl", hash = "sha256:4fbf1e97a2b25ef64e9f88171ab817162cf45bafc1c63026af4883baf8892a12"}, - {file = "duo_client-5.5.0.tar.gz", hash = "sha256:303109e047fe7525ba4fc4a294c1f3deb4125066e89c10d33f7430378867b1d6"}, -] - -[package.dependencies] -setuptools = "*" - -[[package]] -name = "durationpy" -version = "0.10" -description = "Module for converting between datetime.timedelta and Go's Duration strings." -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286"}, - {file = "durationpy-0.10.tar.gz", hash = "sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba"}, -] - -[[package]] -name = "email-validator" -version = "2.2.0" -description = "A robust email address syntax and deliverability validation library." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631"}, - {file = "email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7"}, -] - -[package.dependencies] -dnspython = ">=2.0.0" -idna = ">=2.0.0" - -[[package]] -name = "execnet" -version = "2.1.2" -description = "execnet: rapid multi-Python deployment" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec"}, - {file = "execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd"}, -] - -[package.extras] -testing = ["hatch", "pre-commit", "pytest", "tox"] - -[[package]] -name = "filelock" -version = "3.20.3" -description = "A platform independent file lock." -optional = false -python-versions = ">=3.10" -groups = ["main", "dev"] -files = [ - {file = "filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1"}, - {file = "filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1"}, -] - -[[package]] -name = "flask" -version = "3.1.3" -description = "A simple framework for building complex web applications." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c"}, - {file = "flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb"}, -] - -[package.dependencies] -blinker = ">=1.9.0" -click = ">=8.1.3" -itsdangerous = ">=2.2.0" -jinja2 = ">=3.1.2" -markupsafe = ">=2.1.1" -werkzeug = ">=3.1.0" - -[package.extras] -async = ["asgiref (>=3.2)"] -dotenv = ["python-dotenv"] - -[[package]] -name = "fonttools" -version = "4.62.1" -description = "Tools to manipulate font files" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "fonttools-4.62.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ad5cca75776cd453b1b035b530e943334957ae152a36a88a320e779d61fc980c"}, - {file = "fonttools-4.62.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b3ae47e8636156a9accff64c02c0924cbebad62854c4a6dbdc110cd5b4b341a"}, - {file = "fonttools-4.62.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b9e288b4da2f64fd6180644221749de651703e8d0c16bd4b719533a3a7d6e3"}, - {file = "fonttools-4.62.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7bca7a1c1faf235ffe25d4f2e555246b4750220b38de8261d94ebc5ce8a23c23"}, - {file = "fonttools-4.62.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b4e0fcf265ad26e487c56cb12a42dffe7162de708762db951e1b3f755319507d"}, - {file = "fonttools-4.62.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2d850f66830a27b0d498ee05adb13a3781637b1826982cd7e2b3789ef0cc71ae"}, - {file = "fonttools-4.62.1-cp310-cp310-win32.whl", hash = "sha256:486f32c8047ccd05652aba17e4a8819a3a9d78570eb8a0e3b4503142947880ed"}, - {file = "fonttools-4.62.1-cp310-cp310-win_amd64.whl", hash = "sha256:5a648bde915fba9da05ae98856987ca91ba832949a9e2888b48c47ef8b96c5a9"}, - {file = "fonttools-4.62.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:40975849bac44fb0b9253d77420c6d8b523ac4dcdcefeff6e4d706838a5b80f7"}, - {file = "fonttools-4.62.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9dde91633f77fa576879a0c76b1d89de373cae751a98ddf0109d54e173b40f14"}, - {file = "fonttools-4.62.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6acb4109f8bee00fec985c8c7afb02299e35e9c94b57287f3ea542f28bd0b0a7"}, - {file = "fonttools-4.62.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1c5c25671ce8805e0d080e2ffdeca7f1e86778c5cbfbeae86d7f866d8830517b"}, - {file = "fonttools-4.62.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a5d8825e1140f04e6c99bb7d37a9e31c172f3bc208afbe02175339e699c710e1"}, - {file = "fonttools-4.62.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:268abb1cb221e66c014acc234e872b7870d8b5d4657a83a8f4205094c32d2416"}, - {file = "fonttools-4.62.1-cp311-cp311-win32.whl", hash = "sha256:942b03094d7edbb99bdf1ae7e9090898cad7bf9030b3d21f33d7072dbcb51a53"}, - {file = "fonttools-4.62.1-cp311-cp311-win_amd64.whl", hash = "sha256:e8514f4924375f77084e81467e63238b095abda5107620f49421c368a6017ed2"}, - {file = "fonttools-4.62.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:90365821debbd7db678809c7491ca4acd1e0779b9624cdc6ddaf1f31992bf974"}, - {file = "fonttools-4.62.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12859ff0b47dd20f110804c3e0d0970f7b832f561630cd879969011541a464a9"}, - {file = "fonttools-4.62.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c125ffa00c3d9003cdaaf7f2c79e6e535628093e14b5de1dccb08859b680936"}, - {file = "fonttools-4.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:149f7d84afca659d1a97e39a4778794a2f83bf344c5ee5134e09995086cc2392"}, - {file = "fonttools-4.62.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0aa72c43a601cfa9273bb1ae0518f1acadc01ee181a6fc60cd758d7fdadffc04"}, - {file = "fonttools-4.62.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:19177c8d96c7c36359266e571c5173bcee9157b59cfc8cb0153c5673dc5a3a7d"}, - {file = "fonttools-4.62.1-cp312-cp312-win32.whl", hash = "sha256:a24decd24d60744ee8b4679d38e88b8303d86772053afc29b19d23bb8207803c"}, - {file = "fonttools-4.62.1-cp312-cp312-win_amd64.whl", hash = "sha256:9e7863e10b3de72376280b515d35b14f5eeed639d1aa7824f4cf06779ec65e42"}, - {file = "fonttools-4.62.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c22b1014017111c401469e3acc5433e6acf6ebcc6aa9efb538a533c800971c79"}, - {file = "fonttools-4.62.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68959f5fc58ed4599b44aad161c2837477d7f35f5f79402d97439974faebfebe"}, - {file = "fonttools-4.62.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef46db46c9447103b8f3ff91e8ba009d5fe181b1920a83757a5762551e32bb68"}, - {file = "fonttools-4.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6706d1cb1d5e6251a97ad3c1b9347505c5615c112e66047abbef0f8545fa30d1"}, - {file = "fonttools-4.62.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e7abd2b1e11736f58c1de27819e1955a53267c21732e78243fa2fa2e5c1e069"}, - {file = "fonttools-4.62.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:403d28ce06ebfc547fbcb0cb8b7f7cc2f7a2d3e1a67ba9a34b14632df9e080f9"}, - {file = "fonttools-4.62.1-cp313-cp313-win32.whl", hash = "sha256:93c316e0f5301b2adbe6a5f658634307c096fd5aae60a5b3412e4f3e1728ab24"}, - {file = "fonttools-4.62.1-cp313-cp313-win_amd64.whl", hash = "sha256:7aa21ff53e28a9c2157acbc44e5b401149d3c9178107130e82d74ceb500e5056"}, - {file = "fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca"}, - {file = "fonttools-4.62.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aa69d10ed420d8121118e628ad47d86e4caa79ba37f968597b958f6cceab7eca"}, - {file = "fonttools-4.62.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd13b7999d59c5eb1c2b442eb2d0c427cb517a0b7a1f5798fc5c9e003f5ff782"}, - {file = "fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae"}, - {file = "fonttools-4.62.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d241cdc4a67b5431c6d7f115fdf63335222414995e3a1df1a41e1182acd4bcc7"}, - {file = "fonttools-4.62.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c05557a78f8fa514da0f869556eeda40887a8abc77c76ee3f74cf241778afd5a"}, - {file = "fonttools-4.62.1-cp314-cp314-win32.whl", hash = "sha256:49a445d2f544ce4a69338694cad575ba97b9a75fff02720da0882d1a73f12800"}, - {file = "fonttools-4.62.1-cp314-cp314-win_amd64.whl", hash = "sha256:1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e"}, - {file = "fonttools-4.62.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1596aeaddf7f78e21e68293c011316a25267b3effdaccaf4d59bc9159d681b82"}, - {file = "fonttools-4.62.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8f8fca95d3bb3208f59626a4b0ea6e526ee51f5a8ad5d91821c165903e8d9260"}, - {file = "fonttools-4.62.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee91628c08e76f77b533d65feb3fbe6d9dad699f95be51cf0d022db94089cdc4"}, - {file = "fonttools-4.62.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f37df1cac61d906e7b836abe356bc2f34c99d4477467755c216b72aa3dc748b"}, - {file = "fonttools-4.62.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92bb00a947e666169c99b43753c4305fc95a890a60ef3aeb2a6963e07902cc87"}, - {file = "fonttools-4.62.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bdfe592802ef939a0e33106ea4a318eeb17822c7ee168c290273cbd5fabd746c"}, - {file = "fonttools-4.62.1-cp314-cp314t-win32.whl", hash = "sha256:b820fcb92d4655513d8402d5b219f94481c4443d825b4372c75a2072aa4b357a"}, - {file = "fonttools-4.62.1-cp314-cp314t-win_amd64.whl", hash = "sha256:59b372b4f0e113d3746b88985f1c796e7bf830dd54b28374cd85c2b8acd7583e"}, - {file = "fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd"}, - {file = "fonttools-4.62.1.tar.gz", hash = "sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d"}, -] - -[package.extras] -all = ["brotli (>=1.0.1) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\"", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres ; platform_python_implementation == \"PyPy\"", "pycairo", "scipy ; platform_python_implementation != \"PyPy\"", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.45.0)", "unicodedata2 (>=17.0.0) ; python_version <= \"3.14\"", "xattr ; sys_platform == \"darwin\"", "zopfli (>=0.1.4)"] -graphite = ["lz4 (>=1.7.4.2)"] -interpolatable = ["munkres ; platform_python_implementation == \"PyPy\"", "pycairo", "scipy ; platform_python_implementation != \"PyPy\""] -lxml = ["lxml (>=4.0)"] -pathops = ["skia-pathops (>=0.5.0)"] -plot = ["matplotlib"] -repacker = ["uharfbuzz (>=0.45.0)"] -symfont = ["sympy"] -type1 = ["xattr ; sys_platform == \"darwin\""] -unicode = ["unicodedata2 (>=17.0.0) ; python_version <= \"3.14\""] -woff = ["brotli (>=1.0.1) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\"", "zopfli (>=0.1.4)"] - -[[package]] -name = "freezegun" -version = "1.5.1" -description = "Let your Python tests travel through time" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "freezegun-1.5.1-py3-none-any.whl", hash = "sha256:bf111d7138a8abe55ab48a71755673dbaa4ab87f4cff5634a4442dfec34c15f1"}, - {file = "freezegun-1.5.1.tar.gz", hash = "sha256:b29dedfcda6d5e8e083ce71b2b542753ad48cfec44037b3fc79702e2980a89e9"}, -] - -[package.dependencies] -python-dateutil = ">=2.7" - -[[package]] -name = "frozenlist" -version = "1.8.0" -description = "A list-like structure which implements collections.abc.MutableSequence" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011"}, - {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565"}, - {file = "frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad"}, - {file = "frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2"}, - {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186"}, - {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e"}, - {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450"}, - {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef"}, - {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4"}, - {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff"}, - {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c"}, - {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f"}, - {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7"}, - {file = "frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a"}, - {file = "frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6"}, - {file = "frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e"}, - {file = "frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84"}, - {file = "frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9"}, - {file = "frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93"}, - {file = "frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f"}, - {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695"}, - {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52"}, - {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581"}, - {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567"}, - {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b"}, - {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92"}, - {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d"}, - {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd"}, - {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967"}, - {file = "frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25"}, - {file = "frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b"}, - {file = "frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a"}, - {file = "frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1"}, - {file = "frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b"}, - {file = "frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4"}, - {file = "frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383"}, - {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4"}, - {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8"}, - {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b"}, - {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52"}, - {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29"}, - {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3"}, - {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143"}, - {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608"}, - {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa"}, - {file = "frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf"}, - {file = "frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746"}, - {file = "frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd"}, - {file = "frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a"}, - {file = "frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7"}, - {file = "frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40"}, - {file = "frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027"}, - {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822"}, - {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121"}, - {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5"}, - {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e"}, - {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11"}, - {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1"}, - {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1"}, - {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8"}, - {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed"}, - {file = "frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496"}, - {file = "frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231"}, - {file = "frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62"}, - {file = "frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94"}, - {file = "frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c"}, - {file = "frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52"}, - {file = "frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51"}, - {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65"}, - {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82"}, - {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714"}, - {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d"}, - {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506"}, - {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51"}, - {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e"}, - {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0"}, - {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41"}, - {file = "frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b"}, - {file = "frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888"}, - {file = "frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042"}, - {file = "frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0"}, - {file = "frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f"}, - {file = "frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c"}, - {file = "frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2"}, - {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8"}, - {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686"}, - {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e"}, - {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a"}, - {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128"}, - {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f"}, - {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7"}, - {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30"}, - {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7"}, - {file = "frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806"}, - {file = "frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0"}, - {file = "frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b"}, - {file = "frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d"}, - {file = "frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed"}, - {file = "frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930"}, - {file = "frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c"}, - {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24"}, - {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37"}, - {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a"}, - {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2"}, - {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef"}, - {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe"}, - {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8"}, - {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a"}, - {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e"}, - {file = "frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df"}, - {file = "frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd"}, - {file = "frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79"}, - {file = "frozenlist-1.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8b7138e5cd0647e4523d6685b0eac5d4be9a184ae9634492f25c6eb38c12a47"}, - {file = "frozenlist-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a6483e309ca809f1efd154b4d37dc6d9f61037d6c6a81c2dc7a15cb22c8c5dca"}, - {file = "frozenlist-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1b9290cf81e95e93fdf90548ce9d3c1211cf574b8e3f4b3b7cb0537cf2227068"}, - {file = "frozenlist-1.8.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59a6a5876ca59d1b63af8cd5e7ffffb024c3dc1e9cf9301b21a2e76286505c95"}, - {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6dc4126390929823e2d2d9dc79ab4046ed74680360fc5f38b585c12c66cdf459"}, - {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:332db6b2563333c5671fecacd085141b5800cb866be16d5e3eb15a2086476675"}, - {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ff15928d62a0b80bb875655c39bf517938c7d589554cbd2669be42d97c2cb61"}, - {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7bf6cdf8e07c8151fba6fe85735441240ec7f619f935a5205953d58009aef8c6"}, - {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:48e6d3f4ec5c7273dfe83ff27c91083c6c9065af655dc2684d2c200c94308bb5"}, - {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:1a7607e17ad33361677adcd1443edf6f5da0ce5e5377b798fba20fae194825f3"}, - {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3a935c3a4e89c733303a2d5a7c257ea44af3a56c8202df486b7f5de40f37e1"}, - {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:940d4a017dbfed9daf46a3b086e1d2167e7012ee297fef9e1c545c4d022f5178"}, - {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b9be22a69a014bc47e78072d0ecae716f5eb56c15238acca0f43d6eb8e4a5bda"}, - {file = "frozenlist-1.8.0-cp39-cp39-win32.whl", hash = "sha256:1aa77cb5697069af47472e39612976ed05343ff2e84a3dcf15437b232cbfd087"}, - {file = "frozenlist-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:7398c222d1d405e796970320036b1b563892b65809d9e5261487bb2c7f7b5c6a"}, - {file = "frozenlist-1.8.0-cp39-cp39-win_arm64.whl", hash = "sha256:b4f3b365f31c6cd4af24545ca0a244a53688cad8834e32f56831c4923b50a103"}, - {file = "frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d"}, - {file = "frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad"}, -] - -[[package]] -name = "gevent" -version = "25.9.1" -description = "Coroutine-based network library" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "gevent-25.9.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:856b990be5590e44c3a3dc6c8d48a40eaccbb42e99d2b791d11d1e7711a4297e"}, - {file = "gevent-25.9.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fe1599d0b30e6093eb3213551751b24feeb43db79f07e89d98dd2f3330c9063e"}, - {file = "gevent-25.9.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:f0d8b64057b4bf1529b9ef9bd2259495747fba93d1f836c77bfeaacfec373fd0"}, - {file = "gevent-25.9.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b56cbc820e3136ba52cd690bdf77e47a4c239964d5f80dc657c1068e0fe9521c"}, - {file = "gevent-25.9.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c5fa9ce5122c085983e33e0dc058f81f5264cebe746de5c401654ab96dddfca8"}, - {file = "gevent-25.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:03c74fec58eda4b4edc043311fca8ba4f8744ad1632eb0a41d5ec25413581975"}, - {file = "gevent-25.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:a8ae9f895e8651d10b0a8328a61c9c53da11ea51b666388aa99b0ce90f9fdc27"}, - {file = "gevent-25.9.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5aff9e8342dc954adb9c9c524db56c2f3557999463445ba3d9cbe3dada7b7"}, - {file = "gevent-25.9.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1cdf6db28f050ee103441caa8b0448ace545364f775059d5e2de089da975c457"}, - {file = "gevent-25.9.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:812debe235a8295be3b2a63b136c2474241fa5c58af55e6a0f8cfc29d4936235"}, - {file = "gevent-25.9.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b28b61ff9216a3d73fe8f35669eefcafa957f143ac534faf77e8a19eb9e6883a"}, - {file = "gevent-25.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5e4b6278b37373306fc6b1e5f0f1cf56339a1377f67c35972775143d8d7776ff"}, - {file = "gevent-25.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d99f0cb2ce43c2e8305bf75bee61a8bde06619d21b9d0316ea190fc7a0620a56"}, - {file = "gevent-25.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:72152517ecf548e2f838c61b4be76637d99279dbaa7e01b3924df040aa996586"}, - {file = "gevent-25.9.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:46b188248c84ffdec18a686fcac5dbb32365d76912e14fda350db5dc0bfd4f86"}, - {file = "gevent-25.9.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f2b54ea3ca6f0c763281cd3f96010ac7e98c2e267feb1221b5a26e2ca0b9a692"}, - {file = "gevent-25.9.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7a834804ac00ed8a92a69d3826342c677be651b1c3cd66cc35df8bc711057aa2"}, - {file = "gevent-25.9.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:323a27192ec4da6b22a9e51c3d9d896ff20bc53fdc9e45e56eaab76d1c39dd74"}, - {file = "gevent-25.9.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6ea78b39a2c51d47ff0f130f4c755a9a4bbb2dd9721149420ad4712743911a51"}, - {file = "gevent-25.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:dc45cd3e1cc07514a419960af932a62eb8515552ed004e56755e4bf20bad30c5"}, - {file = "gevent-25.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34e01e50c71eaf67e92c186ee0196a039d6e4f4b35670396baed4a2d8f1b347f"}, - {file = "gevent-25.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:4acd6bcd5feabf22c7c5174bd3b9535ee9f088d2bbce789f740ad8d6554b18f3"}, - {file = "gevent-25.9.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:4f84591d13845ee31c13f44bdf6bd6c3dbf385b5af98b2f25ec328213775f2ed"}, - {file = "gevent-25.9.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9cdbb24c276a2d0110ad5c978e49daf620b153719ac8a548ce1250a7eb1b9245"}, - {file = "gevent-25.9.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:88b6c07169468af631dcf0fdd3658f9246d6822cc51461d43f7c44f28b0abb82"}, - {file = "gevent-25.9.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b7bb0e29a7b3e6ca9bed2394aa820244069982c36dc30b70eb1004dd67851a48"}, - {file = "gevent-25.9.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2951bb070c0ee37b632ac9134e4fdaad70d2e660c931bb792983a0837fe5b7d7"}, - {file = "gevent-25.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e4e17c2d57e9a42e25f2a73d297b22b60b2470a74be5a515b36c984e1a246d47"}, - {file = "gevent-25.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d94936f8f8b23d9de2251798fcb603b84f083fdf0d7f427183c1828fb64f117"}, - {file = "gevent-25.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:eb51c5f9537b07da673258b4832f6635014fee31690c3f0944d34741b69f92fa"}, - {file = "gevent-25.9.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:1a3fe4ea1c312dbf6b375b416925036fe79a40054e6bf6248ee46526ea628be1"}, - {file = "gevent-25.9.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0adb937f13e5fb90cca2edf66d8d7e99d62a299687400ce2edee3f3504009356"}, - {file = "gevent-25.9.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:427f869a2050a4202d93cf7fd6ab5cffb06d3e9113c10c967b6e2a0d45237cb8"}, - {file = "gevent-25.9.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c049880175e8c93124188f9d926af0a62826a3b81aa6d3074928345f8238279e"}, - {file = "gevent-25.9.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b5a67a0974ad9f24721034d1e008856111e0535f1541499f72a733a73d658d1c"}, - {file = "gevent-25.9.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1d0f5d8d73f97e24ea8d24d8be0f51e0cf7c54b8021c1fddb580bf239474690f"}, - {file = "gevent-25.9.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ddd3ff26e5c4240d3fbf5516c2d9d5f2a998ef87cfb73e1429cfaeaaec860fa6"}, - {file = "gevent-25.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:bb63c0d6cb9950cc94036a4995b9cc4667b8915366613449236970f4394f94d7"}, - {file = "gevent-25.9.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f18f80aef6b1f6907219affe15b36677904f7cfeed1f6a6bc198616e507ae2d7"}, - {file = "gevent-25.9.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b274a53e818124a281540ebb4e7a2c524778f745b7a99b01bdecf0ca3ac0ddb0"}, - {file = "gevent-25.9.1-cp39-cp39-win32.whl", hash = "sha256:c6c91f7e33c7f01237755884316110ee7ea076f5bdb9aa0982b6dc63243c0a38"}, - {file = "gevent-25.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:012a44b0121f3d7c800740ff80351c897e85e76a7e4764690f35c5ad9ec17de5"}, - {file = "gevent-25.9.1.tar.gz", hash = "sha256:adf9cd552de44a4e6754c51ff2e78d9193b7fa6eab123db9578a210e657235dd"}, -] - -[package.dependencies] -cffi = {version = ">=1.17.1", markers = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""} -greenlet = {version = ">=3.2.2", markers = "platform_python_implementation == \"CPython\""} -"zope.event" = "*" -"zope.interface" = "*" - -[package.extras] -dnspython = ["dnspython (>=1.16.0,<2.0) ; python_version < \"3.10\"", "idna ; python_version < \"3.10\""] -docs = ["furo", "repoze.sphinx.autointerface", "sphinx", "sphinxcontrib-programoutput", "zope.schema"] -monitor = ["psutil (>=5.7.0) ; sys_platform != \"win32\" or platform_python_implementation == \"CPython\""] -recommended = ["cffi (>=1.17.1) ; platform_python_implementation == \"CPython\"", "dnspython (>=1.16.0,<2.0) ; python_version < \"3.10\"", "idna ; python_version < \"3.10\"", "psutil (>=5.7.0) ; sys_platform != \"win32\" or platform_python_implementation == \"CPython\""] -test = ["cffi (>=1.17.1) ; platform_python_implementation == \"CPython\"", "coverage (>=5.0) ; sys_platform != \"win32\"", "dnspython (>=1.16.0,<2.0) ; python_version < \"3.10\"", "idna ; python_version < \"3.10\"", "objgraph", "psutil (>=5.7.0) ; sys_platform != \"win32\" or platform_python_implementation == \"CPython\"", "requests"] - -[[package]] -name = "google-api-core" -version = "2.29.0" -description = "Google API client core library" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "google_api_core-2.29.0-py3-none-any.whl", hash = "sha256:d30bc60980daa36e314b5d5a3e5958b0200cb44ca8fa1be2b614e932b75a3ea9"}, - {file = "google_api_core-2.29.0.tar.gz", hash = "sha256:84181be0f8e6b04006df75ddfe728f24489f0af57c96a529ff7cf45bc28797f7"}, -] - -[package.dependencies] -google-auth = ">=2.14.1,<3.0.0" -googleapis-common-protos = ">=1.56.2,<2.0.0" -grpcio = {version = ">=1.49.1,<2.0.0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""} -grpcio-status = {version = ">=1.49.1,<2.0.0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""} -proto-plus = ">=1.22.3,<2.0.0" -protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" -requests = ">=2.18.0,<3.0.0" - -[package.extras] -async-rest = ["google-auth[aiohttp] (>=2.35.0,<3.0.0)"] -grpc = ["grpcio (>=1.33.2,<2.0.0)", "grpcio (>=1.49.1,<2.0.0) ; python_version >= \"3.11\"", "grpcio (>=1.75.1,<2.0.0) ; python_version >= \"3.14\"", "grpcio-status (>=1.33.2,<2.0.0)", "grpcio-status (>=1.49.1,<2.0.0) ; python_version >= \"3.11\"", "grpcio-status (>=1.75.1,<2.0.0) ; python_version >= \"3.14\""] -grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.0)"] -grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.0)"] - -[[package]] -name = "google-api-python-client" -version = "2.163.0" -description = "Google API Client Library for Python" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "google_api_python_client-2.163.0-py2.py3-none-any.whl", hash = "sha256:080e8bc0669cb4c1fb8efb8da2f5b91a2625d8f0e7796cfad978f33f7016c6c4"}, - {file = "google_api_python_client-2.163.0.tar.gz", hash = "sha256:88dee87553a2d82176e2224648bf89272d536c8f04dcdda37ef0a71473886dd7"}, -] - -[package.dependencies] -google-api-core = ">=1.31.5,<2.0.dev0 || >2.3.0,<3.0.0.dev0" -google-auth = ">=1.32.0,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0.dev0" -google-auth-httplib2 = ">=0.2.0,<1.0.0" -httplib2 = ">=0.19.0,<1.dev0" -uritemplate = ">=3.0.1,<5" - -[[package]] -name = "google-auth" -version = "2.48.0" -description = "Google Authentication Library" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "google_auth-2.48.0-py3-none-any.whl", hash = "sha256:2e2a537873d449434252a9632c28bfc268b0adb1e53f9fb62afc5333a975903f"}, - {file = "google_auth-2.48.0.tar.gz", hash = "sha256:4f7e706b0cd3208a3d940a19a822c37a476ddba5450156c3e6624a71f7c841ce"}, -] - -[package.dependencies] -cryptography = ">=38.0.3" -pyasn1-modules = ">=0.2.1" -rsa = ">=3.1.4,<5" - -[package.extras] -aiohttp = ["aiohttp (>=3.6.2,<4.0.0)", "requests (>=2.20.0,<3.0.0)"] -cryptography = ["cryptography (>=38.0.3)"] -enterprise-cert = ["pyopenssl"] -pyjwt = ["pyjwt (>=2.0)"] -pyopenssl = ["pyopenssl (>=20.0.0)"] -reauth = ["pyu2f (>=0.1.5)"] -requests = ["requests (>=2.20.0,<3.0.0)"] -testing = ["aiohttp (<3.10.0)", "aiohttp (>=3.6.2,<4.0.0)", "aioresponses", "flask", "freezegun", "grpcio", "oauth2client", "packaging", "pyjwt (>=2.0)", "pyopenssl (<24.3.0)", "pyopenssl (>=20.0.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-localserver", "pyu2f (>=0.1.5)", "requests (>=2.20.0,<3.0.0)", "responses", "urllib3"] -urllib3 = ["packaging", "urllib3"] - -[[package]] -name = "google-auth-httplib2" -version = "0.2.0" -description = "Google Authentication Library: httplib2 transport" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "google-auth-httplib2-0.2.0.tar.gz", hash = "sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05"}, - {file = "google_auth_httplib2-0.2.0-py2.py3-none-any.whl", hash = "sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d"}, -] - -[package.dependencies] -google-auth = "*" -httplib2 = ">=0.19.0" - -[[package]] -name = "google-cloud-access-context-manager" -version = "0.3.0" -description = "Google Cloud Access Context Manager Protobufs" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "google_cloud_access_context_manager-0.3.0-py3-none-any.whl", hash = "sha256:5d15ad51547f06c281e35f16b4ffcb3e98bb2d898b01470f88b94edfb2eeb0a3"}, - {file = "google_cloud_access_context_manager-0.3.0.tar.gz", hash = "sha256:f3aa35c9225b7aaef85ecdacedcc1577789be8d458b7a41b6ad23b504786e5f9"}, -] - -[package.dependencies] -google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0", extras = ["grpc"]} -protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" - -[[package]] -name = "google-cloud-asset" -version = "4.2.0" -description = "Google Cloud Asset API client library" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "google_cloud_asset-4.2.0-py3-none-any.whl", hash = "sha256:fd7ea04c64948a4779790343204cd5b41d4772d6ab1d05a9125e28a637ac0862"}, - {file = "google_cloud_asset-4.2.0.tar.gz", hash = "sha256:1734906cfd9b6ea6922861c8f1b4fcabe90d53ca267ee88499e8532b7593b35f"}, -] - -[package.dependencies] -google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0", extras = ["grpc"]} -google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0" -google-cloud-access-context-manager = ">=0.1.2,<1.0.0" -google-cloud-org-policy = ">=0.1.2,<2.0.0" -google-cloud-os-config = ">=1.0.0,<2.0.0" -grpc-google-iam-v1 = ">=0.14.0,<1.0.0" -grpcio = ">=1.33.2,<2.0.0" -proto-plus = ">=1.22.3,<2.0.0" -protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" - -[[package]] -name = "google-cloud-org-policy" -version = "1.16.0" -description = "Google Cloud Org Policy API client library" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "google_cloud_org_policy-1.16.0-py3-none-any.whl", hash = "sha256:96d1ed38f795182600a58f8eb2879e1577ce663b6b27df0b8a3050960cff87a5"}, - {file = "google_cloud_org_policy-1.16.0.tar.gz", hash = "sha256:c72147127d88d9809af8738b2abe34806eac529c3cdc57aa915cc08a1b842a13"}, -] - -[package.dependencies] -google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0", extras = ["grpc"]} -google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0" -grpcio = ">=1.33.2,<2.0.0" -proto-plus = ">=1.22.3,<2.0.0" -protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" - -[[package]] -name = "google-cloud-os-config" -version = "1.23.0" -description = "Google Cloud Os Config API client library" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "google_cloud_os_config-1.23.0-py3-none-any.whl", hash = "sha256:fea865018391abca42a9a74d270ddab516ae0865d6e9ad3bcb503286ca01c069"}, - {file = "google_cloud_os_config-1.23.0.tar.gz", hash = "sha256:a629cf55b3ede36b2df89814c6ccf3c1d43c7f1b43db6c7c02eb4860851baf3a"}, -] - -[package.dependencies] -google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0", extras = ["grpc"]} -google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0" -grpcio = ">=1.33.2,<2.0.0" -proto-plus = ">=1.22.3,<2.0.0" -protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" - -[[package]] -name = "google-cloud-resource-manager" -version = "1.16.0" -description = "Google Cloud Resource Manager API client library" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "google_cloud_resource_manager-1.16.0-py3-none-any.whl", hash = "sha256:fb9a2ad2b5053c508e1c407ac31abfd1a22e91c32876c1892830724195819a28"}, - {file = "google_cloud_resource_manager-1.16.0.tar.gz", hash = "sha256:cc938f87cc36c2672f062b1e541650629e0d954c405a4dac35ceedee70c267c3"}, -] - -[package.dependencies] -google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0", extras = ["grpc"]} -google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0" -grpc-google-iam-v1 = ">=0.14.0,<1.0.0" -grpcio = ">=1.33.2,<2.0.0" -proto-plus = ">=1.22.3,<2.0.0" -protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" - -[[package]] -name = "googleapis-common-protos" -version = "1.72.0" -description = "Common protobufs used in Google APIs" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038"}, - {file = "googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5"}, -] - -[package.dependencies] -grpcio = {version = ">=1.44.0,<2.0.0", optional = true, markers = "extra == \"grpc\""} -protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" - -[package.extras] -grpc = ["grpcio (>=1.44.0,<2.0.0)"] - -[[package]] -name = "gprof2dot" -version = "2025.4.14" -description = "Generate a dot graph from the output of several profilers." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "gprof2dot-2025.4.14-py3-none-any.whl", hash = "sha256:0742e4c0b4409a5e8777e739388a11e1ed3750be86895655312ea7c20bd0090e"}, - {file = "gprof2dot-2025.4.14.tar.gz", hash = "sha256:35743e2d2ca027bf48fa7cba37021aaf4a27beeae1ae8e05a50b55f1f921a6ce"}, -] - -[[package]] -name = "graphemeu" -version = "0.7.2" -description = "Unicode grapheme helpers" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "graphemeu-0.7.2-py3-none-any.whl", hash = "sha256:1444520f6899fd30114fc2a39f297d86d10fa0f23bf7579f772f8bc7efaa2542"}, - {file = "graphemeu-0.7.2.tar.gz", hash = "sha256:42bbe373d7c146160f286cd5f76b1a8ad29172d7333ce10705c5cc282462a4f8"}, -] - -[package.extras] -dev = ["pytest"] -docs = ["sphinx", "sphinx-autobuild"] - -[[package]] -name = "greenlet" -version = "3.3.1" -description = "Lightweight in-process concurrent programming" -optional = false -python-versions = ">=3.10" -groups = ["main"] -markers = "platform_python_implementation == \"CPython\"" -files = [ - {file = "greenlet-3.3.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:04bee4775f40ecefcdaa9d115ab44736cd4b9c5fba733575bfe9379419582e13"}, - {file = "greenlet-3.3.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e1457f4fed12a50e427988a07f0f9df53cf0ee8da23fab16e6732c2ec909d4"}, - {file = "greenlet-3.3.1-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:070472cd156f0656f86f92e954591644e158fd65aa415ffbe2d44ca77656a8f5"}, - {file = "greenlet-3.3.1-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1108b61b06b5224656121c3c8ee8876161c491cbe74e5c519e0634c837cf93d5"}, - {file = "greenlet-3.3.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a300354f27dd86bae5fbf7002e6dd2b3255cd372e9242c933faf5e859b703fe"}, - {file = "greenlet-3.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e84b51cbebf9ae573b5fbd15df88887815e3253fc000a7d0ff95170e8f7e9729"}, - {file = "greenlet-3.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0093bd1a06d899892427217f0ff2a3c8f306182b8c754336d32e2d587c131b4"}, - {file = "greenlet-3.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:7932f5f57609b6a3b82cc11877709aa7a98e3308983ed93552a1c377069b20c8"}, - {file = "greenlet-3.3.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5fd23b9bc6d37b563211c6abbb1b3cab27db385a4449af5c32e932f93017080c"}, - {file = "greenlet-3.3.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f51496a0bfbaa9d74d36a52d2580d1ef5ed4fdfcff0a73730abfbbbe1403dd"}, - {file = "greenlet-3.3.1-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb0feb07fe6e6a74615ee62a880007d976cf739b6669cce95daa7373d4fc69c5"}, - {file = "greenlet-3.3.1-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:67ea3fc73c8cd92f42467a72b75e8f05ed51a0e9b1d15398c913416f2dafd49f"}, - {file = "greenlet-3.3.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39eda9ba259cc9801da05351eaa8576e9aa83eb9411e8f0c299e05d712a210f2"}, - {file = "greenlet-3.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e2e7e882f83149f0a71ac822ebf156d902e7a5d22c9045e3e0d1daf59cee2cc9"}, - {file = "greenlet-3.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80aa4d79eb5564f2e0a6144fcc744b5a37c56c4a92d60920720e99210d88db0f"}, - {file = "greenlet-3.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:32e4ca9777c5addcbf42ff3915d99030d8e00173a56f80001fb3875998fe410b"}, - {file = "greenlet-3.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:da19609432f353fed186cc1b85e9440db93d489f198b4bdf42ae19cc9d9ac9b4"}, - {file = "greenlet-3.3.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7e806ca53acf6d15a888405880766ec84721aa4181261cd11a457dfe9a7a4975"}, - {file = "greenlet-3.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d842c94b9155f1c9b3058036c24ffb8ff78b428414a19792b2380be9cecf4f36"}, - {file = "greenlet-3.3.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20fedaadd422fa02695f82093f9a98bad3dab5fcda793c658b945fcde2ab27ba"}, - {file = "greenlet-3.3.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c620051669fd04ac6b60ebc70478210119c56e2d5d5df848baec4312e260e4ca"}, - {file = "greenlet-3.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14194f5f4305800ff329cbf02c5fcc88f01886cadd29941b807668a45f0d2336"}, - {file = "greenlet-3.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7b2fe4150a0cf59f847a67db8c155ac36aed89080a6a639e9f16df5d6c6096f1"}, - {file = "greenlet-3.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49f4ad195d45f4a66a0eb9c1ba4832bb380570d361912fa3554746830d332149"}, - {file = "greenlet-3.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:cc98b9c4e4870fa983436afa999d4eb16b12872fab7071423d5262fa7120d57a"}, - {file = "greenlet-3.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:bfb2d1763d777de5ee495c85309460f6fd8146e50ec9d0ae0183dbf6f0a829d1"}, - {file = "greenlet-3.3.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:7ab327905cabb0622adca5971e488064e35115430cec2c35a50fd36e72a315b3"}, - {file = "greenlet-3.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65be2f026ca6a176f88fb935ee23c18333ccea97048076aef4db1ef5bc0713ac"}, - {file = "greenlet-3.3.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a3ae05b3d225b4155bda56b072ceb09d05e974bc74be6c3fc15463cf69f33fd"}, - {file = "greenlet-3.3.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:12184c61e5d64268a160226fb4818af4df02cfead8379d7f8b99a56c3a54ff3e"}, - {file = "greenlet-3.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6423481193bbbe871313de5fd06a082f2649e7ce6e08015d2a76c1e9186ca5b3"}, - {file = "greenlet-3.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:33a956fe78bbbda82bfc95e128d61129b32d66bcf0a20a1f0c08aa4839ffa951"}, - {file = "greenlet-3.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b065d3284be43728dd280f6f9a13990b56470b81be20375a207cdc814a983f2"}, - {file = "greenlet-3.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:27289986f4e5b0edec7b5a91063c109f0276abb09a7e9bdab08437525977c946"}, - {file = "greenlet-3.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:2f080e028001c5273e0b42690eaf359aeef9cb1389da0f171ea51a5dc3c7608d"}, - {file = "greenlet-3.3.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:bd59acd8529b372775cd0fcbc5f420ae20681c5b045ce25bd453ed8455ab99b5"}, - {file = "greenlet-3.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b31c05dd84ef6871dd47120386aed35323c944d86c3d91a17c4b8d23df62f15b"}, - {file = "greenlet-3.3.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02925a0bfffc41e542c70aa14c7eda3593e4d7e274bfcccca1827e6c0875902e"}, - {file = "greenlet-3.3.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3e0f3878ca3a3ff63ab4ea478585942b53df66ddde327b59ecb191b19dbbd62d"}, - {file = "greenlet-3.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34a729e2e4e4ffe9ae2408d5ecaf12f944853f40ad724929b7585bca808a9d6f"}, - {file = "greenlet-3.3.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aec9ab04e82918e623415947921dea15851b152b822661cce3f8e4393c3df683"}, - {file = "greenlet-3.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71c767cf281a80d02b6c1bdc41c9468e1f5a494fb11bc8688c360524e273d7b1"}, - {file = "greenlet-3.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:96aff77af063b607f2489473484e39a0bbae730f2ea90c9e5606c9b73c44174a"}, - {file = "greenlet-3.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:b066e8b50e28b503f604fa538adc764a638b38cf8e81e025011d26e8a627fa79"}, - {file = "greenlet-3.3.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:3e63252943c921b90abb035ebe9de832c436401d9c45f262d80e2d06cc659242"}, - {file = "greenlet-3.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76e39058e68eb125de10c92524573924e827927df5d3891fbc97bd55764a8774"}, - {file = "greenlet-3.3.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9f9d5e7a9310b7a2f416dd13d2e3fd8b42d803968ea580b7c0f322ccb389b97"}, - {file = "greenlet-3.3.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b9721549a95db96689458a1e0ae32412ca18776ed004463df3a9299c1b257ab"}, - {file = "greenlet-3.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92497c78adf3ac703b57f1e3813c2d874f27f71a178f9ea5887855da413cd6d2"}, - {file = "greenlet-3.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ed6b402bc74d6557a705e197d47f9063733091ed6357b3de33619d8a8d93ac53"}, - {file = "greenlet-3.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:59913f1e5ada20fde795ba906916aea25d442abcc0593fba7e26c92b7ad76249"}, - {file = "greenlet-3.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:301860987846c24cb8964bdec0e31a96ad4a2a801b41b4ef40963c1b44f33451"}, - {file = "greenlet-3.3.1.tar.gz", hash = "sha256:41848f3230b58c08bb43dee542e74a2a2e34d3c59dc3076cec9151aeeedcae98"}, -] - -[package.extras] -docs = ["Sphinx", "furo"] -test = ["objgraph", "psutil", "setuptools"] - -[[package]] -name = "grpc-google-iam-v1" -version = "0.14.3" -description = "IAM API client library" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "grpc_google_iam_v1-0.14.3-py3-none-any.whl", hash = "sha256:7a7f697e017a067206a3dfef44e4c634a34d3dee135fe7d7a4613fe3e59217e6"}, - {file = "grpc_google_iam_v1-0.14.3.tar.gz", hash = "sha256:879ac4ef33136c5491a6300e27575a9ec760f6cdf9a2518798c1b8977a5dc389"}, -] - -[package.dependencies] -googleapis-common-protos = {version = ">=1.56.0,<2.0.0", extras = ["grpc"]} -grpcio = ">=1.44.0,<2.0.0" -protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" - -[[package]] -name = "grpcio" -version = "1.76.0" -description = "HTTP/2-based RPC framework" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "grpcio-1.76.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:65a20de41e85648e00305c1bb09a3598f840422e522277641145a32d42dcefcc"}, - {file = "grpcio-1.76.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:40ad3afe81676fd9ec6d9d406eda00933f218038433980aa19d401490e46ecde"}, - {file = "grpcio-1.76.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:035d90bc79eaa4bed83f524331d55e35820725c9fbb00ffa1904d5550ed7ede3"}, - {file = "grpcio-1.76.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4215d3a102bd95e2e11b5395c78562967959824156af11fa93d18fdd18050990"}, - {file = "grpcio-1.76.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:49ce47231818806067aea3324d4bf13825b658ad662d3b25fada0bdad9b8a6af"}, - {file = "grpcio-1.76.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8cc3309d8e08fd79089e13ed4819d0af72aa935dd8f435a195fd152796752ff2"}, - {file = "grpcio-1.76.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:971fd5a1d6e62e00d945423a567e42eb1fa678ba89072832185ca836a94daaa6"}, - {file = "grpcio-1.76.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d9adda641db7207e800a7f089068f6f645959f2df27e870ee81d44701dd9db3"}, - {file = "grpcio-1.76.0-cp310-cp310-win32.whl", hash = "sha256:063065249d9e7e0782d03d2bca50787f53bd0fb89a67de9a7b521c4a01f1989b"}, - {file = "grpcio-1.76.0-cp310-cp310-win_amd64.whl", hash = "sha256:a6ae758eb08088d36812dd5d9af7a9859c05b1e0f714470ea243694b49278e7b"}, - {file = "grpcio-1.76.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2e1743fbd7f5fa713a1b0a8ac8ebabf0ec980b5d8809ec358d488e273b9cf02a"}, - {file = "grpcio-1.76.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:a8c2cf1209497cf659a667d7dea88985e834c24b7c3b605e6254cbb5076d985c"}, - {file = "grpcio-1.76.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:08caea849a9d3c71a542827d6df9d5a69067b0a1efbea8a855633ff5d9571465"}, - {file = "grpcio-1.76.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f0e34c2079d47ae9f6188211db9e777c619a21d4faba6977774e8fa43b085e48"}, - {file = "grpcio-1.76.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8843114c0cfce61b40ad48df65abcfc00d4dba82eae8718fab5352390848c5da"}, - {file = "grpcio-1.76.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8eddfb4d203a237da6f3cc8a540dad0517d274b5a1e9e636fd8d2c79b5c1d397"}, - {file = "grpcio-1.76.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:32483fe2aab2c3794101c2a159070584e5db11d0aa091b2c0ea9c4fc43d0d749"}, - {file = "grpcio-1.76.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dcfe41187da8992c5f40aa8c5ec086fa3672834d2be57a32384c08d5a05b4c00"}, - {file = "grpcio-1.76.0-cp311-cp311-win32.whl", hash = "sha256:2107b0c024d1b35f4083f11245c0e23846ae64d02f40b2b226684840260ed054"}, - {file = "grpcio-1.76.0-cp311-cp311-win_amd64.whl", hash = "sha256:522175aba7af9113c48ec10cc471b9b9bd4f6ceb36aeb4544a8e2c80ed9d252d"}, - {file = "grpcio-1.76.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8"}, - {file = "grpcio-1.76.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280"}, - {file = "grpcio-1.76.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4"}, - {file = "grpcio-1.76.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9f8f757bebaaea112c00dba718fc0d3260052ce714e25804a03f93f5d1c6cc11"}, - {file = "grpcio-1.76.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6"}, - {file = "grpcio-1.76.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f92f88e6c033db65a5ae3d97905c8fea9c725b63e28d5a75cb73b49bda5024d8"}, - {file = "grpcio-1.76.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4baf3cbe2f0be3289eb68ac8ae771156971848bb8aaff60bad42005539431980"}, - {file = "grpcio-1.76.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882"}, - {file = "grpcio-1.76.0-cp312-cp312-win32.whl", hash = "sha256:45d59a649a82df5718fd9527ce775fd66d1af35e6d31abdcdc906a49c6822958"}, - {file = "grpcio-1.76.0-cp312-cp312-win_amd64.whl", hash = "sha256:c088e7a90b6017307f423efbb9d1ba97a22aa2170876223f9709e9d1de0b5347"}, - {file = "grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2"}, - {file = "grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468"}, - {file = "grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3"}, - {file = "grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb"}, - {file = "grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae"}, - {file = "grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77"}, - {file = "grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03"}, - {file = "grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42"}, - {file = "grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f"}, - {file = "grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8"}, - {file = "grpcio-1.76.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62"}, - {file = "grpcio-1.76.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd"}, - {file = "grpcio-1.76.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc"}, - {file = "grpcio-1.76.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a"}, - {file = "grpcio-1.76.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba"}, - {file = "grpcio-1.76.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09"}, - {file = "grpcio-1.76.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc"}, - {file = "grpcio-1.76.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc"}, - {file = "grpcio-1.76.0-cp314-cp314-win32.whl", hash = "sha256:747fa73efa9b8b1488a95d0ba1039c8e2dca0f741612d80415b1e1c560febf4e"}, - {file = "grpcio-1.76.0-cp314-cp314-win_amd64.whl", hash = "sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e"}, - {file = "grpcio-1.76.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:8ebe63ee5f8fa4296b1b8cfc743f870d10e902ca18afc65c68cf46fd39bb0783"}, - {file = "grpcio-1.76.0-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:3bf0f392c0b806905ed174dcd8bdd5e418a40d5567a05615a030a5aeddea692d"}, - {file = "grpcio-1.76.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0b7604868b38c1bfd5cf72d768aedd7db41d78cb6a4a18585e33fb0f9f2363fd"}, - {file = "grpcio-1.76.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e6d1db20594d9daba22f90da738b1a0441a7427552cc6e2e3d1297aeddc00378"}, - {file = "grpcio-1.76.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d099566accf23d21037f18a2a63d323075bebace807742e4b0ac210971d4dd70"}, - {file = "grpcio-1.76.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ebea5cc3aa8ea72e04df9913492f9a96d9348db876f9dda3ad729cfedf7ac416"}, - {file = "grpcio-1.76.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0c37db8606c258e2ee0c56b78c62fc9dee0e901b5dbdcf816c2dd4ad652b8b0c"}, - {file = "grpcio-1.76.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ebebf83299b0cb1721a8859ea98f3a77811e35dce7609c5c963b9ad90728f886"}, - {file = "grpcio-1.76.0-cp39-cp39-win32.whl", hash = "sha256:0aaa82d0813fd4c8e589fac9b65d7dd88702555f702fb10417f96e2a2a6d4c0f"}, - {file = "grpcio-1.76.0-cp39-cp39-win_amd64.whl", hash = "sha256:acab0277c40eff7143c2323190ea57b9ee5fd353d8190ee9652369fae735668a"}, - {file = "grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73"}, -] - -[package.dependencies] -typing-extensions = ">=4.12,<5.0" - -[package.extras] -protobuf = ["grpcio-tools (>=1.76.0)"] - -[[package]] -name = "grpcio-status" -version = "1.76.0" -description = "Status proto mapping for gRPC" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "grpcio_status-1.76.0-py3-none-any.whl", hash = "sha256:380568794055a8efbbd8871162df92012e0228a5f6dffaf57f2a00c534103b18"}, - {file = "grpcio_status-1.76.0.tar.gz", hash = "sha256:25fcbfec74c15d1a1cb5da3fab8ee9672852dc16a5a9eeb5baf7d7a9952943cd"}, -] - -[package.dependencies] -googleapis-common-protos = ">=1.5.5" -grpcio = ">=1.76.0" -protobuf = ">=6.31.1,<7.0.0" - -[[package]] -name = "gunicorn" -version = "23.0.0" -description = "WSGI HTTP Server for UNIX" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"}, - {file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"}, -] - -[package.dependencies] -packaging = "*" - -[package.extras] -eventlet = ["eventlet (>=0.24.1,!=0.36.0)"] -gevent = ["gevent (>=1.4.0)"] -setproctitle = ["setproctitle"] -testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"] -tornado = ["tornado (>=0.2)"] - -[[package]] -name = "h11" -version = "0.16.0" -description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, - {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, -] - -[[package]] -name = "h2" -version = "4.3.0" -description = "Pure-Python HTTP/2 protocol implementation" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd"}, - {file = "h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1"}, -] - -[package.dependencies] -hpack = ">=4.1,<5" -hyperframe = ">=6.1,<7" - -[[package]] -name = "hpack" -version = "4.1.0" -description = "Pure-Python HPACK header encoding" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496"}, - {file = "hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca"}, -] - -[[package]] -name = "httpcore" -version = "1.0.9" -description = "A minimal low-level HTTP client." -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, - {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, -] - -[package.dependencies] -certifi = "*" -h11 = ">=0.16" - -[package.extras] -asyncio = ["anyio (>=4.0,<5.0)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] -trio = ["trio (>=0.22.0,<1.0)"] - -[[package]] -name = "httplib2" -version = "0.31.2" -description = "A comprehensive HTTP client library." -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "httplib2-0.31.2-py3-none-any.whl", hash = "sha256:dbf0c2fa3862acf3c55c078ea9c0bc4481d7dc5117cae71be9514912cf9f8349"}, - {file = "httplib2-0.31.2.tar.gz", hash = "sha256:385e0869d7397484f4eab426197a4c020b606edd43372492337c0b4010ae5d24"}, -] - -[package.dependencies] -pyparsing = ">=3.1,<4" - -[[package]] -name = "httpx" -version = "0.28.1" -description = "The next generation HTTP client." -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, - {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, -] - -[package.dependencies] -anyio = "*" -certifi = "*" -h2 = {version = ">=3,<5", optional = true, markers = "extra == \"http2\""} -httpcore = "==1.*" -idna = "*" - -[package.extras] -brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] -cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "humanfriendly" -version = "10.0" -description = "Human friendly output for text interfaces using Python" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -groups = ["main"] -files = [ - {file = "humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477"}, - {file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"}, -] - -[package.dependencies] -pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_version >= \"3.8\""} - -[[package]] -name = "hyperframe" -version = "6.1.0" -description = "Pure-Python HTTP/2 framing" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5"}, - {file = "hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08"}, -] - -[[package]] -name = "iamdata" -version = "0.1.202602021" -description = "IAM data for AWS actions, resources, and conditions based on IAM policy documents. Checked for updates daily." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "iamdata-0.1.202602021-py3-none-any.whl", hash = "sha256:48419662d75dd0e1ea22b9cc98fd70201d4c72760c6897acc46ad9ab90633d18"}, - {file = "iamdata-0.1.202602021.tar.gz", hash = "sha256:c24265fc3694076f65da91a8aa9361b60da25f7b8cfd8ba4ddd6aa1b9bb5153e"}, -] - -[[package]] -name = "idna" -version = "3.11" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, - {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, -] - -[package.extras] -all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] - -[[package]] -name = "importlib-metadata" -version = "8.7.1" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151"}, - {file = "importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb"}, -] - -[package.dependencies] -zipp = ">=3.20" - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=3.4)"] -perf = ["ipython"] -test = ["flufl.flake8", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] -type = ["mypy (<1.19) ; platform_python_implementation == \"PyPy\"", "pytest-mypy (>=1.0.1)"] - -[[package]] -name = "inflection" -version = "0.5.1" -description = "A port of Ruby on Rails inflector to Python" -optional = false -python-versions = ">=3.5" -groups = ["main"] -files = [ - {file = "inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"}, - {file = "inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417"}, -] - -[[package]] -name = "iniconfig" -version = "2.3.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.10" -groups = ["main", "dev"] -files = [ - {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, - {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, -] - -[[package]] -name = "iso8601" -version = "2.1.0" -description = "Simple module to parse ISO 8601 dates" -optional = false -python-versions = ">=3.7,<4.0" -groups = ["main"] -files = [ - {file = "iso8601-2.1.0-py3-none-any.whl", hash = "sha256:aac4145c4dcb66ad8b648a02830f5e2ff6c24af20f4f482689be402db2429242"}, - {file = "iso8601-2.1.0.tar.gz", hash = "sha256:6b1d3829ee8921c4301998c909f7829fa9ed3cbdac0d3b16af2d743aed1ba8df"}, -] - -[[package]] -name = "isodate" -version = "0.7.2" -description = "An ISO 8601 date/time/duration parser and formatter" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15"}, - {file = "isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6"}, -] - -[[package]] -name = "isort" -version = "5.13.2" -description = "A Python utility / library to sort Python imports." -optional = false -python-versions = ">=3.8.0" -groups = ["dev"] -files = [ - {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, - {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, -] - -[package.extras] -colors = ["colorama (>=0.4.6)"] - -[[package]] -name = "itsdangerous" -version = "2.2.0" -description = "Safely pass data to untrusted environments and back." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, - {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, -] - -[[package]] -name = "jinja2" -version = "3.1.6" -description = "A very fast and expressive template engine." -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -files = [ - {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, - {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, -] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "jiter" -version = "0.13.0" -description = "Fast iterable JSON parser." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "jiter-0.13.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2ffc63785fd6c7977defe49b9824ae6ce2b2e2b77ce539bdaf006c26da06342e"}, - {file = "jiter-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4a638816427006c1e3f0013eb66d391d7a3acda99a7b0cf091eff4497ccea33a"}, - {file = "jiter-0.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19928b5d1ce0ff8c1ee1b9bdef3b5bfc19e8304f1b904e436caf30bc15dc6cf5"}, - {file = "jiter-0.13.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:309549b778b949d731a2f0e1594a3f805716be704a73bf3ad9a807eed5eb5721"}, - {file = "jiter-0.13.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcdabaea26cb04e25df3103ce47f97466627999260290349a88c8136ecae0060"}, - {file = "jiter-0.13.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a377af27b236abbf665a69b2bdd680e3b5a0bd2af825cd3b81245279a7606c"}, - {file = "jiter-0.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe49d3ff6db74321f144dff9addd4a5874d3105ac5ba7c5b77fac099cfae31ae"}, - {file = "jiter-0.13.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2113c17c9a67071b0f820733c0893ed1d467b5fcf4414068169e5c2cabddb1e2"}, - {file = "jiter-0.13.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ab1185ca5c8b9491b55ebf6c1e8866b8f68258612899693e24a92c5fdb9455d5"}, - {file = "jiter-0.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9621ca242547edc16400981ca3231e0c91c0c4c1ab8573a596cd9bb3575d5c2b"}, - {file = "jiter-0.13.0-cp310-cp310-win32.whl", hash = "sha256:a7637d92b1c9d7a771e8c56f445c7f84396d48f2e756e5978840ecba2fac0894"}, - {file = "jiter-0.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c1b609e5cbd2f52bb74fb721515745b407df26d7b800458bd97cb3b972c29e7d"}, - {file = "jiter-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ea026e70a9a28ebbdddcbcf0f1323128a8db66898a06eaad3a4e62d2f554d096"}, - {file = "jiter-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911"}, - {file = "jiter-0.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701"}, - {file = "jiter-0.13.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c"}, - {file = "jiter-0.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4"}, - {file = "jiter-0.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165"}, - {file = "jiter-0.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018"}, - {file = "jiter-0.13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9da38b4fedde4fb528c740c2564628fbab737166a0e73d6d46cb4bb5463ff411"}, - {file = "jiter-0.13.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5"}, - {file = "jiter-0.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3"}, - {file = "jiter-0.13.0-cp311-cp311-win32.whl", hash = "sha256:0f0c065695f616a27c920a56ad0d4fc46415ef8b806bf8fc1cacf25002bd24e1"}, - {file = "jiter-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:0733312953b909688ae3c2d58d043aa040f9f1a6a75693defed7bc2cc4bf2654"}, - {file = "jiter-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:5d9b34ad56761b3bf0fbe8f7e55468704107608512350962d3317ffd7a4382d5"}, - {file = "jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663"}, - {file = "jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505"}, - {file = "jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152"}, - {file = "jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726"}, - {file = "jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0"}, - {file = "jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089"}, - {file = "jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93"}, - {file = "jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08"}, - {file = "jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2"}, - {file = "jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228"}, - {file = "jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394"}, - {file = "jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92"}, - {file = "jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9"}, - {file = "jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf"}, - {file = "jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a"}, - {file = "jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb"}, - {file = "jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2"}, - {file = "jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f"}, - {file = "jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159"}, - {file = "jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663"}, - {file = "jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa"}, - {file = "jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820"}, - {file = "jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68"}, - {file = "jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72"}, - {file = "jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc"}, - {file = "jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b"}, - {file = "jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10"}, - {file = "jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef"}, - {file = "jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6"}, - {file = "jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d"}, - {file = "jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d"}, - {file = "jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0"}, - {file = "jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91"}, - {file = "jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09"}, - {file = "jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607"}, - {file = "jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66"}, - {file = "jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2"}, - {file = "jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad"}, - {file = "jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d"}, - {file = "jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df"}, - {file = "jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d"}, - {file = "jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6"}, - {file = "jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f"}, - {file = "jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d"}, - {file = "jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0"}, - {file = "jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40"}, - {file = "jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202"}, - {file = "jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0"}, - {file = "jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95"}, - {file = "jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59"}, - {file = "jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe"}, - {file = "jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939"}, - {file = "jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9"}, - {file = "jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6"}, - {file = "jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8"}, - {file = "jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024"}, - {file = "jiter-0.13.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:4397ee562b9f69d283e5674445551b47a5e8076fdde75e71bfac5891113dc543"}, - {file = "jiter-0.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f90023f8f672e13ea1819507d2d21b9d2d1c18920a3b3a5f1541955a85b5504"}, - {file = "jiter-0.13.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed0240dd1536a98c3ab55e929c60dfff7c899fecafcb7d01161b21a99fc8c363"}, - {file = "jiter-0.13.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6207fc61c395b26fffdcf637a0b06b4326f35bfa93c6e92fe1a166a21aeb6731"}, - {file = "jiter-0.13.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00203f47c214156df427b5989de74cb340c65c8180d09be1bf9de81d0abad599"}, - {file = "jiter-0.13.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c26ad6967c9dcedf10c995a21539c3aa57d4abad7001b7a84f621a263a6b605"}, - {file = "jiter-0.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a576f5dce9ac7de5d350b8e2f552cf364f32975ed84717c35379a51c7cb198bd"}, - {file = "jiter-0.13.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b22945be8425d161f2e536cdae66da300b6b000f1c0ba3ddf237d1bfd45d21b8"}, - {file = "jiter-0.13.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6eeb7db8bc77dc20476bc2f7407a23dbe3d46d9cc664b166e3d474e1c1de4baa"}, - {file = "jiter-0.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:19cd6f85e1dc090277c3ce90a5b7d96f32127681d825e71c9dce28788e39fc0c"}, - {file = "jiter-0.13.0-cp39-cp39-win32.whl", hash = "sha256:dc3ce84cfd4fa9628fe62c4f85d0d597a4627d4242cfafac32a12cc1455d00f7"}, - {file = "jiter-0.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:9ffda299e417dc83362963966c50cb76d42da673ee140de8a8ac762d4bb2378b"}, - {file = "jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b1cbfa133241d0e6bdab48dcdc2604e8ba81512f6bbd68ec3e8e1357dd3c316c"}, - {file = "jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2"}, - {file = "jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434"}, - {file = "jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d"}, - {file = "jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a"}, - {file = "jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f"}, - {file = "jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59"}, - {file = "jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19"}, - {file = "jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4"}, -] - -[[package]] -name = "jmespath" -version = "1.1.0" -description = "JSON Matching Expressions" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64"}, - {file = "jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d"}, -] - -[[package]] -name = "joblib" -version = "1.5.3" -description = "Lightweight pipelining with Python functions" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713"}, - {file = "joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3"}, -] - -[[package]] -name = "jsonpatch" -version = "1.33" -description = "Apply JSON-Patches (RFC 6902)" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" -groups = ["main"] -files = [ - {file = "jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade"}, - {file = "jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c"}, -] - -[package.dependencies] -jsonpointer = ">=1.9" - -[[package]] -name = "jsonpickle" -version = "4.1.1" -description = "jsonpickle encodes/decodes any Python object to/from JSON" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "jsonpickle-4.1.1-py3-none-any.whl", hash = "sha256:bb141da6057898aa2438ff268362b126826c812a1721e31cf08a6e142910dc91"}, - {file = "jsonpickle-4.1.1.tar.gz", hash = "sha256:f86e18f13e2b96c1c1eede0b7b90095bbb61d99fedc14813c44dc2f361dbbae1"}, -] - -[package.extras] -cov = ["pytest-cov"] -dev = ["black", "pyupgrade"] -docs = ["furo", "rst.linker (>=1.9)", "sphinx (>=3.5)"] -packaging = ["build", "setuptools (>=61.2)", "setuptools_scm[toml] (>=6.0)", "twine"] -testing = ["PyYAML", "atheris (>=2.3.0,<2.4.0) ; python_version < \"3.12\"", "bson", "ecdsa", "feedparser", "gmpy2", "numpy", "pandas", "pymongo", "pytest (>=6.0,!=8.1.*)", "pytest-benchmark", "pytest-benchmark[histogram]", "pytest-checkdocs (>=1.2.3)", "pytest-enabler (>=1.0.1)", "pytest-ruff (>=0.2.1)", "scikit-learn", "scipy (>=1.9.3) ; python_version > \"3.10\"", "scipy ; python_version <= \"3.10\"", "simplejson", "sqlalchemy", "ujson"] - -[[package]] -name = "jsonpointer" -version = "3.0.0" -description = "Identify specific nodes in a JSON document (RFC 6901)" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942"}, - {file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"}, -] - -[[package]] -name = "jsonschema" -version = "4.23.0" -description = "An implementation of JSON Schema validation for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566"}, - {file = "jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4"}, -] - -[package.dependencies] -attrs = ">=22.2.0" -jsonschema-specifications = ">=2023.3.6" -referencing = ">=0.28.4" -rpds-py = ">=0.7.1" - -[package.extras] -format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] -format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=24.6.0)"] - -[[package]] -name = "jsonschema-specifications" -version = "2025.9.1" -description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe"}, - {file = "jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d"}, -] - -[package.dependencies] -referencing = ">=0.31.0" - -[[package]] -name = "keystoneauth1" -version = "5.13.0" -description = "Authentication Library for OpenStack Identity" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "keystoneauth1-5.13.0-py3-none-any.whl", hash = "sha256:5ab81412eb0923ceb9c602cc3decce514b399523cb83d16b409ed3b0f9b03d41"}, - {file = "keystoneauth1-5.13.0.tar.gz", hash = "sha256:57c9ca407207899b50d8ff1ca8abb4a4e7427461bfc1877eb8519c3989ce63ec"}, -] - -[package.dependencies] -iso8601 = ">=2.0.0" -os-service-types = ">=1.2.0" -pbr = ">=2.0.0" -requests = ">=2.14.2" -stevedore = ">=1.20.0" -typing-extensions = ">=4.12" - -[package.extras] -betamax = ["PyYAML (>=3.13)", "betamax (>=0.7.0)", "fixtures (>=3.0.0)"] -kerberos = ["requests-kerberos (>=0.8.0)"] -oauth1 = ["oauthlib (>=0.6.2)"] -saml2 = ["lxml (>=4.2.0)"] - -[[package]] -name = "kiwisolver" -version = "1.4.9" -description = "A fast implementation of the Cassowary constraint solver" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "kiwisolver-1.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b4b4d74bda2b8ebf4da5bd42af11d02d04428b2c32846e4c2c93219df8a7987b"}, - {file = "kiwisolver-1.4.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fb3b8132019ea572f4611d770991000d7f58127560c4889729248eb5852a102f"}, - {file = "kiwisolver-1.4.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84fd60810829c27ae375114cd379da1fa65e6918e1da405f356a775d49a62bcf"}, - {file = "kiwisolver-1.4.9-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b78efa4c6e804ecdf727e580dbb9cba85624d2e1c6b5cb059c66290063bd99a9"}, - {file = "kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4efec7bcf21671db6a3294ff301d2fc861c31faa3c8740d1a94689234d1b415"}, - {file = "kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:90f47e70293fc3688b71271100a1a5453aa9944a81d27ff779c108372cf5567b"}, - {file = "kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fdca1def57a2e88ef339de1737a1449d6dbf5fab184c54a1fca01d541317154"}, - {file = "kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9cf554f21be770f5111a1690d42313e140355e687e05cf82cb23d0a721a64a48"}, - {file = "kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1795ac5cd0510207482c3d1d3ed781143383b8cfd36f5c645f3897ce066220"}, - {file = "kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ccd09f20ccdbbd341b21a67ab50a119b64a403b09288c27481575105283c1586"}, - {file = "kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:540c7c72324d864406a009d72f5d6856f49693db95d1fbb46cf86febef873634"}, - {file = "kiwisolver-1.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:ede8c6d533bc6601a47ad4046080d36b8fc99f81e6f1c17b0ac3c2dc91ac7611"}, - {file = "kiwisolver-1.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:7b4da0d01ac866a57dd61ac258c5607b4cd677f63abaec7b148354d2b2cdd536"}, - {file = "kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16"}, - {file = "kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089"}, - {file = "kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543"}, - {file = "kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61"}, - {file = "kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1"}, - {file = "kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872"}, - {file = "kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26"}, - {file = "kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028"}, - {file = "kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771"}, - {file = "kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a"}, - {file = "kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464"}, - {file = "kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2"}, - {file = "kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7"}, - {file = "kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999"}, - {file = "kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2"}, - {file = "kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14"}, - {file = "kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04"}, - {file = "kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752"}, - {file = "kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77"}, - {file = "kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198"}, - {file = "kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d"}, - {file = "kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab"}, - {file = "kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2"}, - {file = "kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145"}, - {file = "kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54"}, - {file = "kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60"}, - {file = "kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8"}, - {file = "kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2"}, - {file = "kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f"}, - {file = "kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098"}, - {file = "kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed"}, - {file = "kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525"}, - {file = "kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78"}, - {file = "kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b"}, - {file = "kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799"}, - {file = "kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3"}, - {file = "kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c"}, - {file = "kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d"}, - {file = "kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c"}, - {file = "kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386"}, - {file = "kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552"}, - {file = "kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3"}, - {file = "kiwisolver-1.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58"}, - {file = "kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4"}, - {file = "kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df"}, - {file = "kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6"}, - {file = "kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5"}, - {file = "kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf"}, - {file = "kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5"}, - {file = "kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce"}, - {file = "kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7"}, - {file = "kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32"}, - {file = "kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4d1d9e582ad4d63062d34077a9a1e9f3c34088a2ec5135b1f7190c07cf366527"}, - {file = "kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:deed0c7258ceb4c44ad5ec7d9918f9f14fd05b2be86378d86cf50e63d1e7b771"}, - {file = "kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a590506f303f512dff6b7f75fd2fd18e16943efee932008fe7140e5fa91d80e"}, - {file = "kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e09c2279a4d01f099f52d5c4b3d9e208e91edcbd1a175c9662a8b16e000fece9"}, - {file = "kiwisolver-1.4.9-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c9e7cdf45d594ee04d5be1b24dd9d49f3d1590959b2271fb30b5ca2b262c00fb"}, - {file = "kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5"}, - {file = "kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa"}, - {file = "kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2"}, - {file = "kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f"}, - {file = "kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1"}, - {file = "kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d"}, -] - -[[package]] -name = "knack" -version = "0.11.0" -description = "A Command-Line Interface framework" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "knack-0.11.0-py3-none-any.whl", hash = "sha256:6704c867840978a119a193914a90e2e98c7be7dff764c8fcd8a2286c5a978d00"}, - {file = "knack-0.11.0.tar.gz", hash = "sha256:eb6568001e9110b1b320941431c51033d104cc98cda2254a5c2b09ba569fd494"}, -] - -[package.dependencies] -argcomplete = "*" -jmespath = "*" -packaging = "*" -pygments = "*" -pyyaml = "*" -tabulate = "*" - -[[package]] -name = "kombu" -version = "5.6.2" -description = "Messaging library for Python." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "kombu-5.6.2-py3-none-any.whl", hash = "sha256:efcfc559da324d41d61ca311b0c64965ea35b4c55cc04ee36e55386145dace93"}, - {file = "kombu-5.6.2.tar.gz", hash = "sha256:8060497058066c6f5aed7c26d7cd0d3b574990b09de842a8c5aaed0b92cc5a55"}, -] - -[package.dependencies] -amqp = ">=5.1.1,<6.0.0" -packaging = "*" -tzdata = ">=2025.2" -vine = "5.1.0" - -[package.extras] -azureservicebus = ["azure-servicebus (>=7.10.0)"] -azurestoragequeues = ["azure-identity (>=1.12.0)", "azure-storage-queue (>=12.6.0)"] -confluentkafka = ["confluent-kafka (>=2.2.0)"] -consul = ["python-consul2 (==0.1.5)"] -gcpubsub = ["google-cloud-monitoring (>=2.16.0)", "google-cloud-pubsub (>=2.18.4)", "grpcio (==1.75.1)", "protobuf (==6.32.1)"] -librabbitmq = ["librabbitmq (>=2.0.0) ; python_version < \"3.11\""] -mongodb = ["pymongo (==4.15.3)"] -msgpack = ["msgpack (==1.1.2)"] -pyro = ["pyro4 (==4.82)"] -qpid = ["qpid-python (==1.36.0.post1)", "qpid-tools (==1.36.0.post1)"] -redis = ["redis (>=4.5.2,!=4.5.5,!=5.0.2,<6.5)"] -slmq = ["softlayer_messaging (>=1.0.3)"] -sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] -sqs = ["boto3 (>=1.26.143)", "pycurl (>=7.43.0.5) ; sys_platform != \"win32\" and platform_python_implementation == \"CPython\"", "urllib3 (>=1.26.16)"] -yaml = ["PyYAML (>=3.10)"] -zookeeper = ["kazoo (>=2.8.0)"] - -[[package]] -name = "kubernetes" -version = "32.0.1" -description = "Kubernetes python client" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "kubernetes-32.0.1-py2.py3-none-any.whl", hash = "sha256:35282ab8493b938b08ab5526c7ce66588232df00ef5e1dbe88a419107dc10998"}, - {file = "kubernetes-32.0.1.tar.gz", hash = "sha256:42f43d49abd437ada79a79a16bd48a604d3471a117a8347e87db693f2ba0ba28"}, -] - -[package.dependencies] -certifi = ">=14.5.14" -durationpy = ">=0.7" -google-auth = ">=1.0.1" -oauthlib = ">=3.2.2" -python-dateutil = ">=2.5.3" -pyyaml = ">=5.4.1" -requests = "*" -requests-oauthlib = "*" -six = ">=1.9.0" -urllib3 = ">=1.24.2" -websocket-client = ">=0.32.0,<0.40.0 || >0.40.0,<0.41.dev0 || >=0.43.dev0" - -[package.extras] -adal = ["adal (>=1.0.2)"] - -[[package]] -name = "lxml" -version = "5.3.2" -description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "lxml-5.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c4b84d6b580a9625dfa47269bf1fd7fbba7ad69e08b16366a46acb005959c395"}, - {file = "lxml-5.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b4c08ecb26e4270a62f81f81899dfff91623d349e433b126931c9c4577169666"}, - {file = "lxml-5.3.2-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef926e9f11e307b5a7c97b17c5c609a93fb59ffa8337afac8f89e6fe54eb0b37"}, - {file = "lxml-5.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:017ceeabe739100379fe6ed38b033cd244ce2da4e7f6f07903421f57da3a19a2"}, - {file = "lxml-5.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dae97d9435dc90590f119d056d233c33006b2fd235dd990d5564992261ee7ae8"}, - {file = "lxml-5.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:910f39425c6798ce63c93976ae5af5fff6949e2cb446acbd44d6d892103eaea8"}, - {file = "lxml-5.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9780de781a0d62a7c3680d07963db3048b919fc9e3726d9cfd97296a65ffce1"}, - {file = "lxml-5.3.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:1a06b0c6ba2e3ca45a009a78a4eb4d6b63831830c0a83dcdc495c13b9ca97d3e"}, - {file = "lxml-5.3.2-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:4c62d0a34d1110769a1bbaf77871a4b711a6f59c4846064ccb78bc9735978644"}, - {file = "lxml-5.3.2-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:8f961a4e82f411b14538fe5efc3e6b953e17f5e809c463f0756a0d0e8039b700"}, - {file = "lxml-5.3.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:3dfc78f5f9251b6b8ad37c47d4d0bfe63ceb073a916e5b50a3bf5fd67a703335"}, - {file = "lxml-5.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10e690bc03214d3537270c88e492b8612d5e41b884f232df2b069b25b09e6711"}, - {file = "lxml-5.3.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aa837e6ee9534de8d63bc4c1249e83882a7ac22bd24523f83fad68e6ffdf41ae"}, - {file = "lxml-5.3.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:da4c9223319400b97a2acdfb10926b807e51b69eb7eb80aad4942c0516934858"}, - {file = "lxml-5.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dc0e9bdb3aa4d1de703a437576007d366b54f52c9897cae1a3716bb44fc1fc85"}, - {file = "lxml-5.3.2-cp310-cp310-win32.win32.whl", hash = "sha256:dd755a0a78dd0b2c43f972e7b51a43be518ebc130c9f1a7c4480cf08b4385486"}, - {file = "lxml-5.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:d64ea1686474074b38da13ae218d9fde0d1dc6525266976808f41ac98d9d7980"}, - {file = "lxml-5.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9d61a7d0d208ace43986a92b111e035881c4ed45b1f5b7a270070acae8b0bfb4"}, - {file = "lxml-5.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:856dfd7eda0b75c29ac80a31a6411ca12209183e866c33faf46e77ace3ce8a79"}, - {file = "lxml-5.3.2-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a01679e4aad0727bedd4c9407d4d65978e920f0200107ceeffd4b019bd48529"}, - {file = "lxml-5.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6b37b4c3acb8472d191816d4582379f64d81cecbdce1a668601745c963ca5cc"}, - {file = "lxml-5.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3df5a54e7b7c31755383f126d3a84e12a4e0333db4679462ef1165d702517477"}, - {file = "lxml-5.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c09a40f28dcded933dc16217d6a092be0cc49ae25811d3b8e937c8060647c353"}, - {file = "lxml-5.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1ef20f1851ccfbe6c5a04c67ec1ce49da16ba993fdbabdce87a92926e505412"}, - {file = "lxml-5.3.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f79a63289dbaba964eb29ed3c103b7911f2dce28c36fe87c36a114e6bd21d7ad"}, - {file = "lxml-5.3.2-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:75a72697d95f27ae00e75086aed629f117e816387b74a2f2da6ef382b460b710"}, - {file = "lxml-5.3.2-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:b9b00c9ee1cc3a76f1f16e94a23c344e0b6e5c10bec7f94cf2d820ce303b8c01"}, - {file = "lxml-5.3.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:77cbcab50cbe8c857c6ba5f37f9a3976499c60eada1bf6d38f88311373d7b4bc"}, - {file = "lxml-5.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:29424058f072a24622a0a15357bca63d796954758248a72da6d512f9bd9a4493"}, - {file = "lxml-5.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7d82737a8afe69a7c80ef31d7626075cc7d6e2267f16bf68af2c764b45ed68ab"}, - {file = "lxml-5.3.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:95473d1d50a5d9fcdb9321fdc0ca6e1edc164dce4c7da13616247d27f3d21e31"}, - {file = "lxml-5.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2162068f6da83613f8b2a32ca105e37a564afd0d7009b0b25834d47693ce3538"}, - {file = "lxml-5.3.2-cp311-cp311-win32.whl", hash = "sha256:f8695752cf5d639b4e981afe6c99e060621362c416058effd5c704bede9cb5d1"}, - {file = "lxml-5.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:d1a94cbb4ee64af3ab386c2d63d6d9e9cf2e256ac0fd30f33ef0a3c88f575174"}, - {file = "lxml-5.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:16b3897691ec0316a1aa3c6585f61c8b7978475587c5b16fc1d2c28d283dc1b0"}, - {file = "lxml-5.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a8d4b34a0eeaf6e73169dcfd653c8d47f25f09d806c010daf074fba2db5e2d3f"}, - {file = "lxml-5.3.2-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9cd7a959396da425022e1e4214895b5cfe7de7035a043bcc2d11303792b67554"}, - {file = "lxml-5.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cac5eaeec3549c5df7f8f97a5a6db6963b91639389cdd735d5a806370847732b"}, - {file = "lxml-5.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29b5f7d77334877c2146e7bb8b94e4df980325fab0a8af4d524e5d43cd6f789d"}, - {file = "lxml-5.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13f3495cfec24e3d63fffd342cc8141355d1d26ee766ad388775f5c8c5ec3932"}, - {file = "lxml-5.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e70ad4c9658beeff99856926fd3ee5fde8b519b92c693f856007177c36eb2e30"}, - {file = "lxml-5.3.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:507085365783abd7879fa0a6fa55eddf4bdd06591b17a2418403bb3aff8a267d"}, - {file = "lxml-5.3.2-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:5bb304f67cbf5dfa07edad904732782cbf693286b9cd85af27059c5779131050"}, - {file = "lxml-5.3.2-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:3d84f5c093645c21c29a4e972b84cb7cf682f707f8706484a5a0c7ff13d7a988"}, - {file = "lxml-5.3.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:bdc13911db524bd63f37b0103af014b7161427ada41f1b0b3c9b5b5a9c1ca927"}, - {file = "lxml-5.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ec944539543f66ebc060ae180d47e86aca0188bda9cbfadff47d86b0dc057dc"}, - {file = "lxml-5.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:59d437cc8a7f838282df5a199cf26f97ef08f1c0fbec6e84bd6f5cc2b7913f6e"}, - {file = "lxml-5.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e275961adbd32e15672e14e0cc976a982075208224ce06d149c92cb43db5b93"}, - {file = "lxml-5.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:038aeb6937aa404480c2966b7f26f1440a14005cb0702078c173c028eca72c31"}, - {file = "lxml-5.3.2-cp312-cp312-win32.whl", hash = "sha256:3c2c8d0fa3277147bff180e3590be67597e17d365ce94beb2efa3138a2131f71"}, - {file = "lxml-5.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:77809fcd97dfda3f399102db1794f7280737b69830cd5c961ac87b3c5c05662d"}, - {file = "lxml-5.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:77626571fb5270ceb36134765f25b665b896243529eefe840974269b083e090d"}, - {file = "lxml-5.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78a533375dc7aa16d0da44af3cf6e96035e484c8c6b2b2445541a5d4d3d289ee"}, - {file = "lxml-5.3.2-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6f62b2404b3f3f0744bbcabb0381c5fe186fa2a9a67ecca3603480f4846c585"}, - {file = "lxml-5.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ea918da00091194526d40c30c4996971f09dacab032607581f8d8872db34fbf"}, - {file = "lxml-5.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c35326f94702a7264aa0eea826a79547d3396a41ae87a70511b9f6e9667ad31c"}, - {file = "lxml-5.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3bef90af21d31c4544bc917f51e04f94ae11b43156356aff243cdd84802cbf2"}, - {file = "lxml-5.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52fa7ba11a495b7cbce51573c73f638f1dcff7b3ee23697467dc063f75352a69"}, - {file = "lxml-5.3.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ad131e2c4d2c3803e736bb69063382334e03648de2a6b8f56a878d700d4b557d"}, - {file = "lxml-5.3.2-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:00a4463ca409ceacd20490a893a7e08deec7870840eff33dc3093067b559ce3e"}, - {file = "lxml-5.3.2-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:87e8d78205331cace2b73ac8249294c24ae3cba98220687b5b8ec5971a2267f1"}, - {file = "lxml-5.3.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:bf6389133bb255e530a4f2f553f41c4dd795b1fbb6f797aea1eff308f1e11606"}, - {file = "lxml-5.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b3709fc752b42fb6b6ffa2ba0a5b9871646d97d011d8f08f4d5b3ee61c7f3b2b"}, - {file = "lxml-5.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:abc795703d0de5d83943a4badd770fbe3d1ca16ee4ff3783d7caffc252f309ae"}, - {file = "lxml-5.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:98050830bb6510159f65d9ad1b8aca27f07c01bb3884ba95f17319ccedc4bcf9"}, - {file = "lxml-5.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6ba465a91acc419c5682f8b06bcc84a424a7aa5c91c220241c6fd31de2a72bc6"}, - {file = "lxml-5.3.2-cp313-cp313-win32.whl", hash = "sha256:56a1d56d60ea1ec940f949d7a309e0bff05243f9bd337f585721605670abb1c1"}, - {file = "lxml-5.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:1a580dc232c33d2ad87d02c8a3069d47abbcdce974b9c9cc82a79ff603065dbe"}, - {file = "lxml-5.3.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1a59f7fe888d0ec1916d0ad69364c5400cfa2f885ae0576d909f342e94d26bc9"}, - {file = "lxml-5.3.2-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d67b50abc2df68502a26ed2ccea60c1a7054c289fb7fc31c12e5e55e4eec66bd"}, - {file = "lxml-5.3.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2cb08d2cb047c98d6fbbb2e77d6edd132ad6e3fa5aa826ffa9ea0c9b1bc74a84"}, - {file = "lxml-5.3.2-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:495ddb7e10911fb4d673d8aa8edd98d1eadafb3b56e8c1b5f427fd33cadc455b"}, - {file = "lxml-5.3.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:884d9308ac7d581b705a3371185282e1b8eebefd68ccf288e00a2d47f077cc51"}, - {file = "lxml-5.3.2-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:37f3d7cf7f2dd2520df6cc8a13df4c3e3f913c8e0a1f9a875e44f9e5f98d7fee"}, - {file = "lxml-5.3.2-cp36-cp36m-win32.whl", hash = "sha256:e885a1bf98a76dff0a0648850c3083b99d9358ef91ba8fa307c681e8e0732503"}, - {file = "lxml-5.3.2-cp36-cp36m-win_amd64.whl", hash = "sha256:b45f505d0d85f4cdd440cd7500689b8e95110371eaa09da0c0b1103e9a05030f"}, - {file = "lxml-5.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b53cd668facd60b4f0dfcf092e01bbfefd88271b5b4e7b08eca3184dd006cb30"}, - {file = "lxml-5.3.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5dea998c891f082fe204dec6565dbc2f9304478f2fc97bd4d7a940fec16c873"}, - {file = "lxml-5.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d46bc3e58b01e4f38d75e0d7f745a46875b7a282df145aca9d1479c65ff11561"}, - {file = "lxml-5.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:661feadde89159fd5f7d7639a81ccae36eec46974c4a4d5ccce533e2488949c8"}, - {file = "lxml-5.3.2-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:43af2a69af2cacc2039024da08a90174e85f3af53483e6b2e3485ced1bf37151"}, - {file = "lxml-5.3.2-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:1539f962d82436f3d386eb9f29b2a29bb42b80199c74a695dff51b367a61ec0a"}, - {file = "lxml-5.3.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:6673920bf976421b5fac4f29b937702eef4555ee42329546a5fc68bae6178a48"}, - {file = "lxml-5.3.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:9fa722a9cd8845594593cce399a49aa6bfc13b6c83a7ee05e2ab346d9253d52f"}, - {file = "lxml-5.3.2-cp37-cp37m-win32.whl", hash = "sha256:2eadd4efa487f4710755415aed3d6ae9ac8b4327ea45226ffccb239766c8c610"}, - {file = "lxml-5.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:83d8707b1b08cd02c04d3056230ec3b771b18c566ec35e723e60cdf037064e08"}, - {file = "lxml-5.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bc6e8678bfa5ccba370103976ccfcf776c85c83da9220ead41ea6fd15d2277b4"}, - {file = "lxml-5.3.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bed509662f67f719119ad56006cd4a38efa68cfa74383060612044915e5f7ad"}, - {file = "lxml-5.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e3925975fadd6fd72a6d80541a6ec75dfbad54044a03aa37282dafcb80fbdfa"}, - {file = "lxml-5.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83c0462dedc5213ac586164c6d7227da9d4d578cf45dd7fbab2ac49b63a008eb"}, - {file = "lxml-5.3.2-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:53e3f9ca72858834688afa17278649d62aa768a4b2018344be00c399c4d29e95"}, - {file = "lxml-5.3.2-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:32ba634ef3f1b20f781019a91d78599224dc45745dd572f951adbf1c0c9b0d75"}, - {file = "lxml-5.3.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:1b16504c53f41da5fcf04868a80ac40a39d3eec5329caf761114caec6e844ad1"}, - {file = "lxml-5.3.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:1f9682786138549da44ca4c49b20e7144d063b75f2b2ba611f4cff9b83db1062"}, - {file = "lxml-5.3.2-cp38-cp38-win32.whl", hash = "sha256:d8f74ef8aacdf6ee5c07566a597634bb8535f6b53dc89790db43412498cf6026"}, - {file = "lxml-5.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:49f1cee0fa27e1ee02589c696a9bdf4027e7427f184fa98e6bef0c6613f6f0fa"}, - {file = "lxml-5.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:741c126bcf9aa939e950e64e5e0a89c8e01eda7a5f5ffdfc67073f2ed849caea"}, - {file = "lxml-5.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ab6e9e6aca1fd7d725ffa132286e70dee5b9a4561c5ed291e836440b82888f89"}, - {file = "lxml-5.3.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58e8c9b9ed3c15c2d96943c14efc324b69be6352fe5585733a7db2bf94d97841"}, - {file = "lxml-5.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7811828ddfb8c23f4f1fbf35e7a7b2edec2f2e4c793dee7c52014f28c4b35238"}, - {file = "lxml-5.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:72968623efb1e12e950cbdcd1d0f28eb14c8535bf4be153f1bfffa818b1cf189"}, - {file = "lxml-5.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ebfceaa2ea588b54efb6160e3520983663d45aed8a3895bb2031ada080fb5f04"}, - {file = "lxml-5.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d685d458505b2bfd2e28c812749fe9194a2b0ce285a83537e4309a187ffa270b"}, - {file = "lxml-5.3.2-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:334e0e414dab1f5366ead8ca34ec3148415f236d5660e175f1d640b11d645847"}, - {file = "lxml-5.3.2-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:02e56f7de72fa82561eae69628a7d6febd7891d72248c7ff7d3e7814d4031017"}, - {file = "lxml-5.3.2-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:638d06b4e1d34d1a074fa87deed5fb55c18485fa0dab97abc5604aad84c12031"}, - {file = "lxml-5.3.2-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:354dab7206d22d7a796fa27c4c5bffddd2393da2ad61835355a4759d435beb47"}, - {file = "lxml-5.3.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d9d9f82ff2c3bf9bb777cb355149f7f3a98ec58f16b7428369dc27ea89556a4c"}, - {file = "lxml-5.3.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:95ad58340e3b7d2b828efc370d1791856613c5cb62ae267158d96e47b3c978c9"}, - {file = "lxml-5.3.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:30fe05f4b7f6e9eb32862745512e7cbd021070ad0f289a7f48d14a0d3fc1d8a9"}, - {file = "lxml-5.3.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:34c688fef86f73dbca0798e0a61bada114677006afa524a8ce97d9e5fabf42e6"}, - {file = "lxml-5.3.2-cp39-cp39-win32.whl", hash = "sha256:4d6d3d1436d57f41984920667ec5ef04bcb158f80df89ac4d0d3f775a2ac0c87"}, - {file = "lxml-5.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:2996e1116bbb3ae2a1fbb2ba4da8f92742290b4011e7e5bce2bd33bbc9d9485a"}, - {file = "lxml-5.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:521ab9c80b98c30b2d987001c3ede2e647e92eeb2ca02e8cb66ef5122d792b24"}, - {file = "lxml-5.3.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f1231b0f9810289d41df1eacc4ebb859c63e4ceee29908a0217403cddce38d0"}, - {file = "lxml-5.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271f1a4d5d2b383c36ad8b9b489da5ea9c04eca795a215bae61ed6a57cf083cd"}, - {file = "lxml-5.3.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:6fca8a5a13906ba2677a5252752832beb0f483a22f6c86c71a2bb320fba04f61"}, - {file = "lxml-5.3.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ea0c3b7922209160faef194a5b6995bfe7fa05ff7dda6c423ba17646b7b9de10"}, - {file = "lxml-5.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0a006390834603e5952a2ff74b9a31a6007c7cc74282a087aa6467afb4eea987"}, - {file = "lxml-5.3.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:eae4136a3b8c4cf76f69461fc8f9410d55d34ea48e1185338848a888d71b9675"}, - {file = "lxml-5.3.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d48e06be8d8c58e7feaedd8a37897a6122637efb1637d7ce00ddf5f11f9a92ad"}, - {file = "lxml-5.3.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4b83aed409134093d90e114007034d2c1ebcd92e501b71fd9ec70e612c8b2eb"}, - {file = "lxml-5.3.2-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7a0e77edfe26d3703f954d46bed52c3ec55f58586f18f4b7f581fc56954f1d84"}, - {file = "lxml-5.3.2-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:19f6fcfd15b82036b4d235749d78785eb9c991c7812012dc084e0d8853b4c1c0"}, - {file = "lxml-5.3.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:d49919c95d31ee06eefd43d8c6f69a3cc9bdf0a9b979cc234c4071f0eb5cb173"}, - {file = "lxml-5.3.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2d0a60841410123c533990f392819804a8448853f06daf412c0f383443925e89"}, - {file = "lxml-5.3.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b7f729e03090eb4e3981f10efaee35e6004b548636b1a062b8b9a525e752abc"}, - {file = "lxml-5.3.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:579df6e20d8acce3bcbc9fb8389e6ae00c19562e929753f534ba4c29cfe0be4b"}, - {file = "lxml-5.3.2-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2abcf3f3b8367d6400b908d00d4cd279fc0b8efa287e9043820525762d383699"}, - {file = "lxml-5.3.2-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:348c06cb2e3176ce98bee8c397ecc89181681afd13d85870df46167f140a305f"}, - {file = "lxml-5.3.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:617ecaccd565cbf1ac82ffcaa410e7da5bd3a4b892bb3543fb2fe19bd1c4467d"}, - {file = "lxml-5.3.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c3eb4278dcdb9d86265ed2c20b9ecac45f2d6072e3904542e591e382c87a9c00"}, - {file = "lxml-5.3.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:258b6b53458c5cbd2a88795557ff7e0db99f73a96601b70bc039114cd4ee9e02"}, - {file = "lxml-5.3.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0a9d8d25ed2f2183e8471c97d512a31153e123ac5807f61396158ef2793cb6e"}, - {file = "lxml-5.3.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:73bcb635a848c18a3e422ea0ab0092f2e4ef3b02d8ebe87ab49748ebc8ec03d8"}, - {file = "lxml-5.3.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1545de0a69a16ced5767bae8cca1801b842e6e49e96f5e4a8a5acbef023d970b"}, - {file = "lxml-5.3.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:165fcdc2f40fc0fe88a3c3c06c9c2a097388a90bda6a16e6f7c9199c903c9b8e"}, - {file = "lxml-5.3.2.tar.gz", hash = "sha256:773947d0ed809ddad824b7b14467e1a481b8976e87278ac4a730c2f7c7fcddc1"}, -] - -[package.extras] -cssselect = ["cssselect (>=0.7)"] -html-clean = ["lxml_html_clean"] -html5 = ["html5lib"] -htmlsoup = ["BeautifulSoup4"] -source = ["Cython (>=3.0.11,<3.1.0)"] - -[[package]] -name = "lz4" -version = "4.4.5" -description = "LZ4 Bindings for Python" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "lz4-4.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d221fa421b389ab2345640a508db57da36947a437dfe31aeddb8d5c7b646c22d"}, - {file = "lz4-4.4.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7dc1e1e2dbd872f8fae529acd5e4839efd0b141eaa8ae7ce835a9fe80fbad89f"}, - {file = "lz4-4.4.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e928ec2d84dc8d13285b4a9288fd6246c5cde4f5f935b479f50d986911f085e3"}, - {file = "lz4-4.4.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daffa4807ef54b927451208f5f85750c545a4abbff03d740835fc444cd97f758"}, - {file = "lz4-4.4.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a2b7504d2dffed3fd19d4085fe1cc30cf221263fd01030819bdd8d2bb101cf1"}, - {file = "lz4-4.4.5-cp310-cp310-win32.whl", hash = "sha256:0846e6e78f374156ccf21c631de80967e03cc3c01c373c665789dc0c5431e7fc"}, - {file = "lz4-4.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:7c4e7c44b6a31de77d4dc9772b7d2561937c9588a734681f70ec547cfbc51ecd"}, - {file = "lz4-4.4.5-cp310-cp310-win_arm64.whl", hash = "sha256:15551280f5656d2206b9b43262799c89b25a25460416ec554075a8dc568e4397"}, - {file = "lz4-4.4.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d6da84a26b3aa5da13a62e4b89ab36a396e9327de8cd48b436a3467077f8ccd4"}, - {file = "lz4-4.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61d0ee03e6c616f4a8b69987d03d514e8896c8b1b7cc7598ad029e5c6aedfd43"}, - {file = "lz4-4.4.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:33dd86cea8375d8e5dd001e41f321d0a4b1eb7985f39be1b6a4f466cd480b8a7"}, - {file = "lz4-4.4.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:609a69c68e7cfcfa9d894dc06be13f2e00761485b62df4e2472f1b66f7b405fb"}, - {file = "lz4-4.4.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:75419bb1a559af00250b8f1360d508444e80ed4b26d9d40ec5b09fe7875cb989"}, - {file = "lz4-4.4.5-cp311-cp311-win32.whl", hash = "sha256:12233624f1bc2cebc414f9efb3113a03e89acce3ab6f72035577bc61b270d24d"}, - {file = "lz4-4.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:8a842ead8ca7c0ee2f396ca5d878c4c40439a527ebad2b996b0444f0074ed004"}, - {file = "lz4-4.4.5-cp311-cp311-win_arm64.whl", hash = "sha256:83bc23ef65b6ae44f3287c38cbf82c269e2e96a26e560aa551735883388dcc4b"}, - {file = "lz4-4.4.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:df5aa4cead2044bab83e0ebae56e0944cc7fcc1505c7787e9e1057d6d549897e"}, - {file = "lz4-4.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d0bf51e7745484d2092b3a51ae6eb58c3bd3ce0300cf2b2c14f76c536d5697a"}, - {file = "lz4-4.4.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7b62f94b523c251cf32aa4ab555f14d39bd1a9df385b72443fd76d7c7fb051f5"}, - {file = "lz4-4.4.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c3ea562c3af274264444819ae9b14dbbf1ab070aff214a05e97db6896c7597e"}, - {file = "lz4-4.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24092635f47538b392c4eaeff14c7270d2c8e806bf4be2a6446a378591c5e69e"}, - {file = "lz4-4.4.5-cp312-cp312-win32.whl", hash = "sha256:214e37cfe270948ea7eb777229e211c601a3e0875541c1035ab408fbceaddf50"}, - {file = "lz4-4.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:713a777de88a73425cf08eb11f742cd2c98628e79a8673d6a52e3c5f0c116f33"}, - {file = "lz4-4.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:a88cbb729cc333334ccfb52f070463c21560fca63afcf636a9f160a55fac3301"}, - {file = "lz4-4.4.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6bb05416444fafea170b07181bc70640975ecc2a8c92b3b658c554119519716c"}, - {file = "lz4-4.4.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b424df1076e40d4e884cfcc4c77d815368b7fb9ebcd7e634f937725cd9a8a72a"}, - {file = "lz4-4.4.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:216ca0c6c90719731c64f41cfbd6f27a736d7e50a10b70fad2a9c9b262ec923d"}, - {file = "lz4-4.4.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:533298d208b58b651662dd972f52d807d48915176e5b032fb4f8c3b6f5fe535c"}, - {file = "lz4-4.4.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:451039b609b9a88a934800b5fc6ee401c89ad9c175abf2f4d9f8b2e4ef1afc64"}, - {file = "lz4-4.4.5-cp313-cp313-win32.whl", hash = "sha256:a5f197ffa6fc0e93207b0af71b302e0a2f6f29982e5de0fbda61606dd3a55832"}, - {file = "lz4-4.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:da68497f78953017deb20edff0dba95641cc86e7423dfadf7c0264e1ac60dc22"}, - {file = "lz4-4.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:c1cfa663468a189dab510ab231aad030970593f997746d7a324d40104db0d0a9"}, - {file = "lz4-4.4.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67531da3b62f49c939e09d56492baf397175ff39926d0bd5bd2d191ac2bff95f"}, - {file = "lz4-4.4.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a1acbbba9edbcbb982bc2cac5e7108f0f553aebac1040fbec67a011a45afa1ba"}, - {file = "lz4-4.4.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a482eecc0b7829c89b498fda883dbd50e98153a116de612ee7c111c8bcf82d1d"}, - {file = "lz4-4.4.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e099ddfaa88f59dd8d36c8a3c66bd982b4984edf127eb18e30bb49bdba68ce67"}, - {file = "lz4-4.4.5-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2af2897333b421360fdcce895c6f6281dc3fab018d19d341cf64d043fc8d90d"}, - {file = "lz4-4.4.5-cp313-cp313t-win32.whl", hash = "sha256:66c5de72bf4988e1b284ebdd6524c4bead2c507a2d7f172201572bac6f593901"}, - {file = "lz4-4.4.5-cp313-cp313t-win_amd64.whl", hash = "sha256:cdd4bdcbaf35056086d910d219106f6a04e1ab0daa40ec0eeef1626c27d0fddb"}, - {file = "lz4-4.4.5-cp313-cp313t-win_arm64.whl", hash = "sha256:28ccaeb7c5222454cd5f60fcd152564205bcb801bd80e125949d2dfbadc76bbd"}, - {file = "lz4-4.4.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c216b6d5275fc060c6280936bb3bb0e0be6126afb08abccde27eed23dead135f"}, - {file = "lz4-4.4.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c8e71b14938082ebaf78144f3b3917ac715f72d14c076f384a4c062df96f9df6"}, - {file = "lz4-4.4.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b5e6abca8df9f9bdc5c3085f33ff32cdc86ed04c65e0355506d46a5ac19b6e9"}, - {file = "lz4-4.4.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b84a42da86e8ad8537aabef062e7f661f4a877d1c74d65606c49d835d36d668"}, - {file = "lz4-4.4.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bba042ec5a61fa77c7e380351a61cb768277801240249841defd2ff0a10742f"}, - {file = "lz4-4.4.5-cp314-cp314-win32.whl", hash = "sha256:bd85d118316b53ed73956435bee1997bd06cc66dd2fa74073e3b1322bd520a67"}, - {file = "lz4-4.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:92159782a4502858a21e0079d77cdcaade23e8a5d252ddf46b0652604300d7be"}, - {file = "lz4-4.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:d994b87abaa7a88ceb7a37c90f547b8284ff9da694e6afcfaa8568d739faf3f7"}, - {file = "lz4-4.4.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f6538aaaedd091d6e5abdaa19b99e6e82697d67518f114721b5248709b639fad"}, - {file = "lz4-4.4.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:13254bd78fef50105872989a2dc3418ff09aefc7d0765528adc21646a7288294"}, - {file = "lz4-4.4.5-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e64e61f29cf95afb43549063d8433b46352baf0c8a70aa45e2585618fcf59d86"}, - {file = "lz4-4.4.5-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff1b50aeeec64df5603f17984e4b5be6166058dcf8f1e26a3da40d7a0f6ab547"}, - {file = "lz4-4.4.5-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1dd4d91d25937c2441b9fc0f4af01704a2d09f30a38c5798bc1d1b5a15ec9581"}, - {file = "lz4-4.4.5-cp39-cp39-win32.whl", hash = "sha256:d64141085864918392c3159cdad15b102a620a67975c786777874e1e90ef15ce"}, - {file = "lz4-4.4.5-cp39-cp39-win_amd64.whl", hash = "sha256:f32b9e65d70f3684532358255dc053f143835c5f5991e28a5ac4c93ce94b9ea7"}, - {file = "lz4-4.4.5-cp39-cp39-win_arm64.whl", hash = "sha256:f9b8bde9909a010c75b3aea58ec3910393b758f3c219beed67063693df854db0"}, - {file = "lz4-4.4.5.tar.gz", hash = "sha256:5f0b9e53c1e82e88c10d7c180069363980136b9d7a8306c4dca4f760d60c39f0"}, -] - -[package.extras] -docs = ["sphinx (>=1.6.0)", "sphinx_bootstrap_theme"] -flake8 = ["flake8"] -tests = ["psutil", "pytest (!=3.3.0)", "pytest-cov"] - -[[package]] -name = "markdown" -version = "3.10.2" -description = "Python implementation of John Gruber's Markdown." -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36"}, - {file = "markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950"}, -] - -[package.extras] -docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python] (>=0.28.3)"] -testing = ["coverage", "pyyaml"] - -[[package]] -name = "markdown-it-py" -version = "4.0.0" -description = "Python port of markdown-it. Markdown parsing, done right!" -optional = false -python-versions = ">=3.10" -groups = ["main", "dev"] -files = [ - {file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"}, - {file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"}, -] - -[package.dependencies] -mdurl = ">=0.1,<1.0" - -[package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"] -linkify = ["linkify-it-py (>=1,<3)"] -plugins = ["mdit-py-plugins (>=0.5.0)"] -profiling = ["gprof2dot"] -rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "requests"] - -[[package]] -name = "markupsafe" -version = "3.0.3" -description = "Safely add untrusted strings to HTML/XML markup." -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, - {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, - {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"}, - {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"}, - {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"}, - {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"}, - {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"}, - {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"}, - {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"}, - {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}, - {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}, - {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"}, - {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"}, - {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"}, - {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"}, - {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"}, - {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"}, - {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"}, - {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"}, - {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"}, - {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"}, - {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"}, - {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"}, - {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"}, - {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"}, - {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"}, - {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"}, - {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"}, - {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"}, - {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"}, - {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"}, - {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"}, - {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"}, - {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"}, - {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"}, - {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"}, - {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"}, - {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"}, - {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"}, - {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"}, - {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"}, - {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"}, - {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"}, - {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"}, - {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"}, - {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"}, - {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"}, - {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"}, - {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"}, - {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"}, - {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"}, - {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"}, - {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"}, - {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"}, - {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"}, - {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"}, - {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"}, - {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"}, - {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"}, - {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"}, - {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"}, - {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"}, - {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"}, - {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"}, - {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"}, - {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"}, - {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"}, - {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"}, - {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"}, - {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"}, - {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"}, - {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"}, - {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"}, - {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"}, - {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"}, - {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"}, - {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"}, - {file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"}, - {file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"}, - {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"}, - {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"}, - {file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"}, - {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"}, - {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"}, - {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"}, - {file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"}, - {file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"}, - {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}, - {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, -] - -[[package]] -name = "marshmallow" -version = "4.3.0" -description = "A lightweight library for converting complex datatypes to and from native Python datatypes." -optional = false -python-versions = ">=3.10" -groups = ["main", "dev"] -files = [ - {file = "marshmallow-4.3.0-py3-none-any.whl", hash = "sha256:46c4fe6984707e3cbd485dfebbf0a59874f58d695aad05c1668d15e8c6e13b46"}, - {file = "marshmallow-4.3.0.tar.gz", hash = "sha256:fb43c53b3fe240b8f6af37223d6ef1636f927ad9bea8ab323afad95dff090880"}, -] - -[[package]] -name = "matplotlib" -version = "3.10.8" -description = "Python plotting package" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "matplotlib-3.10.8-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:00270d217d6b20d14b584c521f810d60c5c78406dc289859776550df837dcda7"}, - {file = "matplotlib-3.10.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37b3c1cc42aa184b3f738cfa18c1c1d72fd496d85467a6cf7b807936d39aa656"}, - {file = "matplotlib-3.10.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee40c27c795bda6a5292e9cff9890189d32f7e3a0bf04e0e3c9430c4a00c37df"}, - {file = "matplotlib-3.10.8-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a48f2b74020919552ea25d222d5cc6af9ca3f4eb43a93e14d068457f545c2a17"}, - {file = "matplotlib-3.10.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f254d118d14a7f99d616271d6c3c27922c092dac11112670b157798b89bf4933"}, - {file = "matplotlib-3.10.8-cp310-cp310-win_amd64.whl", hash = "sha256:f9b587c9c7274c1613a30afabf65a272114cd6cdbe67b3406f818c79d7ab2e2a"}, - {file = "matplotlib-3.10.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160"}, - {file = "matplotlib-3.10.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78"}, - {file = "matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4"}, - {file = "matplotlib-3.10.8-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2"}, - {file = "matplotlib-3.10.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6"}, - {file = "matplotlib-3.10.8-cp311-cp311-win_amd64.whl", hash = "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9"}, - {file = "matplotlib-3.10.8-cp311-cp311-win_arm64.whl", hash = "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2"}, - {file = "matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a"}, - {file = "matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58"}, - {file = "matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04"}, - {file = "matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f"}, - {file = "matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466"}, - {file = "matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf"}, - {file = "matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b"}, - {file = "matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6"}, - {file = "matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1"}, - {file = "matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486"}, - {file = "matplotlib-3.10.8-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce"}, - {file = "matplotlib-3.10.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6"}, - {file = "matplotlib-3.10.8-cp313-cp313-win_amd64.whl", hash = "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149"}, - {file = "matplotlib-3.10.8-cp313-cp313-win_arm64.whl", hash = "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645"}, - {file = "matplotlib-3.10.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077"}, - {file = "matplotlib-3.10.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22"}, - {file = "matplotlib-3.10.8-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39"}, - {file = "matplotlib-3.10.8-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565"}, - {file = "matplotlib-3.10.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a"}, - {file = "matplotlib-3.10.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958"}, - {file = "matplotlib-3.10.8-cp313-cp313t-win_arm64.whl", hash = "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5"}, - {file = "matplotlib-3.10.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f"}, - {file = "matplotlib-3.10.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b"}, - {file = "matplotlib-3.10.8-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d"}, - {file = "matplotlib-3.10.8-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008"}, - {file = "matplotlib-3.10.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c"}, - {file = "matplotlib-3.10.8-cp314-cp314-win_amd64.whl", hash = "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11"}, - {file = "matplotlib-3.10.8-cp314-cp314-win_arm64.whl", hash = "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8"}, - {file = "matplotlib-3.10.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50"}, - {file = "matplotlib-3.10.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908"}, - {file = "matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a"}, - {file = "matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1"}, - {file = "matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c"}, - {file = "matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b"}, - {file = "matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f"}, - {file = "matplotlib-3.10.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f97aeb209c3d2511443f8797e3e5a569aebb040d4f8bc79aa3ee78a8fb9e3dd8"}, - {file = "matplotlib-3.10.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fb061f596dad3a0f52b60dc6a5dec4a0c300dec41e058a7efe09256188d170b7"}, - {file = "matplotlib-3.10.8-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12d90df9183093fcd479f4172ac26b322b1248b15729cb57f42f71f24c7e37a3"}, - {file = "matplotlib-3.10.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1"}, - {file = "matplotlib-3.10.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a"}, - {file = "matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2"}, - {file = "matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3"}, -] - -[package.dependencies] -contourpy = ">=1.0.1" -cycler = ">=0.10" -fonttools = ">=4.22.0" -kiwisolver = ">=1.3.1" -numpy = ">=1.23" -packaging = ">=20.0" -pillow = ">=8" -pyparsing = ">=3" -python-dateutil = ">=2.7" - -[package.extras] -dev = ["meson-python (>=0.13.1,<0.17.0)", "pybind11 (>=2.13.2,!=2.13.3)", "setuptools (>=64)", "setuptools_scm (>=7)"] - -[[package]] -name = "mccabe" -version = "0.7.0" -description = "McCabe checker, plugin for flake8" -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -description = "Markdown URL utilities" -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, -] - -[[package]] -name = "microsoft-kiota-abstractions" -version = "1.9.2" -description = "Core abstractions for kiota generated libraries in Python" -optional = false -python-versions = "<4.0,>=3.9" -groups = ["main"] -files = [ - {file = "microsoft_kiota_abstractions-1.9.2-py3-none-any.whl", hash = "sha256:a8853d272a84da59d6a2fe11a76c28e9c55bdab268a345ba48e918cb6822b607"}, - {file = "microsoft_kiota_abstractions-1.9.2.tar.gz", hash = "sha256:29cdafe8d0672f23099556e0b120dca6231c752cca9393e1e0092fa9ca594572"}, -] - -[package.dependencies] -opentelemetry-api = ">=1.27.0" -opentelemetry-sdk = ">=1.27.0" -std-uritemplate = ">=2.0.0" - -[[package]] -name = "microsoft-kiota-authentication-azure" -version = "1.9.2" -description = "Core abstractions for kiota generated libraries in Python" -optional = false -python-versions = "<4.0,>=3.9" -groups = ["main"] -files = [ - {file = "microsoft_kiota_authentication_azure-1.9.2-py3-none-any.whl", hash = "sha256:56840f8b15df8aedfd143fb2deb7cc7fae4ac0bafb1a50546b7313a7b3ab4ca0"}, - {file = "microsoft_kiota_authentication_azure-1.9.2.tar.gz", hash = "sha256:171045f522a93d9340fbddc4cabb218f14f1d9d289e82e535b3d9291986c3d5a"}, -] - -[package.dependencies] -aiohttp = ">=3.8.0" -azure-core = ">=1.21.1" -microsoft-kiota-abstractions = ">=1.9.2,<1.10.0" -opentelemetry-api = ">=1.27.0" -opentelemetry-sdk = ">=1.27.0" - -[[package]] -name = "microsoft-kiota-http" -version = "1.9.2" -description = "Core abstractions for kiota generated libraries in Python" -optional = false -python-versions = "<4.0,>=3.9" -groups = ["main"] -files = [ - {file = "microsoft_kiota_http-1.9.2-py3-none-any.whl", hash = "sha256:3a2d930a70d0184d9f4848473f929ee892462cae1acfaf33b2d193f1828c76c2"}, - {file = "microsoft_kiota_http-1.9.2.tar.gz", hash = "sha256:2ba3d04a3d1d5d600736eebc1e33533d54d87799ac4fbb92c9cce4a97809af61"}, -] - -[package.dependencies] -httpx = {version = ">=0.25,<1.0.0", extras = ["http2"]} -microsoft-kiota-abstractions = ">=1.9.2,<1.10.0" -opentelemetry-api = ">=1.27.0" -opentelemetry-sdk = ">=1.27.0" - -[[package]] -name = "microsoft-kiota-serialization-form" -version = "1.9.2" -description = "Core abstractions for kiota generated libraries in Python" -optional = false -python-versions = "<4.0,>=3.9" -groups = ["main"] -files = [ - {file = "microsoft_kiota_serialization_form-1.9.2-py3-none-any.whl", hash = "sha256:7b997efb2c8750b1d4fbc00878ba2a3e6e1df3fcefc8815226c90fcc9c54f218"}, - {file = "microsoft_kiota_serialization_form-1.9.2.tar.gz", hash = "sha256:badfbe65d8ec3369bd58b01022d13ef590edf14babeef94188efe3f4ec24fe41"}, -] - -[package.dependencies] -microsoft-kiota-abstractions = ">=1.9.2,<1.10.0" - -[[package]] -name = "microsoft-kiota-serialization-json" -version = "1.9.2" -description = "Core abstractions for kiota generated libraries in Python" -optional = false -python-versions = "<4.0,>=3.9" -groups = ["main"] -files = [ - {file = "microsoft_kiota_serialization_json-1.9.2-py3-none-any.whl", hash = "sha256:8f4ecf485607fff3df5ce8fa9b9c957bc7f4bff1658b183703e180af753098e3"}, - {file = "microsoft_kiota_serialization_json-1.9.2.tar.gz", hash = "sha256:19f7beb69c67b2cb77ca96f77824ee78a693929e20237bb5476ea54f69118bf1"}, -] - -[package.dependencies] -microsoft-kiota-abstractions = ">=1.9.2,<1.10.0" - -[[package]] -name = "microsoft-kiota-serialization-multipart" -version = "1.9.2" -description = "Core abstractions for kiota generated libraries in Python" -optional = false -python-versions = "<4.0,>=3.9" -groups = ["main"] -files = [ - {file = "microsoft_kiota_serialization_multipart-1.9.2-py3-none-any.whl", hash = "sha256:641ad374046f1c7adff90d110bdc68d77418adb1e479a716f4ffea3647f0ead6"}, - {file = "microsoft_kiota_serialization_multipart-1.9.2.tar.gz", hash = "sha256:b1851409205668d83f5c7a35a8b6fca974b341985b4a92841e95aaec93b7ca0a"}, -] - -[package.dependencies] -microsoft-kiota-abstractions = ">=1.9.2,<1.10.0" - -[[package]] -name = "microsoft-kiota-serialization-text" -version = "1.9.2" -description = "Core abstractions for kiota generated libraries in Python" -optional = false -python-versions = "<4.0,>=3.9" -groups = ["main"] -files = [ - {file = "microsoft_kiota_serialization_text-1.9.2-py3-none-any.whl", hash = "sha256:6e63129ea29eb9b976f4ed56fc6595d204e29fc309958b639299e9f9f4e5edb4"}, - {file = "microsoft_kiota_serialization_text-1.9.2.tar.gz", hash = "sha256:4289508ebac0cefdc4fa21c545051769a9409913972355ccda9116b647f978f2"}, -] - -[package.dependencies] -microsoft-kiota-abstractions = ">=1.9.2,<1.10.0" - -[[package]] -name = "microsoft-security-utilities-secret-masker" -version = "1.0.0b4" -description = "A tool for detecting and masking secrets" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "microsoft_security_utilities_secret_masker-1.0.0b4-py3-none-any.whl", hash = "sha256:0429fcaad10fc8ae3f940ab84fd2926e4f50ede134162144123b35937be831a8"}, - {file = "microsoft_security_utilities_secret_masker-1.0.0b4.tar.gz", hash = "sha256:a30bd361ac18c8b52f6844076bc26465335949ea9c7a004d95f5196ec6fdef3e"}, -] - -[[package]] -name = "msal" -version = "1.35.0b1" -description = "The Microsoft Authentication Library (MSAL) for Python library enables your app to access the Microsoft Cloud by supporting authentication of users with Microsoft Azure Active Directory accounts (AAD) and Microsoft Accounts (MSA) using industry standard OAuth2 and OpenID Connect." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "msal-1.35.0b1-py3-none-any.whl", hash = "sha256:bf656775c64bbc2103d8255980f5c3c966c7432106795e1fe70ca338a7e43150"}, - {file = "msal-1.35.0b1.tar.gz", hash = "sha256:fe8143079183a5c952cd9f3ba66a148fe7bae9fb9952bd0e834272bfbeb34508"}, -] - -[package.dependencies] -cryptography = ">=2.5,<49" -PyJWT = {version = ">=1.0.0,<3", extras = ["crypto"]} -pymsalruntime = [ - {version = ">=0.14,<0.21", optional = true, markers = "python_version >= \"3.8\" and platform_system == \"Windows\" and extra == \"broker\""}, - {version = ">=0.17,<0.21", optional = true, markers = "python_version >= \"3.8\" and platform_system == \"Darwin\" and extra == \"broker\""}, - {version = ">=0.18,<0.21", optional = true, markers = "python_version >= \"3.8\" and platform_system == \"Linux\" and extra == \"broker\""}, -] -requests = ">=2.0.0,<3" - -[package.extras] -broker = ["pymsalruntime (>=0.14,<0.21) ; python_version >= \"3.8\" and platform_system == \"Windows\"", "pymsalruntime (>=0.17,<0.21) ; python_version >= \"3.8\" and platform_system == \"Darwin\"", "pymsalruntime (>=0.18,<0.21) ; python_version >= \"3.8\" and platform_system == \"Linux\""] - -[[package]] -name = "msal-extensions" -version = "1.2.0" -description = "Microsoft Authentication Library extensions (MSAL EX) provides a persistence API that can save your data on disk, encrypted on Windows, macOS and Linux. Concurrent data access will be coordinated by a file lock mechanism." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "msal_extensions-1.2.0-py3-none-any.whl", hash = "sha256:cf5ba83a2113fa6dc011a254a72f1c223c88d7dfad74cc30617c4679a417704d"}, - {file = "msal_extensions-1.2.0.tar.gz", hash = "sha256:6f41b320bfd2933d631a215c91ca0dd3e67d84bd1a2f50ce917d5874ec646bef"}, -] - -[package.dependencies] -msal = ">=1.29,<2" -portalocker = ">=1.4,<3" - -[[package]] -name = "msgraph-core" -version = "1.3.8" -description = "Core component of the Microsoft Graph Python SDK" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "msgraph_core-1.3.8-py3-none-any.whl", hash = "sha256:86d83edcf62119946f201d13b7e857c947ef67addb088883940197081de85bea"}, - {file = "msgraph_core-1.3.8.tar.gz", hash = "sha256:6e883f9d4c4ad57501234749e07b010478c1a5f19550ef4cf005bbcac4a63ae7"}, -] - -[package.dependencies] -httpx = {version = ">=0.23.0", extras = ["http2"]} -microsoft-kiota-abstractions = ">=1.8.0,<2.0.0" -microsoft-kiota-authentication-azure = ">=1.8.0,<2.0.0" -microsoft-kiota-http = ">=1.8.0,<2.0.0" - -[package.extras] -dev = ["bumpver", "isort", "mypy", "pylint", "pytest", "yapf"] - -[[package]] -name = "msgraph-sdk" -version = "1.55.0" -description = "The Microsoft Graph Python SDK" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "msgraph_sdk-1.55.0-py3-none-any.whl", hash = "sha256:c8e68ebc4b88af5111de312e7fa910a4e76ddf48a4534feadb1fb8a411c48cfc"}, - {file = "msgraph_sdk-1.55.0.tar.gz", hash = "sha256:6df691a31954a050d26b8a678968017e157d940fb377f2a8a4e17a9741b98756"}, -] - -[package.dependencies] -azure-identity = ">=1.12.0" -microsoft-kiota-serialization-form = ">=1.8.0,<2.0.0" -microsoft-kiota-serialization-json = ">=1.8.0,<2.0.0" -microsoft-kiota-serialization-multipart = ">=1.8.0,<2.0.0" -microsoft-kiota-serialization-text = ">=1.8.0,<2.0.0" -msgraph_core = ">=1.3.1" - -[package.extras] -dev = ["bumpver", "isort", "mypy", "pylint", "pytest", "yapf"] - -[[package]] -name = "msrest" -version = "0.7.1" -description = "AutoRest swagger generator Python client runtime." -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "msrest-0.7.1-py3-none-any.whl", hash = "sha256:21120a810e1233e5e6cc7fe40b474eeb4ec6f757a15d7cf86702c369f9567c32"}, - {file = "msrest-0.7.1.zip", hash = "sha256:6e7661f46f3afd88b75667b7187a92829924446c7ea1d169be8c4bb7eeb788b9"}, -] - -[package.dependencies] -azure-core = ">=1.24.0" -certifi = ">=2017.4.17" -isodate = ">=0.6.0" -requests = ">=2.16,<3.0" -requests-oauthlib = ">=0.5.0" - -[package.extras] -async = ["aiodns ; python_version >= \"3.5\"", "aiohttp (>=3.0) ; python_version >= \"3.5\""] - -[[package]] -name = "msrestazure" -version = "0.6.4.post1" -description = "AutoRest swagger generator Python client runtime. Azure-specific module." -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "msrestazure-0.6.4.post1-py2.py3-none-any.whl", hash = "sha256:2264493b086c2a0a82ddf5fd87b35b3fffc443819127fed992ac5028354c151e"}, - {file = "msrestazure-0.6.4.post1.tar.gz", hash = "sha256:39842007569e8c77885ace5c46e4bf2a9108fcb09b1e6efdf85b6e2c642b55d4"}, -] - -[package.dependencies] -adal = ">=0.6.0,<2.0.0" -msrest = ">=0.6.0,<2.0.0" -six = "*" - -[[package]] -name = "multidict" -version = "6.7.1" -description = "multidict implementation" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5"}, - {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8"}, - {file = "multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872"}, - {file = "multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991"}, - {file = "multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03"}, - {file = "multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981"}, - {file = "multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6"}, - {file = "multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190"}, - {file = "multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92"}, - {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee"}, - {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2"}, - {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568"}, - {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40"}, - {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962"}, - {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505"}, - {file = "multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122"}, - {file = "multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df"}, - {file = "multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db"}, - {file = "multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d"}, - {file = "multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e"}, - {file = "multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855"}, - {file = "multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3"}, - {file = "multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e"}, - {file = "multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a"}, - {file = "multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8"}, - {file = "multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0"}, - {file = "multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144"}, - {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49"}, - {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71"}, - {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3"}, - {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c"}, - {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0"}, - {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa"}, - {file = "multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a"}, - {file = "multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b"}, - {file = "multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6"}, - {file = "multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172"}, - {file = "multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd"}, - {file = "multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7"}, - {file = "multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53"}, - {file = "multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75"}, - {file = "multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b"}, - {file = "multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733"}, - {file = "multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a"}, - {file = "multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961"}, - {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582"}, - {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e"}, - {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3"}, - {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6"}, - {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a"}, - {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba"}, - {file = "multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511"}, - {file = "multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19"}, - {file = "multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf"}, - {file = "multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23"}, - {file = "multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2"}, - {file = "multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445"}, - {file = "multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177"}, - {file = "multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23"}, - {file = "multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060"}, - {file = "multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d"}, - {file = "multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed"}, - {file = "multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429"}, - {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6"}, - {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9"}, - {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c"}, - {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84"}, - {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d"}, - {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33"}, - {file = "multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3"}, - {file = "multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5"}, - {file = "multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df"}, - {file = "multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1"}, - {file = "multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963"}, - {file = "multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34"}, - {file = "multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65"}, - {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292"}, - {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43"}, - {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca"}, - {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd"}, - {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7"}, - {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3"}, - {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4"}, - {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8"}, - {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c"}, - {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52"}, - {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108"}, - {file = "multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32"}, - {file = "multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8"}, - {file = "multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118"}, - {file = "multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee"}, - {file = "multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2"}, - {file = "multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1"}, - {file = "multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d"}, - {file = "multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31"}, - {file = "multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048"}, - {file = "multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362"}, - {file = "multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37"}, - {file = "multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709"}, - {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0"}, - {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb"}, - {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd"}, - {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601"}, - {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1"}, - {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b"}, - {file = "multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d"}, - {file = "multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f"}, - {file = "multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5"}, - {file = "multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581"}, - {file = "multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a"}, - {file = "multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c"}, - {file = "multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262"}, - {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59"}, - {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889"}, - {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4"}, - {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d"}, - {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609"}, - {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489"}, - {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c"}, - {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e"}, - {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c"}, - {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9"}, - {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2"}, - {file = "multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7"}, - {file = "multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5"}, - {file = "multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2"}, - {file = "multidict-6.7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:65573858d27cdeaca41893185677dc82395159aa28875a8867af66532d413a8f"}, - {file = "multidict-6.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c524c6fb8fc342793708ab111c4dbc90ff9abd568de220432500e47e990c0358"}, - {file = "multidict-6.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aa23b001d968faef416ff70dc0f1ab045517b9b42a90edd3e9bcdb06479e31d5"}, - {file = "multidict-6.7.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6704fa2b7453b2fb121740555fa1ee20cd98c4d011120caf4d2b8d4e7c76eec0"}, - {file = "multidict-6.7.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:121a34e5bfa410cdf2c8c49716de160de3b1dbcd86b49656f5681e4543bcd1a8"}, - {file = "multidict-6.7.1-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:026d264228bcd637d4e060844e39cdc60f86c479e463d49075dedc21b18fbbe0"}, - {file = "multidict-6.7.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e697826df7eb63418ee190fd06ce9f1803593bb4b9517d08c60d9b9a7f69d8f"}, - {file = "multidict-6.7.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb08271280173720e9fea9ede98e5231defcbad90f1624bea26f32ec8a956e2f"}, - {file = "multidict-6.7.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6b3228e1d80af737b72925ce5fb4daf5a335e49cd7ab77ed7b9fdfbf58c526e"}, - {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3943debf0fbb57bdde5901695c11094a9a36723e5c03875f87718ee15ca2f4d2"}, - {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:98c5787b0a0d9a41d9311eae44c3b76e6753def8d8870ab501320efe75a6a5f8"}, - {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:08ccb2a6dc72009093ebe7f3f073e5ec5964cba9a706fa94b1a1484039b87941"}, - {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb351f72c26dc9abe338ca7294661aa22969ad8ffe7ef7d5541d19f368dc854a"}, - {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ac1c665bad8b5d762f5f85ebe4d94130c26965f11de70c708c75671297c776de"}, - {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fa6609d0364f4f6f58351b4659a1f3e0e898ba2a8c5cac04cb2c7bc556b0bc5"}, - {file = "multidict-6.7.1-cp39-cp39-win32.whl", hash = "sha256:6f77ce314a29263e67adadc7e7c1bc699fcb3a305059ab973d038f87caa42ed0"}, - {file = "multidict-6.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:f537b55778cd3cbee430abe3131255d3a78202e0f9ea7ffc6ada893a4bcaeea4"}, - {file = "multidict-6.7.1-cp39-cp39-win_arm64.whl", hash = "sha256:749aa54f578f2e5f439538706a475aa844bfa8ef75854b1401e6e528e4937cf9"}, - {file = "multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56"}, - {file = "multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d"}, -] - -[[package]] -name = "mypy" -version = "1.10.1" -description = "Optional static typing for Python" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "mypy-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e36f229acfe250dc660790840916eb49726c928e8ce10fbdf90715090fe4ae02"}, - {file = "mypy-1.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:51a46974340baaa4145363b9e051812a2446cf583dfaeba124af966fa44593f7"}, - {file = "mypy-1.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:901c89c2d67bba57aaaca91ccdb659aa3a312de67f23b9dfb059727cce2e2e0a"}, - {file = "mypy-1.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0cd62192a4a32b77ceb31272d9e74d23cd88c8060c34d1d3622db3267679a5d9"}, - {file = "mypy-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:a2cbc68cb9e943ac0814c13e2452d2046c2f2b23ff0278e26599224cf164e78d"}, - {file = "mypy-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bd6f629b67bb43dc0d9211ee98b96d8dabc97b1ad38b9b25f5e4c4d7569a0c6a"}, - {file = "mypy-1.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1bbb3a6f5ff319d2b9d40b4080d46cd639abe3516d5a62c070cf0114a457d84"}, - {file = "mypy-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8edd4e9bbbc9d7b79502eb9592cab808585516ae1bcc1446eb9122656c6066f"}, - {file = "mypy-1.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6166a88b15f1759f94a46fa474c7b1b05d134b1b61fca627dd7335454cc9aa6b"}, - {file = "mypy-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:5bb9cd11c01c8606a9d0b83ffa91d0b236a0e91bc4126d9ba9ce62906ada868e"}, - {file = "mypy-1.10.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d8681909f7b44d0b7b86e653ca152d6dff0eb5eb41694e163c6092124f8246d7"}, - {file = "mypy-1.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:378c03f53f10bbdd55ca94e46ec3ba255279706a6aacaecac52ad248f98205d3"}, - {file = "mypy-1.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bacf8f3a3d7d849f40ca6caea5c055122efe70e81480c8328ad29c55c69e93e"}, - {file = "mypy-1.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:701b5f71413f1e9855566a34d6e9d12624e9e0a8818a5704d74d6b0402e66c04"}, - {file = "mypy-1.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c4c2992f6ea46ff7fce0072642cfb62af7a2484efe69017ed8b095f7b39ef31"}, - {file = "mypy-1.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:604282c886497645ffb87b8f35a57ec773a4a2721161e709a4422c1636ddde5c"}, - {file = "mypy-1.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37fd87cab83f09842653f08de066ee68f1182b9b5282e4634cdb4b407266bade"}, - {file = "mypy-1.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8addf6313777dbb92e9564c5d32ec122bf2c6c39d683ea64de6a1fd98b90fe37"}, - {file = "mypy-1.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cc3ca0a244eb9a5249c7c583ad9a7e881aa5d7b73c35652296ddcdb33b2b9c7"}, - {file = "mypy-1.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:1b3a2ffce52cc4dbaeee4df762f20a2905aa171ef157b82192f2e2f368eec05d"}, - {file = "mypy-1.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe85ed6836165d52ae8b88f99527d3d1b2362e0cb90b005409b8bed90e9059b3"}, - {file = "mypy-1.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2ae450d60d7d020d67ab440c6e3fae375809988119817214440033f26ddf7bf"}, - {file = "mypy-1.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6be84c06e6abd72f960ba9a71561c14137a583093ffcf9bbfaf5e613d63fa531"}, - {file = "mypy-1.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2189ff1e39db399f08205e22a797383613ce1cb0cb3b13d8bcf0170e45b96cc3"}, - {file = "mypy-1.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:97a131ee36ac37ce9581f4220311247ab6cba896b4395b9c87af0675a13a755f"}, - {file = "mypy-1.10.1-py3-none-any.whl", hash = "sha256:71d8ac0b906354ebda8ef1673e5fde785936ac1f29ff6987c7483cfbd5a4235a"}, - {file = "mypy-1.10.1.tar.gz", hash = "sha256:1f8f492d7db9e3593ef42d4f115f04e556130f2819ad33ab84551403e97dd4c0"}, -] - -[package.dependencies] -mypy-extensions = ">=1.0.0" -typing-extensions = ">=4.1.0" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -install-types = ["pip"] -mypyc = ["setuptools (>=50)"] -reports = ["lxml"] - -[[package]] -name = "mypy-extensions" -version = "1.1.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, - {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, -] - -[[package]] -name = "narwhals" -version = "2.16.0" -description = "Extremely lightweight compatibility layer between dataframe libraries" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "narwhals-2.16.0-py3-none-any.whl", hash = "sha256:846f1fd7093ac69d63526e50732033e86c30ea0026a44d9b23991010c7d1485d"}, - {file = "narwhals-2.16.0.tar.gz", hash = "sha256:155bb45132b370941ba0396d123cf9ed192bf25f39c4cea726f2da422ca4e145"}, -] - -[package.extras] -cudf = ["cudf-cu12 (>=24.10.0)"] -dask = ["dask[dataframe] (>=2024.8)"] -duckdb = ["duckdb (>=1.1)"] -ibis = ["ibis-framework (>=6.0.0)", "packaging", "pyarrow-hotfix", "rich"] -modin = ["modin"] -pandas = ["pandas (>=1.1.3)"] -polars = ["polars (>=0.20.4)"] -pyarrow = ["pyarrow (>=13.0.0)"] -pyspark = ["pyspark (>=3.5.0)"] -pyspark-connect = ["pyspark[connect] (>=3.5.0)"] -sql = ["duckdb (>=1.1)", "sqlparse"] -sqlframe = ["sqlframe (>=3.22.0,!=3.39.3)"] - -[[package]] -name = "neo4j" -version = "6.1.0" -description = "Neo4j Bolt driver for Python" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "neo4j-6.1.0-py3-none-any.whl", hash = "sha256:3bd93941f3a3559af197031157220af9fd71f4f93a311db687bd69ffa417b67d"}, - {file = "neo4j-6.1.0.tar.gz", hash = "sha256:b5dde8c0d8481e7b6ae3733569d990dd3e5befdc5d452f531ad1884ed3500b84"}, -] - -[package.dependencies] -pytz = "*" - -[package.extras] -numpy = ["numpy (>=1.21.2,<3.0.0)"] -pandas = ["numpy (>=1.21.2,<3.0.0)", "pandas (>=1.1.0,<3.0.0)"] -pyarrow = ["pyarrow (>=6.0.0,<23.0.0)"] - -[[package]] -name = "nest-asyncio" -version = "1.6.0" -description = "Patch asyncio to allow nested event loops" -optional = false -python-versions = ">=3.5" -groups = ["main"] -files = [ - {file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"}, - {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, -] - -[[package]] -name = "nltk" -version = "3.9.4" -description = "Natural Language Toolkit" -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "nltk-3.9.4-py3-none-any.whl", hash = "sha256:f2fa301c3a12718ce4a0e9305c5675299da5ad9e26068218b69d692fda84828f"}, - {file = "nltk-3.9.4.tar.gz", hash = "sha256:ed03bc098a40481310320808b2db712d95d13ca65b27372f8a403949c8b523d0"}, -] - -[package.dependencies] -click = "*" -joblib = "*" -regex = ">=2021.8.3" -tqdm = "*" - -[package.extras] -all = ["matplotlib", "numpy", "pyparsing", "python-crfsuite", "requests", "scikit-learn", "scipy", "twython"] -corenlp = ["requests"] -machine-learning = ["numpy", "python-crfsuite", "scikit-learn", "scipy"] -plot = ["matplotlib"] -tgrep = ["pyparsing"] -twitter = ["twython"] - -[[package]] -name = "numpy" -version = "2.0.2" -description = "Fundamental package for array computing in Python" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece"}, - {file = "numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04"}, - {file = "numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66"}, - {file = "numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b"}, - {file = "numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd"}, - {file = "numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318"}, - {file = "numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8"}, - {file = "numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326"}, - {file = "numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97"}, - {file = "numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a"}, - {file = "numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669"}, - {file = "numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951"}, - {file = "numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9"}, - {file = "numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15"}, - {file = "numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4"}, - {file = "numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c"}, - {file = "numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692"}, - {file = "numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a"}, - {file = "numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c"}, - {file = "numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded"}, - {file = "numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5"}, - {file = "numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729"}, - {file = "numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1"}, - {file = "numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd"}, - {file = "numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d"}, - {file = "numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d"}, - {file = "numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa"}, - {file = "numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385"}, - {file = "numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78"}, -] - -[[package]] -name = "oauthlib" -version = "3.3.1" -description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1"}, - {file = "oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9"}, -] - -[package.extras] -rsa = ["cryptography (>=3.0.0)"] -signals = ["blinker (>=1.4.0)"] -signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] - -[[package]] -name = "oci" -version = "2.169.0" -description = "Oracle Cloud Infrastructure Python SDK" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "oci-2.169.0-py3-none-any.whl", hash = "sha256:c71bb5143f307791082b3e33cc1545c2490a518cfed85ab1948ef5107c36d30b"}, - {file = "oci-2.169.0.tar.gz", hash = "sha256:f3c5fff00b01783b5325ea7b13bf140053ec1e9f41da20bfb9c8a349ee7662fa"}, -] - -[package.dependencies] -certifi = "*" -circuitbreaker = {version = ">=1.3.1,<3.0.0", markers = "python_version >= \"3.7\""} -cryptography = ">=3.2.1,<47.0.0" -pyOpenSSL = ">=17.5.0,<27.0.0" -python-dateutil = ">=2.5.3,<3.0.0" -pytz = ">=2016.10" -urllib3 = {version = ">=2.6.3", markers = "python_version >= \"3.10.0\""} - -[package.extras] -adk = ["docstring-parser (>=0.16) ; python_version >= \"3.10\" and python_version < \"4\"", "mcp (>=1.6.0) ; python_version >= \"3.10\" and python_version < \"4\"", "pydantic (>=2.10.6) ; python_version >= \"3.10\" and python_version < \"4\"", "rich (>=13.9.4) ; python_version >= \"3.10\" and python_version < \"4\""] - -[[package]] -name = "okta" -version = "0.0.4" -description = "Okta client APIs" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "okta-0.0.4.tar.gz", hash = "sha256:53e792c68d3684ff4140b4cb1c02af3821090368f8110fde54c0bdb638449332"}, -] - -[package.dependencies] -python-dateutil = ">=2.4.2" -requests = ">=2.5.3" -six = ">=1.9.0" - -[[package]] -name = "openai" -version = "1.109.1" -description = "The official Python library for the openai API" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "openai-1.109.1-py3-none-any.whl", hash = "sha256:6bcaf57086cf59159b8e27447e4e7dd019db5d29a438072fbd49c290c7e65315"}, - {file = "openai-1.109.1.tar.gz", hash = "sha256:d173ed8dbca665892a6db099b4a2dfac624f94d20a93f46eb0b56aae940ed869"}, -] - -[package.dependencies] -anyio = ">=3.5.0,<5" -distro = ">=1.7.0,<2" -httpx = ">=0.23.0,<1" -jiter = ">=0.4.0,<1" -pydantic = ">=1.9.0,<3" -sniffio = "*" -tqdm = ">4" -typing-extensions = ">=4.11,<5" - -[package.extras] -aiohttp = ["aiohttp", "httpx-aiohttp (>=0.1.8)"] -datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] -realtime = ["websockets (>=13,<16)"] -voice-helpers = ["numpy (>=2.0.2)", "sounddevice (>=0.5.1)"] - -[[package]] -name = "openstacksdk" -version = "4.2.0" -description = "An SDK for building applications to work with OpenStack" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "openstacksdk-4.2.0-py3-none-any.whl", hash = "sha256:238be0fa5d9899872b00787ab38e84f92fd6dc87525fde0965dadcdc12196dc6"}, - {file = "openstacksdk-4.2.0.tar.gz", hash = "sha256:5cb9450dcce8054a2caf89d8be9e55057ddfa219a954e781032241eb29280445"}, -] - -[package.dependencies] -cryptography = ">=2.7" -decorator = ">=4.4.1" -"dogpile.cache" = ">=0.6.5" -iso8601 = ">=0.1.11" -jmespath = ">=0.9.0" -jsonpatch = ">=1.16,<1.20 || >1.20" -keystoneauth1 = ">=3.18.0" -os-service-types = ">=1.7.0" -pbr = ">=2.0.0,<2.1.0 || >2.1.0" -platformdirs = ">=3" -psutil = ">=3.2.2" -PyYAML = ">=3.13" -requestsexceptions = ">=1.2.0" - -[[package]] -name = "opentelemetry-api" -version = "1.39.1" -description = "OpenTelemetry Python API" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950"}, - {file = "opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c"}, -] - -[package.dependencies] -importlib-metadata = ">=6.0,<8.8.0" -typing-extensions = ">=4.5.0" - -[[package]] -name = "opentelemetry-sdk" -version = "1.39.1" -description = "OpenTelemetry Python SDK" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c"}, - {file = "opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6"}, -] - -[package.dependencies] -opentelemetry-api = "1.39.1" -opentelemetry-semantic-conventions = "0.60b1" -typing-extensions = ">=4.5.0" - -[[package]] -name = "opentelemetry-semantic-conventions" -version = "0.60b1" -description = "OpenTelemetry Semantic Conventions" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb"}, - {file = "opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953"}, -] - -[package.dependencies] -opentelemetry-api = "1.39.1" -typing-extensions = ">=4.5.0" - -[[package]] -name = "os-service-types" -version = "1.8.2" -description = "Python library for consuming OpenStack sevice-types-authority data" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "os_service_types-1.8.2-py3-none-any.whl", hash = "sha256:f78890d71814deffabf0ed4358288ec2ced579bc4d0bb87a79ae806cbb4deb6e"}, - {file = "os_service_types-1.8.2.tar.gz", hash = "sha256:ab7648d7232849943196e1bb00a30e2e25e600fa3b57bb241d15b7f521b5b575"}, -] - -[package.dependencies] -pbr = ">=2.0.0,<2.1.0 || >2.1.0" -typing-extensions = ">=4.1.0" - -[[package]] -name = "packageurl-python" -version = "0.17.6" -description = "A purl aka. Package URL parser and builder" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "packageurl_python-0.17.6-py3-none-any.whl", hash = "sha256:31a85c2717bc41dd818f3c62908685ff9eebcb68588213745b14a6ee9e7df7c9"}, - {file = "packageurl_python-0.17.6.tar.gz", hash = "sha256:1252ce3a102372ca6f86eb968e16f9014c4ba511c5c37d95a7f023e2ca6e5c25"}, -] - -[package.extras] -build = ["setuptools", "wheel"] -lint = ["black", "isort", "mypy"] -sqlalchemy = ["sqlalchemy (>=2.0.0)"] -test = ["pytest"] - -[[package]] -name = "packaging" -version = "26.0" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}, - {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, -] - -[[package]] -name = "pagerduty" -version = "6.1.0" -description = "Clients for PagerDuty's Public APIs" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "pagerduty-6.1.0-py3-none-any.whl", hash = "sha256:ca4954b917cb8e92f83e6b4e18d0f81fdaa73768edb7ad6e859edcc8f950f4eb"}, - {file = "pagerduty-6.1.0.tar.gz", hash = "sha256:84dfba74f68142c4a71c88af4858f1eb8671e7bc564bc133ac41c59daa7b54f8"}, -] - -[package.dependencies] -httpx = "*" - -[[package]] -name = "pandas" -version = "2.2.3" -description = "Powerful data structures for data analysis, time series, and statistics" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5"}, - {file = "pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348"}, - {file = "pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed"}, - {file = "pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57"}, - {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42"}, - {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f"}, - {file = "pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645"}, - {file = "pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039"}, - {file = "pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd"}, - {file = "pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698"}, - {file = "pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc"}, - {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3"}, - {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32"}, - {file = "pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5"}, - {file = "pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9"}, - {file = "pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4"}, - {file = "pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3"}, - {file = "pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319"}, - {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8"}, - {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a"}, - {file = "pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13"}, - {file = "pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015"}, - {file = "pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28"}, - {file = "pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0"}, - {file = "pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24"}, - {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659"}, - {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb"}, - {file = "pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d"}, - {file = "pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468"}, - {file = "pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18"}, - {file = "pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2"}, - {file = "pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4"}, - {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d"}, - {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a"}, - {file = "pandas-2.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc6b93f9b966093cb0fd62ff1a7e4c09e6d546ad7c1de191767baffc57628f39"}, - {file = "pandas-2.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5dbca4c1acd72e8eeef4753eeca07de9b1db4f398669d5994086f788a5d7cc30"}, - {file = "pandas-2.2.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8cd6d7cc958a3910f934ea8dbdf17b2364827bb4dafc38ce6eef6bb3d65ff09c"}, - {file = "pandas-2.2.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99df71520d25fade9db7c1076ac94eb994f4d2673ef2aa2e86ee039b6746d20c"}, - {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31d0ced62d4ea3e231a9f228366919a5ea0b07440d9d4dac345376fd8e1477ea"}, - {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7eee9e7cea6adf3e3d24e304ac6b8300646e2a5d1cd3a3c2abed9101b0846761"}, - {file = "pandas-2.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:4850ba03528b6dd51d6c5d273c46f183f39a9baf3f0143e566b89450965b105e"}, - {file = "pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667"}, -] - -[package.dependencies] -numpy = [ - {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, - {version = ">=1.23.2", markers = "python_version == \"3.11\""}, -] -python-dateutil = ">=2.8.2" -pytz = ">=2020.1" -tzdata = ">=2022.7" - -[package.extras] -all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"] -aws = ["s3fs (>=2022.11.0)"] -clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"] -compression = ["zstandard (>=0.19.0)"] -computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"] -consortium-standard = ["dataframe-api-compat (>=0.1.7)"] -excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"] -feather = ["pyarrow (>=10.0.1)"] -fss = ["fsspec (>=2022.11.0)"] -gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"] -hdf5 = ["tables (>=3.8.0)"] -html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"] -mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"] -output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"] -parquet = ["pyarrow (>=10.0.1)"] -performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"] -plot = ["matplotlib (>=3.6.3)"] -postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"] -pyarrow = ["pyarrow (>=10.0.1)"] -spss = ["pyreadstat (>=1.2.0)"] -sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"] -test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] -xml = ["lxml (>=4.9.2)"] - -[[package]] -name = "pbr" -version = "7.0.3" -description = "Python Build Reasonableness" -optional = false -python-versions = ">=2.6" -groups = ["main"] -files = [ - {file = "pbr-7.0.3-py2.py3-none-any.whl", hash = "sha256:ff223894eb1cd271a98076b13d3badff3bb36c424074d26334cd25aebeecea6b"}, - {file = "pbr-7.0.3.tar.gz", hash = "sha256:b46004ec30a5324672683ec848aed9e8fc500b0d261d40a3229c2d2bbfcedc29"}, -] - -[package.dependencies] -setuptools = "*" - -[[package]] -name = "pillow" -version = "12.1.1" -description = "Python Imaging Library (fork)" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "pillow-12.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f1625b72740fdda5d77b4def688eb8fd6490975d06b909fd19f13f391e077e0"}, - {file = "pillow-12.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:178aa072084bd88ec759052feca8e56cbb14a60b39322b99a049e58090479713"}, - {file = "pillow-12.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b66e95d05ba806247aaa1561f080abc7975daf715c30780ff92a20e4ec546e1b"}, - {file = "pillow-12.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89c7e895002bbe49cdc5426150377cbbc04767d7547ed145473f496dfa40408b"}, - {file = "pillow-12.1.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a5cbdcddad0af3da87cb16b60d23648bc3b51967eb07223e9fed77a82b457c4"}, - {file = "pillow-12.1.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f51079765661884a486727f0729d29054242f74b46186026582b4e4769918e4"}, - {file = "pillow-12.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:99c1506ea77c11531d75e3a412832a13a71c7ebc8192ab9e4b2e355555920e3e"}, - {file = "pillow-12.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:36341d06738a9f66c8287cf8b876d24b18db9bd8740fa0672c74e259ad408cff"}, - {file = "pillow-12.1.1-cp310-cp310-win32.whl", hash = "sha256:6c52f062424c523d6c4db85518774cc3d50f5539dd6eed32b8f6229b26f24d40"}, - {file = "pillow-12.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:c6008de247150668a705a6338156efb92334113421ceecf7438a12c9a12dab23"}, - {file = "pillow-12.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:1a9b0ee305220b392e1124a764ee4265bd063e54a751a6b62eff69992f457fa9"}, - {file = "pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32"}, - {file = "pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38"}, - {file = "pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5"}, - {file = "pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090"}, - {file = "pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af"}, - {file = "pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b"}, - {file = "pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5"}, - {file = "pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d"}, - {file = "pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c"}, - {file = "pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563"}, - {file = "pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80"}, - {file = "pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052"}, - {file = "pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984"}, - {file = "pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79"}, - {file = "pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293"}, - {file = "pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397"}, - {file = "pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0"}, - {file = "pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3"}, - {file = "pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35"}, - {file = "pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a"}, - {file = "pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6"}, - {file = "pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523"}, - {file = "pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e"}, - {file = "pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9"}, - {file = "pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6"}, - {file = "pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60"}, - {file = "pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2"}, - {file = "pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850"}, - {file = "pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289"}, - {file = "pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e"}, - {file = "pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717"}, - {file = "pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a"}, - {file = "pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029"}, - {file = "pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b"}, - {file = "pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1"}, - {file = "pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a"}, - {file = "pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da"}, - {file = "pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc"}, - {file = "pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c"}, - {file = "pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8"}, - {file = "pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20"}, - {file = "pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13"}, - {file = "pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf"}, - {file = "pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524"}, - {file = "pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986"}, - {file = "pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c"}, - {file = "pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3"}, - {file = "pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af"}, - {file = "pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f"}, - {file = "pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642"}, - {file = "pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd"}, - {file = "pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202"}, - {file = "pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f"}, - {file = "pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f"}, - {file = "pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f"}, - {file = "pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e"}, - {file = "pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0"}, - {file = "pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb"}, - {file = "pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f"}, - {file = "pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15"}, - {file = "pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f"}, - {file = "pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8"}, - {file = "pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9"}, - {file = "pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60"}, - {file = "pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7"}, - {file = "pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f"}, - {file = "pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586"}, - {file = "pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce"}, - {file = "pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8"}, - {file = "pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36"}, - {file = "pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b"}, - {file = "pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334"}, - {file = "pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f"}, - {file = "pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9"}, - {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e"}, - {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9"}, - {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3"}, - {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735"}, - {file = "pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e"}, - {file = "pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4"}, -] - -[package.extras] -docs = ["furo", "olefile", "sphinx (>=8.2)", "sphinx-autobuild", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] -fpx = ["olefile"] -mic = ["olefile"] -test-arrow = ["arro3-compute", "arro3-core", "nanoarrow", "pyarrow"] -tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma (>=5)", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "trove-classifiers (>=2024.10.12)"] -xmp = ["defusedxml"] - -[[package]] -name = "pkginfo" -version = "1.12.1.2" -description = "Query metadata from sdists / bdists / installed packages." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "pkginfo-1.12.1.2-py3-none-any.whl", hash = "sha256:c783ac885519cab2c34927ccfa6bf64b5a704d7c69afaea583dd9b7afe969343"}, - {file = "pkginfo-1.12.1.2.tar.gz", hash = "sha256:5cd957824ac36f140260964eba3c6be6442a8359b8c48f4adf90210f33a04b7b"}, -] - -[package.extras] -testing = ["pytest", "pytest-cov", "wheel"] - -[[package]] -name = "platformdirs" -version = "4.5.1" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -optional = false -python-versions = ">=3.10" -groups = ["main", "dev"] -files = [ - {file = "platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31"}, - {file = "platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda"}, -] - -[package.extras] -docs = ["furo (>=2025.9.25)", "proselint (>=0.14)", "sphinx (>=8.2.3)", "sphinx-autodoc-typehints (>=3.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.4.2)", "pytest-cov (>=7)", "pytest-mock (>=3.15.1)"] -type = ["mypy (>=1.18.2)"] - -[[package]] -name = "plotly" -version = "6.5.2" -description = "An open-source interactive data visualization library for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "plotly-6.5.2-py3-none-any.whl", hash = "sha256:91757653bd9c550eeea2fa2404dba6b85d1e366d54804c340b2c874e5a7eb4a4"}, - {file = "plotly-6.5.2.tar.gz", hash = "sha256:7478555be0198562d1435dee4c308268187553cc15516a2f4dd034453699e393"}, -] - -[package.dependencies] -narwhals = ">=1.15.1" -packaging = "*" - -[package.extras] -dev = ["plotly[dev-optional]"] -dev-build = ["build", "jupyter", "plotly[dev-core]"] -dev-core = ["pytest", "requests", "ruff (==0.11.12)"] -dev-optional = ["anywidget", "colorcet", "fiona (<=1.9.6) ; python_version <= \"3.8\"", "geopandas", "inflect", "numpy", "orjson", "pandas", "pdfrw", "pillow", "plotly-geo", "plotly[dev-build]", "plotly[kaleido]", "polars[timezone]", "pyarrow", "pyshp", "pytz", "scikit-image", "scipy", "shapely", "statsmodels", "vaex ; python_version <= \"3.9\"", "xarray"] -express = ["numpy"] -kaleido = ["kaleido (>=1.1.0)"] - -[[package]] -name = "pluggy" -version = "1.6.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, - {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["coverage", "pytest", "pytest-benchmark"] - -[[package]] -name = "policyuniverse" -version = "1.5.1.20231109" -description = "Parse and Process AWS IAM Policies, Statements, ARNs, and wildcards." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "policyuniverse-1.5.1.20231109-py2.py3-none-any.whl", hash = "sha256:0b0ece0ee8285af31fc39ce09c82a551ca62e62bc2842e23952503bccb973321"}, - {file = "policyuniverse-1.5.1.20231109.tar.gz", hash = "sha256:74e56d410560915c2c5132e361b0130e4bffe312a2f45230eac50d7c094bc40a"}, -] - -[package.extras] -dev = ["black", "pre-commit"] -tests = ["bandit", "coveralls", "pytest"] - -[[package]] -name = "portalocker" -version = "2.10.1" -description = "Wraps the portalocker recipe for easy usage" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "portalocker-2.10.1-py3-none-any.whl", hash = "sha256:53a5984ebc86a025552264b459b46a2086e269b21823cb572f8f28ee759e45bf"}, - {file = "portalocker-2.10.1.tar.gz", hash = "sha256:ef1bf844e878ab08aee7e40184156e1151f228f103aa5c6bd0724cc330960f8f"}, -] - -[package.dependencies] -pywin32 = {version = ">=226", markers = "platform_system == \"Windows\""} - -[package.extras] -docs = ["sphinx (>=1.7.1)"] -redis = ["redis"] -tests = ["pytest (>=5.4.1)", "pytest-cov (>=2.8.1)", "pytest-mypy (>=0.8.0)", "pytest-timeout (>=2.1.0)", "redis", "sphinx (>=6.0.0)", "types-redis"] - -[[package]] -name = "prek" -version = "0.3.9" -description = "A Git hook manager written in Rust, designed as a drop-in alternative to pre-commit." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "prek-0.3.9-py3-none-linux_armv6l.whl", hash = "sha256:3ed793d51bfaa27bddb64d525d7acb77a7c8644f549412d82252e3eb0b88aad8"}, - {file = "prek-0.3.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:399c58400c0bd0b82a93a3c09dc1bfd88d8d0cfb242d414d2ed247187b06ead1"}, - {file = "prek-0.3.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e2ea1ffb124e92f081b8e2ca5b5a623a733efb3be0c5b1f4b7ffe2ee17d1f20c"}, - {file = "prek-0.3.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:aaf639f95b7301639298311d8d44aad0d0b4864e9736083ad3c71ce9765d37ab"}, - {file = "prek-0.3.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff104863b187fa443ea8451ca55d51e2c6e94f99f00d88784b5c3c4c623f1ebe"}, - {file = "prek-0.3.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:039ecaf87c63a3e67cca645ebd5bc5eb6aafa6c9d929e9a27b2921e7849d7ef9"}, - {file = "prek-0.3.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3bde2a3d045705095983c7f78ba04f72a7565fe1c2b4e85f5628502a254754ff"}, - {file = "prek-0.3.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28a0960a21543563e2c8e19aaad176cc8423a87aac3c914d0f313030d7a9244a"}, - {file = "prek-0.3.9-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:0dfb5d5171d7523271909246ee306b4dc3d5b63752e7dd7c7e8a8908fc9490d1"}, - {file = "prek-0.3.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:82b791bd36c1430c84d3ae7220a85152babc7eaf00f70adcb961bd594e756ba3"}, - {file = "prek-0.3.9-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:6eac6d2f736b041118f053a1487abed468a70dd85a8688eaf87bb42d3dcecf20"}, - {file = "prek-0.3.9-py3-none-musllinux_1_1_i686.whl", hash = "sha256:5517e46e761367a3759b3168eabc120840ffbca9dfbc53187167298a98f87dc4"}, - {file = "prek-0.3.9-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:92024778cf78683ca32687bb249ab6a7d5c33887b5ee1d1a9f6d0c14228f4cf3"}, - {file = "prek-0.3.9-py3-none-win32.whl", hash = "sha256:7f89c55e5f480f5d073769e319924ad69d4bf9f98c5cb46a83082e26e634c958"}, - {file = "prek-0.3.9-py3-none-win_amd64.whl", hash = "sha256:7722f3372eaa83b147e70a43cb7b9fe2128c13d0c78d8a1cdbf2a8ec2ee071eb"}, - {file = "prek-0.3.9-py3-none-win_arm64.whl", hash = "sha256:0bced6278d6cc8a4b46048979e36bc9da034611dc8facd77ab123177b833a929"}, - {file = "prek-0.3.9.tar.gz", hash = "sha256:f82b92d81f42f1f90a47f5fbbf492373e25ef1f790080215b2722dd6da66510e"}, -] - -[[package]] -name = "prompt-toolkit" -version = "3.0.52" -description = "Library for building powerful interactive command lines in Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955"}, - {file = "prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855"}, -] - -[package.dependencies] -wcwidth = "*" - -[[package]] -name = "propcache" -version = "0.4.1" -description = "Accelerated property cache" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db"}, - {file = "propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8"}, - {file = "propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925"}, - {file = "propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21"}, - {file = "propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5"}, - {file = "propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db"}, - {file = "propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7"}, - {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4"}, - {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60"}, - {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f"}, - {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900"}, - {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c"}, - {file = "propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb"}, - {file = "propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37"}, - {file = "propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581"}, - {file = "propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf"}, - {file = "propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5"}, - {file = "propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e"}, - {file = "propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566"}, - {file = "propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165"}, - {file = "propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc"}, - {file = "propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48"}, - {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570"}, - {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85"}, - {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e"}, - {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757"}, - {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f"}, - {file = "propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1"}, - {file = "propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6"}, - {file = "propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239"}, - {file = "propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2"}, - {file = "propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403"}, - {file = "propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207"}, - {file = "propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72"}, - {file = "propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367"}, - {file = "propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4"}, - {file = "propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf"}, - {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3"}, - {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778"}, - {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6"}, - {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9"}, - {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75"}, - {file = "propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8"}, - {file = "propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db"}, - {file = "propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1"}, - {file = "propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf"}, - {file = "propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311"}, - {file = "propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74"}, - {file = "propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe"}, - {file = "propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af"}, - {file = "propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c"}, - {file = "propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f"}, - {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1"}, - {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24"}, - {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa"}, - {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61"}, - {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66"}, - {file = "propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81"}, - {file = "propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e"}, - {file = "propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1"}, - {file = "propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b"}, - {file = "propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566"}, - {file = "propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835"}, - {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e"}, - {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859"}, - {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b"}, - {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0"}, - {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af"}, - {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393"}, - {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874"}, - {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7"}, - {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1"}, - {file = "propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717"}, - {file = "propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37"}, - {file = "propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a"}, - {file = "propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12"}, - {file = "propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c"}, - {file = "propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded"}, - {file = "propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641"}, - {file = "propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4"}, - {file = "propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44"}, - {file = "propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d"}, - {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b"}, - {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e"}, - {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f"}, - {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49"}, - {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144"}, - {file = "propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f"}, - {file = "propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153"}, - {file = "propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992"}, - {file = "propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f"}, - {file = "propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393"}, - {file = "propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0"}, - {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a"}, - {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be"}, - {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc"}, - {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a"}, - {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89"}, - {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726"}, - {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367"}, - {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36"}, - {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455"}, - {file = "propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85"}, - {file = "propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1"}, - {file = "propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9"}, - {file = "propcache-0.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff"}, - {file = "propcache-0.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb"}, - {file = "propcache-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac"}, - {file = "propcache-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888"}, - {file = "propcache-0.4.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc"}, - {file = "propcache-0.4.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a"}, - {file = "propcache-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88"}, - {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00"}, - {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0"}, - {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e"}, - {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781"}, - {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183"}, - {file = "propcache-0.4.1-cp39-cp39-win32.whl", hash = "sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19"}, - {file = "propcache-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f"}, - {file = "propcache-0.4.1-cp39-cp39-win_arm64.whl", hash = "sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938"}, - {file = "propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237"}, - {file = "propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d"}, -] - -[[package]] -name = "proto-plus" -version = "1.27.0" -description = "Beautiful, Pythonic protocol buffers" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "proto_plus-1.27.0-py3-none-any.whl", hash = "sha256:1baa7f81cf0f8acb8bc1f6d085008ba4171eaf669629d1b6d1673b21ed1c0a82"}, - {file = "proto_plus-1.27.0.tar.gz", hash = "sha256:873af56dd0d7e91836aee871e5799e1c6f1bda86ac9a983e0bb9f0c266a568c4"}, -] - -[package.dependencies] -protobuf = ">=3.19.0,<7.0.0" - -[package.extras] -testing = ["google-api-core (>=1.31.5)"] - -[[package]] -name = "protobuf" -version = "6.33.5" -description = "" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b"}, - {file = "protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c"}, - {file = "protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5"}, - {file = "protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190"}, - {file = "protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd"}, - {file = "protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0"}, - {file = "protobuf-6.33.5-cp39-cp39-win32.whl", hash = "sha256:a3157e62729aafb8df6da2c03aa5c0937c7266c626ce11a278b6eb7963c4e37c"}, - {file = "protobuf-6.33.5-cp39-cp39-win_amd64.whl", hash = "sha256:8f04fa32763dcdb4973d537d6b54e615cc61108c7cb38fe59310c3192d29510a"}, - {file = "protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02"}, - {file = "protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c"}, -] - -[[package]] -name = "prowler" -version = "5.25.0" -description = "Prowler is an Open Source security tool to perform AWS, GCP and Azure security best practices assessments, audits, incident response, continuous monitoring, hardening and forensics readiness. It contains hundreds of controls covering CIS, NIST 800, NIST CSF, CISA, RBI, FedRAMP, PCI-DSS, GDPR, HIPAA, FFIEC, SOC2, GXP, AWS Well-Architected Framework Security Pillar, AWS Foundational Technical Review (FTR), ENS (Spanish National Security Scheme) and your custom security frameworks." -optional = false -python-versions = ">=3.10,<3.13" -groups = ["main"] -files = [] -develop = false - -[package.dependencies] -alibabacloud_actiontrail20200706 = "2.4.1" -alibabacloud_credentials = "1.0.3" -alibabacloud_cs20151215 = "6.1.0" -alibabacloud_ecs20140526 = "7.2.5" -alibabacloud-gateway-oss-util = "0.0.3" -alibabacloud_oss20190517 = "1.0.6" -alibabacloud_ram20150501 = "1.2.0" -alibabacloud-rds20140815 = "12.0.0" -alibabacloud_sas20181203 = "6.1.0" -alibabacloud-sls20201230 = "5.9.0" -alibabacloud_sts20150401 = "1.1.6" -alibabacloud_tea_openapi = "0.4.4" -alibabacloud_vpc20160428 = "6.13.0" -alive-progress = "3.3.0" -awsipranges = "0.3.3" -azure-identity = "1.21.0" -azure-keyvault-keys = "4.10.0" -azure-mgmt-apimanagement = "5.0.0" -azure-mgmt-applicationinsights = "4.1.0" -azure-mgmt-authorization = "4.0.0" -azure-mgmt-compute = "34.0.0" -azure-mgmt-containerregistry = "12.0.0" -azure-mgmt-containerservice = "34.1.0" -azure-mgmt-cosmosdb = "9.7.0" -azure-mgmt-databricks = "2.0.0" -azure-mgmt-keyvault = "10.3.1" -azure-mgmt-loganalytics = "12.0.0" -azure-mgmt-monitor = "6.0.2" -azure-mgmt-network = "28.1.0" -azure-mgmt-postgresqlflexibleservers = "1.1.0" -azure-mgmt-rdbms = "10.1.0" -azure-mgmt-recoveryservices = "3.1.0" -azure-mgmt-recoveryservicesbackup = "9.2.0" -azure-mgmt-resource = "24.0.0" -azure-mgmt-search = "9.1.0" -azure-mgmt-security = "7.0.0" -azure-mgmt-sql = "3.0.1" -azure-mgmt-storage = "22.1.1" -azure-mgmt-subscription = "3.1.1" -azure-mgmt-web = "8.0.0" -azure-monitor-query = "2.0.0" -azure-storage-blob = "12.24.1" -boto3 = "1.40.61" -botocore = "1.40.61" -cloudflare = "4.3.1" -colorama = "0.4.6" -cryptography = "46.0.6" -dash = "3.1.1" -dash-bootstrap-components = "2.0.3" -defusedxml = "0.7.1" -detect-secrets = "1.5.0" -dulwich = "0.23.0" -google-api-python-client = "2.163.0" -google-auth-httplib2 = "0.2.0" -h2 = "4.3.0" -jsonschema = "4.23.0" -kubernetes = "32.0.1" -markdown = "3.10.2" -microsoft-kiota-abstractions = "1.9.2" -msgraph-sdk = "1.55.0" -numpy = "2.0.2" -oci = "2.169.0" -openstacksdk = "4.2.0" -pandas = "2.2.3" -py-iam-expand = "0.1.0" -py-ocsf-models = "0.8.1" -pydantic = "2.12.5" -pygithub = "2.8.0" -python-dateutil = "2.9.0.post0" -pytz = "2025.1" -schema = "0.7.5" -shodan = "1.31.0" -slack-sdk = "3.39.0" -tabulate = "0.9.0" -tzlocal = "5.3.1" -uuid6 = "2024.7.10" - -[package.source] -type = "git" -url = "https://github.com/prowler-cloud/prowler.git" -reference = "master" -resolved_reference = "ca29e354b622198ff6a70e2ea5eb04e4a44a0903" - -[[package]] -name = "psutil" -version = "7.2.2" -description = "Cross-platform lib for process and system monitoring." -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b"}, - {file = "psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea"}, - {file = "psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63"}, - {file = "psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312"}, - {file = "psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b"}, - {file = "psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9"}, - {file = "psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00"}, - {file = "psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9"}, - {file = "psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a"}, - {file = "psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf"}, - {file = "psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1"}, - {file = "psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841"}, - {file = "psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486"}, - {file = "psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979"}, - {file = "psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9"}, - {file = "psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e"}, - {file = "psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8"}, - {file = "psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc"}, - {file = "psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988"}, - {file = "psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee"}, - {file = "psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372"}, -] - -[package.extras] -dev = ["abi3audit", "black", "check-manifest", "colorama ; os_name == \"nt\"", "coverage", "packaging", "psleak", "pylint", "pyperf", "pypinfo", "pyreadline3 ; os_name == \"nt\"", "pytest", "pytest-cov", "pytest-instafail", "pytest-xdist", "pywin32 ; os_name == \"nt\" and implementation_name != \"pypy\"", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "validate-pyproject[all]", "virtualenv", "vulture", "wheel", "wheel ; os_name == \"nt\" and implementation_name != \"pypy\"", "wmi ; os_name == \"nt\" and implementation_name != \"pypy\""] -test = ["psleak", "pytest", "pytest-instafail", "pytest-xdist", "pywin32 ; os_name == \"nt\" and implementation_name != \"pypy\"", "setuptools", "wheel ; os_name == \"nt\" and implementation_name != \"pypy\"", "wmi ; os_name == \"nt\" and implementation_name != \"pypy\""] - -[[package]] -name = "psycopg2-binary" -version = "2.9.9" -description = "psycopg2 - Python-PostgreSQL Database Adapter" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "psycopg2-binary-2.9.9.tar.gz", hash = "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-win32.whl", hash = "sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-win32.whl", hash = "sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-win32.whl", hash = "sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957"}, -] - -[[package]] -name = "py-deviceid" -version = "0.1.1" -description = "A simple library to get or create a unique device id for a device in Python." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "py_deviceid-0.1.1-py3-none-any.whl", hash = "sha256:c0e32815e87a08087a0811c18f4402ee88b28a321f997753d75ecdaab570321b"}, - {file = "py_deviceid-0.1.1.tar.gz", hash = "sha256:c3e7577ada23666e7f39e69370dfdaa76fe9de79c02635376d6aa0229bfa30e3"}, -] - -[[package]] -name = "py-iam-expand" -version = "0.1.0" -description = "This is a Python package to expand and deobfuscate IAM policies." -optional = false -python-versions = "<3.14,>3.9.1" -groups = ["main"] -files = [ - {file = "py_iam_expand-0.1.0-py3-none-any.whl", hash = "sha256:b845ce7b50ac895b02b4f338e09c62a68ea51849794f76e189b02009bd388510"}, - {file = "py_iam_expand-0.1.0.tar.gz", hash = "sha256:5a2884dc267ac59a02c3a80fefc0b34c309dac681baa0f87c436067c6cf53a96"}, -] - -[package.dependencies] -iamdata = ">=0.1.202504091" - -[[package]] -name = "py-ocsf-models" -version = "0.8.1" -description = "This is a Python implementation of the OCSF models. The models are used to represent the data of the OCSF Schema defined in https://schema.ocsf.io/." -optional = false -python-versions = "<3.15,>3.9.1" -groups = ["main"] -files = [ - {file = "py_ocsf_models-0.8.1-py3-none-any.whl", hash = "sha256:061eb446c4171534c09a8b37f5a9d2a2fe9f87c5db32edbd1182446bc5fd097e"}, - {file = "py_ocsf_models-0.8.1.tar.gz", hash = "sha256:c9045237857f951e073c9f9d1f57954c90d86875b469260725292d47f7a7d73c"}, -] - -[package.dependencies] -cryptography = ">=44.0.3,<47" -email-validator = "2.2.0" -pydantic = ">=2.12.0,<3.0.0" - -[[package]] -name = "pyasn1" -version = "0.6.3" -description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde"}, - {file = "pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf"}, -] - -[[package]] -name = "pyasn1-modules" -version = "0.4.2" -description = "A collection of ASN.1-based protocols modules" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a"}, - {file = "pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6"}, -] - -[package.dependencies] -pyasn1 = ">=0.6.1,<0.7.0" - -[[package]] -name = "pycodestyle" -version = "2.14.0" -description = "Python style guide checker" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d"}, - {file = "pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783"}, -] - -[[package]] -name = "pycparser" -version = "3.0" -description = "C parser in Python" -optional = false -python-versions = ">=3.10" -groups = ["main", "dev"] -markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\"" -files = [ - {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}, - {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}, -] - -[[package]] -name = "pydantic" -version = "2.12.5" -description = "Data validation using Python type hints" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d"}, - {file = "pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49"}, -] - -[package.dependencies] -annotated-types = ">=0.6.0" -pydantic-core = "2.41.5" -typing-extensions = ">=4.14.1" -typing-inspection = ">=0.4.2" - -[package.extras] -email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] - -[[package]] -name = "pydantic-core" -version = "2.41.5" -description = "Core functionality for Pydantic validation and serialization" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146"}, - {file = "pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c"}, - {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2"}, - {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556"}, - {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49"}, - {file = "pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba"}, - {file = "pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9"}, - {file = "pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6"}, - {file = "pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284"}, - {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594"}, - {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e"}, - {file = "pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe"}, - {file = "pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f"}, - {file = "pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7"}, - {file = "pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5"}, - {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c"}, - {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294"}, - {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1"}, - {file = "pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d"}, - {file = "pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815"}, - {file = "pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3"}, - {file = "pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9"}, - {file = "pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d"}, - {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740"}, - {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e"}, - {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858"}, - {file = "pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36"}, - {file = "pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11"}, - {file = "pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd"}, - {file = "pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a"}, - {file = "pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553"}, - {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90"}, - {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07"}, - {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb"}, - {file = "pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23"}, - {file = "pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf"}, - {file = "pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008"}, - {file = "pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf"}, - {file = "pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425"}, - {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504"}, - {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5"}, - {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3"}, - {file = "pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460"}, - {file = "pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51"}, - {file = "pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e"}, -] - -[package.dependencies] -typing-extensions = ">=4.14.1" - -[[package]] -name = "pygithub" -version = "2.8.0" -description = "Use the full Github API v3" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "pygithub-2.8.0-py3-none-any.whl", hash = "sha256:11a3473c1c2f1c39c525d0ee8c559f369c6d46c272cb7321c9b0cabc7aa1ce7d"}, - {file = "pygithub-2.8.0.tar.gz", hash = "sha256:72f5f2677d86bc3a8843aa720c6ce4c1c42fb7500243b136e3d5e14ddb5c3386"}, -] - -[package.dependencies] -pyjwt = {version = ">=2.4.0", extras = ["crypto"]} -pynacl = ">=1.4.0" -requests = ">=2.14.0" -typing-extensions = ">=4.5.0" -urllib3 = ">=1.26.0" - -[[package]] -name = "pygments" -version = "2.20.0" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, - {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, -] - -[package.extras] -windows-terminal = ["colorama (>=0.4.6)"] - -[[package]] -name = "pyjwt" -version = "2.12.1" -description = "JSON Web Token implementation in Python" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c"}, - {file = "pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b"}, -] - -[package.dependencies] -cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""} - -[package.extras] -crypto = ["cryptography (>=3.4.0)"] -dev = ["coverage[toml] (==7.10.7)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=8.4.2,<9.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] -docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] -tests = ["coverage[toml] (==7.10.7)", "pytest (>=8.4.2,<9.0.0)"] - -[[package]] -name = "pylint" -version = "3.2.5" -description = "python code static checker" -optional = false -python-versions = ">=3.8.0" -groups = ["dev"] -files = [ - {file = "pylint-3.2.5-py3-none-any.whl", hash = "sha256:32cd6c042b5004b8e857d727708720c54a676d1e22917cf1a2df9b4d4868abd6"}, - {file = "pylint-3.2.5.tar.gz", hash = "sha256:e9b7171e242dcc6ebd0aaa7540481d1a72860748a0a7816b8fe6cf6c80a6fe7e"}, -] - -[package.dependencies] -astroid = ">=3.2.2,<=3.3.0.dev0" -colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -dill = [ - {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, - {version = ">=0.3.6", markers = "python_version == \"3.11\""}, -] -isort = ">=4.2.5,<5.13.0 || >5.13.0,<6" -mccabe = ">=0.6,<0.8" -platformdirs = ">=2.2.0" -tomlkit = ">=0.10.1" - -[package.extras] -spelling = ["pyenchant (>=3.2,<4.0)"] -testutils = ["gitpython (>3)"] - -[[package]] -name = "pymsalruntime" -version = "0.18.1" -description = "The MSALRuntime Python Interop Package" -optional = false -python-versions = ">=3.6" -groups = ["main"] -markers = "sys_platform == \"win32\" and (platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\")" -files = [ - {file = "pymsalruntime-0.18.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:0c22e2e83faa10de422bbfaacc1bb2887c9025ee8a53f0fc2e4f7db01c4a7b66"}, - {file = "pymsalruntime-0.18.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:8ce2944a0f944833d047bb121396091e00287e2b6373716106da86ea99abf379"}, - {file = "pymsalruntime-0.18.1-cp310-cp310-manylinux_2_35_x86_64.whl", hash = "sha256:9f7945ae0ee78357e9ca87d381f1c19763629a7197391ae7f84f4967a9f06e5b"}, - {file = "pymsalruntime-0.18.1-cp310-cp310-win32.whl", hash = "sha256:10020abdfc34bbbf3414b86359de551d2d8bc7c241bc38c59a2468c4d49f21d5"}, - {file = "pymsalruntime-0.18.1-cp310-cp310-win_amd64.whl", hash = "sha256:f9aec2f44470d71feae35b611d1d8f15a549d96446e4f60e1ca1fb71856fffed"}, - {file = "pymsalruntime-0.18.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:e9320fb187fe1298d2165fa248af00907ca15d3a903a1d35fed86f6bc20b5880"}, - {file = "pymsalruntime-0.18.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:9b2cecf3a570b7812d2007764df6dfbc27fca401a0d74532d5403aa20a9ef380"}, - {file = "pymsalruntime-0.18.1-cp311-cp311-manylinux_2_35_x86_64.whl", hash = "sha256:6f66fd99668abc3d4b8d93a9eb80c75178dc63186c79e6dbe133427b279835e0"}, - {file = "pymsalruntime-0.18.1-cp311-cp311-win32.whl", hash = "sha256:74416947b1071054f3258cac3448a7adf708888727bf283267df2bb27f0998f1"}, - {file = "pymsalruntime-0.18.1-cp311-cp311-win_amd64.whl", hash = "sha256:beb926655aae3367b7e4bda2baad86f9271beefee1121f71642da0ed4de37fd2"}, - {file = "pymsalruntime-0.18.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a6c07651cf4e07690d1b022da0977f56820ef553ac6dcbf4c9e68e9611020997"}, - {file = "pymsalruntime-0.18.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:0b6c4f54ec13309cc7b717ac8760c2d9856d4924cefa2b794b6d03db4cfdeef8"}, - {file = "pymsalruntime-0.18.1-cp312-cp312-manylinux_2_35_x86_64.whl", hash = "sha256:06c73a47f024fcf36006b89fe32f2f6f6a004aa661cf8a03d3e496d1ef84cfe8"}, - {file = "pymsalruntime-0.18.1-cp312-cp312-win32.whl", hash = "sha256:ace12bf9b7fcbf1bf21a03c227717e09ba99acd9190623fe0821a08832ece4eb"}, - {file = "pymsalruntime-0.18.1-cp312-cp312-win_amd64.whl", hash = "sha256:f9fd8ea52395f52f7d62498e47754adf2bfe6530816ff57eff1ba6f524aee51b"}, - {file = "pymsalruntime-0.18.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:047a98b6709cddf6a1f50f78ee16d06fea0f42a44971b6d3e2988537277a1a17"}, - {file = "pymsalruntime-0.18.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:910e653c65cd66fa9ce46dec103d3948da2276f7d4d315631a145eaab968d9a8"}, - {file = "pymsalruntime-0.18.1-cp313-cp313-manylinux_2_35_x86_64.whl", hash = "sha256:7ae0b160983ea0715d8ac69b441bbd29e7a9f31c9a5a2c350c79a794f5599f38"}, - {file = "pymsalruntime-0.18.1-cp313-cp313-win32.whl", hash = "sha256:adf4200a1b423fe5d8e984c142cc64f0b76a9b0f7f8ff767490a2dde94fa642b"}, - {file = "pymsalruntime-0.18.1-cp313-cp313-win_amd64.whl", hash = "sha256:5a759aa551d084b160799f6df59c9891898ab305eb75ff1705bf04281675eb4b"}, - {file = "pymsalruntime-0.18.1-cp38-cp38-macosx_14_0_arm64.whl", hash = "sha256:12b8990c4da1327ea46f6271bd57b28a90d3e795deacb370052914c3ff40d4c5"}, - {file = "pymsalruntime-0.18.1-cp38-cp38-macosx_14_0_x86_64.whl", hash = "sha256:8dd68f9fedc200950093378b30a2ade4517324cef060788a759b575ea58dc6b2"}, - {file = "pymsalruntime-0.18.1-cp38-cp38-manylinux_2_35_x86_64.whl", hash = "sha256:7183b1b1542a277db119fe55285c7609c661b8506b99cd7e53b7066ce6b838e4"}, - {file = "pymsalruntime-0.18.1-cp38-cp38-win32.whl", hash = "sha256:56c3d708ba86311f049b004de81aa97655fed82782d3ec67e14ae1e27d4f5e5b"}, - {file = "pymsalruntime-0.18.1-cp38-cp38-win_amd64.whl", hash = "sha256:a8adc80fcf723b980976b81a0b409affe80f32d89ae6096d856fd20471d2f0c1"}, - {file = "pymsalruntime-0.18.1-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:600d0f2b9b03dfb457ee1e13f191c2c217c0f6bceca512f1741e5215bc4bc5dc"}, - {file = "pymsalruntime-0.18.1-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:daae8515ae8adac8662d8230f22af242f87c72d86f308ec51b7432f316199c1b"}, - {file = "pymsalruntime-0.18.1-cp39-cp39-manylinux_2_35_x86_64.whl", hash = "sha256:864b8b9555a180c6baf8a57df3976b2e511582d54099561fbfe73f9f0b95c9f5"}, - {file = "pymsalruntime-0.18.1-cp39-cp39-win32.whl", hash = "sha256:b90a3c8079ded9d5abc765bd90fdc34f6e49412793740ddbc6122a601008d50f"}, - {file = "pymsalruntime-0.18.1-cp39-cp39-win_amd64.whl", hash = "sha256:852dc82b3eaad0cce2c583314705183bf216e7fa7178040defd3a13195c1c406"}, -] - -[[package]] -name = "pynacl" -version = "1.6.2" -description = "Python binding to the Networking and Cryptography (NaCl) library" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "pynacl-1.6.2-cp314-cp314t-macosx_10_10_universal2.whl", hash = "sha256:622d7b07cc5c02c666795792931b50c91f3ce3c2649762efb1ef0d5684c81594"}, - {file = "pynacl-1.6.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d071c6a9a4c94d79eb665db4ce5cedc537faf74f2355e4d502591d850d3913c0"}, - {file = "pynacl-1.6.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe9847ca47d287af41e82be1dd5e23023d3c31a951da134121ab02e42ac218c9"}, - {file = "pynacl-1.6.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04316d1fc625d860b6c162fff704eb8426b1a8bcd3abacea11142cbd99a6b574"}, - {file = "pynacl-1.6.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44081faff368d6c5553ccf55322ef2819abb40e25afaec7e740f159f74813634"}, - {file = "pynacl-1.6.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:a9f9932d8d2811ce1a8ffa79dcbdf3970e7355b5c8eb0c1a881a57e7f7d96e88"}, - {file = "pynacl-1.6.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:bc4a36b28dd72fb4845e5d8f9760610588a96d5a51f01d84d8c6ff9849968c14"}, - {file = "pynacl-1.6.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bffb6d0f6becacb6526f8f42adfb5efb26337056ee0831fb9a7044d1a964444"}, - {file = "pynacl-1.6.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fef529ef3ee487ad8113d287a593fa26f48ee3620d92ecc6f1d09ea38e0709b"}, - {file = "pynacl-1.6.2-cp314-cp314t-win32.whl", hash = "sha256:a84bf1c20339d06dc0c85d9aea9637a24f718f375d861b2668b2f9f96fa51145"}, - {file = "pynacl-1.6.2-cp314-cp314t-win_amd64.whl", hash = "sha256:320ef68a41c87547c91a8b58903c9caa641ab01e8512ce291085b5fe2fcb7590"}, - {file = "pynacl-1.6.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d29bfe37e20e015a7d8b23cfc8bd6aa7909c92a1b8f41ee416bbb3e79ef182b2"}, - {file = "pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:c949ea47e4206af7c8f604b8278093b674f7c79ed0d4719cc836902bf4517465"}, - {file = "pynacl-1.6.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8845c0631c0be43abdd865511c41eab235e0be69c81dc66a50911594198679b0"}, - {file = "pynacl-1.6.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22de65bb9010a725b0dac248f353bb072969c94fa8d6b1f34b87d7953cf7bbe4"}, - {file = "pynacl-1.6.2-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46065496ab748469cdd999246d17e301b2c24ae2fdf739132e580a0e94c94a87"}, - {file = "pynacl-1.6.2-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a66d6fb6ae7661c58995f9c6435bda2b1e68b54b598a6a10247bfcdadac996c"}, - {file = "pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130"}, - {file = "pynacl-1.6.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c8a231e36ec2cab018c4ad4358c386e36eede0319a0c41fed24f840b1dac59f6"}, - {file = "pynacl-1.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:68be3a09455743ff9505491220b64440ced8973fe930f270c8e07ccfa25b1f9e"}, - {file = "pynacl-1.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b097553b380236d51ed11356c953bf8ce36a29a3e596e934ecabe76c985a577"}, - {file = "pynacl-1.6.2-cp38-abi3-win32.whl", hash = "sha256:5811c72b473b2f38f7e2a3dc4f8642e3a3e9b5e7317266e4ced1fba85cae41aa"}, - {file = "pynacl-1.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:62985f233210dee6548c223301b6c25440852e13d59a8b81490203c3227c5ba0"}, - {file = "pynacl-1.6.2-cp38-abi3-win_arm64.whl", hash = "sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c"}, - {file = "pynacl-1.6.2.tar.gz", hash = "sha256:018494d6d696ae03c7e656e5e74cdfd8ea1326962cc401bcf018f1ed8436811c"}, -] - -[package.dependencies] -cffi = {version = ">=2.0.0", markers = "platform_python_implementation != \"PyPy\" and python_version >= \"3.9\""} - -[package.extras] -docs = ["sphinx (<7)", "sphinx_rtd_theme"] -tests = ["hypothesis (>=3.27.0)", "pytest (>=7.4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] - -[[package]] -name = "pyopenssl" -version = "26.0.0" -description = "Python wrapper module around the OpenSSL library" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "pyopenssl-26.0.0-py3-none-any.whl", hash = "sha256:df94d28498848b98cc1c0ffb8ef1e71e40210d3b0a8064c9d29571ed2904bf81"}, - {file = "pyopenssl-26.0.0.tar.gz", hash = "sha256:f293934e52936f2e3413b89c6ce36df66a0b34ae1ea3a053b8c5020ff2f513fc"}, -] - -[package.dependencies] -cryptography = ">=46.0.0,<47" -typing-extensions = {version = ">=4.9", markers = "python_version < \"3.13\" and python_version >= \"3.8\""} - -[package.extras] -docs = ["sphinx (!=5.2.0,!=5.2.0.post0,!=7.2.5)", "sphinx_rtd_theme"] -test = ["pretend", "pytest (>=3.0.1)", "pytest-rerunfailures"] - -[[package]] -name = "pyparsing" -version = "3.3.2" -description = "pyparsing - Classes and methods to define and execute parsing grammars" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d"}, - {file = "pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc"}, -] - -[package.extras] -diagrams = ["jinja2", "railroad-diagrams"] - -[[package]] -name = "pyreadline3" -version = "3.5.4" -description = "A python implementation of GNU readline." -optional = false -python-versions = ">=3.8" -groups = ["main"] -markers = "sys_platform == \"win32\"" -files = [ - {file = "pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6"}, - {file = "pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7"}, -] - -[package.extras] -dev = ["build", "flake8", "mypy", "pytest", "twine"] - -[[package]] -name = "pysocks" -version = "1.7.1" -description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -groups = ["main"] -files = [ - {file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"}, - {file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"}, - {file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"}, -] - -[[package]] -name = "pytest" -version = "9.0.3" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.10" -groups = ["main", "dev"] -files = [ - {file = "pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9"}, - {file = "pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c"}, -] - -[package.dependencies] -colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} -iniconfig = ">=1.0.1" -packaging = ">=22" -pluggy = ">=1.5,<2" -pygments = ">=2.7.2" - -[package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-celery" -version = "1.3.0" -description = "Pytest plugin for Celery" -optional = false -python-versions = "<4.0,>=3.9" -groups = ["main"] -files = [ - {file = "pytest_celery-1.3.0-py3-none-any.whl", hash = "sha256:f02201d7770584a0c412a1ded329a142170c24012467c7046f2c72cc8205ad5d"}, - {file = "pytest_celery-1.3.0.tar.gz", hash = "sha256:bd9e5b0f594ec5de9ab97cf27e3a11c644718a761bab6b997d01800fd7394f64"}, -] - -[package.dependencies] -celery = "*" -debugpy = ">=1.8.12,<2.0.0" -docker = ">=7.1.0,<8.0.0" -kombu = "*" -psutil = ">=7.0.0" -pytest-docker-tools = ">=3.1.3" -redis = {version = "*", optional = true, markers = "extra == \"all\" or extra == \"redis\""} -tenacity = ">=9.0.0" - -[package.extras] -all = ["boto3", "botocore", "pycurl (>=7.43) ; sys_platform != \"win32\" and platform_python_implementation == \"CPython\"", "python-memcached", "redis", "urllib3 (>=1.26.16,<2.0)"] -memcached = ["python-memcached"] -redis = ["redis"] -sqs = ["boto3", "botocore", "pycurl (>=7.43) ; sys_platform != \"win32\" and platform_python_implementation == \"CPython\"", "urllib3 (>=1.26.16,<2.0)"] - -[[package]] -name = "pytest-cov" -version = "5.0.0" -description = "Pytest plugin for measuring coverage." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, - {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, -] - -[package.dependencies] -coverage = {version = ">=5.2.1", extras = ["toml"]} -pytest = ">=4.6" - -[package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] - -[[package]] -name = "pytest-django" -version = "4.8.0" -description = "A Django plugin for pytest." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pytest-django-4.8.0.tar.gz", hash = "sha256:5d054fe011c56f3b10f978f41a8efb2e5adfc7e680ef36fb571ada1f24779d90"}, - {file = "pytest_django-4.8.0-py3-none-any.whl", hash = "sha256:ca1ddd1e0e4c227cf9e3e40a6afc6d106b3e70868fd2ac5798a22501271cd0c7"}, -] - -[package.dependencies] -pytest = ">=7.0.0" - -[package.extras] -docs = ["sphinx", "sphinx-rtd-theme"] -testing = ["Django", "django-configurations (>=2.0)"] - -[[package]] -name = "pytest-docker-tools" -version = "3.1.9" -description = "Docker integration tests for pytest" -optional = false -python-versions = "<4.0.0,>=3.9.0" -groups = ["main"] -files = [ - {file = "pytest_docker_tools-3.1.9-py2.py3-none-any.whl", hash = "sha256:36f8e88d56d84ea177df68a175673681243dd991d2807fbf551d90f60341bfdb"}, - {file = "pytest_docker_tools-3.1.9.tar.gz", hash = "sha256:1b6a0cb633c20145731313335ef15bcf5571839c06726764e60cbe495324782b"}, -] - -[package.dependencies] -docker = ">=4.3.1" -pytest = ">=6.0.1" - -[[package]] -name = "pytest-env" -version = "1.1.3" -description = "pytest plugin that allows you to add environment variables." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pytest_env-1.1.3-py3-none-any.whl", hash = "sha256:aada77e6d09fcfb04540a6e462c58533c37df35fa853da78707b17ec04d17dfc"}, - {file = "pytest_env-1.1.3.tar.gz", hash = "sha256:fcd7dc23bb71efd3d35632bde1bbe5ee8c8dc4489d6617fb010674880d96216b"}, -] - -[package.dependencies] -pytest = ">=7.4.3" - -[package.extras] -test = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "pytest-mock (>=3.12)"] - -[[package]] -name = "pytest-randomly" -version = "3.15.0" -description = "Pytest plugin to randomly order tests and control random.seed." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pytest_randomly-3.15.0-py3-none-any.whl", hash = "sha256:0516f4344b29f4e9cdae8bce31c4aeebf59d0b9ef05927c33354ff3859eeeca6"}, - {file = "pytest_randomly-3.15.0.tar.gz", hash = "sha256:b908529648667ba5e54723088edd6f82252f540cc340d748d1fa985539687047"}, -] - -[package.dependencies] -pytest = "*" - -[[package]] -name = "pytest-xdist" -version = "3.6.1" -description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7"}, - {file = "pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"}, -] - -[package.dependencies] -execnet = ">=2.1" -pytest = ">=7.0.0" - -[package.extras] -psutil = ["psutil (>=3.0)"] -setproctitle = ["setproctitle"] -testing = ["filelock"] - -[[package]] -name = "python-crontab" -version = "3.3.0" -description = "Python Crontab API" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "python_crontab-3.3.0-py3-none-any.whl", hash = "sha256:739a778b1a771379b75654e53fd4df58e5c63a9279a63b5dfe44c0fcc3ee7884"}, - {file = "python_crontab-3.3.0.tar.gz", hash = "sha256:007c8aee68dddf3e04ec4dce0fac124b93bd68be7470fc95d2a9617a15de291b"}, -] - -[package.extras] -cron-description = ["cron-descriptor"] -cron-schedule = ["croniter"] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -description = "Extensions to the standard Python datetime module" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main", "dev"] -files = [ - {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, - {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, -] - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "python-digitalocean" -version = "1.17.0" -description = "digitalocean.com API to manage Droplets and Images" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "python-digitalocean-1.17.0.tar.gz", hash = "sha256:107854fde1aafa21774e8053cf253b04173613c94531f75d5a039ad770562b24"}, - {file = "python_digitalocean-1.17.0-py3-none-any.whl", hash = "sha256:0032168e022e85fca314eb3f8dfaabf82087f2ed40839eb28f1eeeeca5afb1fa"}, -] - -[package.dependencies] -jsonpickle = "*" -requests = "*" - -[[package]] -name = "python3-saml" -version = "1.16.0" -description = "Saml Python Toolkit. Add SAML support to your Python software using this library" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "python3-saml-1.16.0.tar.gz", hash = "sha256:97c9669aecabc283c6e5fb4eb264f446b6e006f5267d01c9734f9d8bffdac133"}, - {file = "python3_saml-1.16.0-py2-none-any.whl", hash = "sha256:c49097863c278ff669a337a96c46dc1f25d16307b4bb2679d2d1733cc4f5176a"}, - {file = "python3_saml-1.16.0-py3-none-any.whl", hash = "sha256:20b97d11b04f01ee22e98f4a38242e2fea2e28fbc7fbc9bdd57cab5ac7fc2d0d"}, -] - -[package.dependencies] -isodate = ">=0.6.1" -lxml = ">=4.6.5,<4.7.0 || >4.7.0" -xmlsec = ">=1.3.9" - -[package.extras] -test = ["coverage (>=4.5.2)", "flake8 (>=3.6.0,<=5.0.0)", "freezegun (>=0.3.11,<=1.1.0)", "pytest (>=4.6)"] - -[[package]] -name = "pytz" -version = "2025.1" -description = "World timezone definitions, modern and historical" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57"}, - {file = "pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e"}, -] - -[[package]] -name = "pywin32" -version = "311" -description = "Python for Window Extensions" -optional = false -python-versions = "*" -groups = ["main", "dev"] -files = [ - {file = "pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3"}, - {file = "pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b"}, - {file = "pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b"}, - {file = "pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151"}, - {file = "pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503"}, - {file = "pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2"}, - {file = "pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31"}, - {file = "pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067"}, - {file = "pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852"}, - {file = "pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d"}, - {file = "pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d"}, - {file = "pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a"}, - {file = "pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee"}, - {file = "pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87"}, - {file = "pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42"}, - {file = "pywin32-311-cp38-cp38-win32.whl", hash = "sha256:6c6f2969607b5023b0d9ce2541f8d2cbb01c4f46bc87456017cf63b73f1e2d8c"}, - {file = "pywin32-311-cp38-cp38-win_amd64.whl", hash = "sha256:c8015b09fb9a5e188f83b7b04de91ddca4658cee2ae6f3bc483f0b21a77ef6cd"}, - {file = "pywin32-311-cp39-cp39-win32.whl", hash = "sha256:aba8f82d551a942cb20d4a83413ccbac30790b50efb89a75e4f586ac0bb8056b"}, - {file = "pywin32-311-cp39-cp39-win_amd64.whl", hash = "sha256:e0c4cfb0621281fe40387df582097fd796e80430597cb9944f0ae70447bacd91"}, - {file = "pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d"}, -] -markers = {main = "sys_platform == \"win32\" or platform_system == \"Windows\"", dev = "sys_platform == \"win32\""} - -[[package]] -name = "pyyaml" -version = "6.0.3" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, - {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, - {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"}, - {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"}, - {file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"}, - {file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"}, - {file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"}, - {file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}, - {file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}, - {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}, - {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"}, - {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"}, - {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"}, - {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"}, - {file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"}, - {file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"}, - {file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"}, - {file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"}, - {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"}, - {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"}, - {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"}, - {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"}, - {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"}, - {file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"}, - {file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"}, - {file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"}, - {file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"}, - {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"}, - {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"}, - {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"}, - {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"}, - {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"}, - {file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"}, - {file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"}, - {file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"}, - {file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"}, - {file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"}, - {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"}, - {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"}, - {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"}, - {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"}, - {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"}, - {file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"}, - {file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"}, - {file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"}, - {file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"}, - {file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"}, - {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"}, - {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"}, - {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"}, - {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"}, - {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"}, - {file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"}, - {file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"}, - {file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"}, - {file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"}, - {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"}, - {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"}, - {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"}, - {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"}, - {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"}, - {file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"}, - {file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"}, - {file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"}, - {file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"}, - {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"}, - {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"}, - {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"}, - {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"}, - {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"}, - {file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"}, - {file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"}, - {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, -] - -[[package]] -name = "redis" -version = "7.1.0" -description = "Python client for Redis database and key-value store" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b"}, - {file = "redis-7.1.0.tar.gz", hash = "sha256:b1cc3cfa5a2cb9c2ab3ba700864fb0ad75617b41f01352ce5779dabf6d5f9c3c"}, -] - -[package.dependencies] -async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\""} - -[package.extras] -circuit-breaker = ["pybreaker (>=1.4.0)"] -hiredis = ["hiredis (>=3.2.0)"] -jwt = ["pyjwt (>=2.9.0)"] -ocsp = ["cryptography (>=36.0.1)", "pyopenssl (>=20.0.1)", "requests (>=2.31.0)"] - -[[package]] -name = "referencing" -version = "0.37.0" -description = "JSON Referencing + Python" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231"}, - {file = "referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8"}, -] - -[package.dependencies] -attrs = ">=22.2.0" -rpds-py = ">=0.7.0" -typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} - -[[package]] -name = "regex" -version = "2026.1.15" -description = "Alternative regular expression module, to replace re." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "regex-2026.1.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e3dd93c8f9abe8aa4b6c652016da9a3afa190df5ad822907efe6b206c09896e"}, - {file = "regex-2026.1.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97499ff7862e868b1977107873dd1a06e151467129159a6ffd07b66706ba3a9f"}, - {file = "regex-2026.1.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0bda75ebcac38d884240914c6c43d8ab5fb82e74cde6da94b43b17c411aa4c2b"}, - {file = "regex-2026.1.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7dcc02368585334f5bc81fc73a2a6a0bbade60e7d83da21cead622faf408f32c"}, - {file = "regex-2026.1.15-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:693b465171707bbe882a7a05de5e866f33c76aa449750bee94a8d90463533cc9"}, - {file = "regex-2026.1.15-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b0d190e6f013ea938623a58706d1469a62103fb2a241ce2873a9906e0386582c"}, - {file = "regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ff818702440a5878a81886f127b80127f5d50563753a28211482867f8318106"}, - {file = "regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f052d1be37ef35a54e394de66136e30fa1191fab64f71fc06ac7bc98c9a84618"}, - {file = "regex-2026.1.15-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6bfc31a37fd1592f0c4fc4bfc674b5c42e52efe45b4b7a6a14f334cca4bcebe4"}, - {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d6ce5ae80066b319ae3bc62fd55a557c9491baa5efd0d355f0de08c4ba54e79"}, - {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1704d204bd42b6bb80167df0e4554f35c255b579ba99616def38f69e14a5ccb9"}, - {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e3174a5ed4171570dc8318afada56373aa9289eb6dc0d96cceb48e7358b0e220"}, - {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:87adf5bd6d72e3e17c9cb59ac4096b1faaf84b7eb3037a5ffa61c4b4370f0f13"}, - {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e85dc94595f4d766bd7d872a9de5ede1ca8d3063f3bdf1e2c725f5eb411159e3"}, - {file = "regex-2026.1.15-cp310-cp310-win32.whl", hash = "sha256:21ca32c28c30d5d65fc9886ff576fc9b59bbca08933e844fa2363e530f4c8218"}, - {file = "regex-2026.1.15-cp310-cp310-win_amd64.whl", hash = "sha256:3038a62fc7d6e5547b8915a3d927a0fbeef84cdbe0b1deb8c99bbd4a8961b52a"}, - {file = "regex-2026.1.15-cp310-cp310-win_arm64.whl", hash = "sha256:505831646c945e3e63552cc1b1b9b514f0e93232972a2d5bedbcc32f15bc82e3"}, - {file = "regex-2026.1.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ae6020fb311f68d753b7efa9d4b9a5d47a5d6466ea0d5e3b5a471a960ea6e4a"}, - {file = "regex-2026.1.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eddf73f41225942c1f994914742afa53dc0d01a6e20fe14b878a1b1edc74151f"}, - {file = "regex-2026.1.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e8cd52557603f5c66a548f69421310886b28b7066853089e1a71ee710e1cdc1"}, - {file = "regex-2026.1.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5170907244b14303edc5978f522f16c974f32d3aa92109fabc2af52411c9433b"}, - {file = "regex-2026.1.15-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2748c1ec0663580b4510bd89941a31560b4b439a0b428b49472a3d9944d11cd8"}, - {file = "regex-2026.1.15-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2f2775843ca49360508d080eaa87f94fa248e2c946bbcd963bb3aae14f333413"}, - {file = "regex-2026.1.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9ea2604370efc9a174c1b5dcc81784fb040044232150f7f33756049edfc9026"}, - {file = "regex-2026.1.15-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcd31594264029b57bf16f37fd7248a70b3b764ed9e0839a8f271b2d22c0785"}, - {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c08c1f3e34338256732bd6938747daa3c0d5b251e04b6e43b5813e94d503076e"}, - {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e43a55f378df1e7a4fa3547c88d9a5a9b7113f653a66821bcea4718fe6c58763"}, - {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f82110ab962a541737bd0ce87978d4c658f06e7591ba899192e2712a517badbb"}, - {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:27618391db7bdaf87ac6c92b31e8f0dfb83a9de0075855152b720140bda177a2"}, - {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bfb0d6be01fbae8d6655c8ca21b3b72458606c4aec9bbc932db758d47aba6db1"}, - {file = "regex-2026.1.15-cp311-cp311-win32.whl", hash = "sha256:b10e42a6de0e32559a92f2f8dc908478cc0fa02838d7dbe764c44dca3fa13569"}, - {file = "regex-2026.1.15-cp311-cp311-win_amd64.whl", hash = "sha256:e9bf3f0bbdb56633c07d7116ae60a576f846efdd86a8848f8d62b749e1209ca7"}, - {file = "regex-2026.1.15-cp311-cp311-win_arm64.whl", hash = "sha256:41aef6f953283291c4e4e6850607bd71502be67779586a61472beacb315c97ec"}, - {file = "regex-2026.1.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1"}, - {file = "regex-2026.1.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681"}, - {file = "regex-2026.1.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f"}, - {file = "regex-2026.1.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa"}, - {file = "regex-2026.1.15-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804"}, - {file = "regex-2026.1.15-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c"}, - {file = "regex-2026.1.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5"}, - {file = "regex-2026.1.15-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3"}, - {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb"}, - {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410"}, - {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4"}, - {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d"}, - {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22"}, - {file = "regex-2026.1.15-cp312-cp312-win32.whl", hash = "sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913"}, - {file = "regex-2026.1.15-cp312-cp312-win_amd64.whl", hash = "sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a"}, - {file = "regex-2026.1.15-cp312-cp312-win_arm64.whl", hash = "sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056"}, - {file = "regex-2026.1.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e69d0deeb977ffe7ed3d2e4439360089f9c3f217ada608f0f88ebd67afb6385e"}, - {file = "regex-2026.1.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3601ffb5375de85a16f407854d11cca8fe3f5febbe3ac78fb2866bb220c74d10"}, - {file = "regex-2026.1.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4c5ef43b5c2d4114eb8ea424bb8c9cec01d5d17f242af88b2448f5ee81caadbc"}, - {file = "regex-2026.1.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:968c14d4f03e10b2fd960f1d5168c1f0ac969381d3c1fcc973bc45fb06346599"}, - {file = "regex-2026.1.15-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56a5595d0f892f214609c9f76b41b7428bed439d98dc961efafdd1354d42baae"}, - {file = "regex-2026.1.15-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf650f26087363434c4e560011f8e4e738f6f3e029b85d4904c50135b86cfa5"}, - {file = "regex-2026.1.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18388a62989c72ac24de75f1449d0fb0b04dfccd0a1a7c1c43af5eb503d890f6"}, - {file = "regex-2026.1.15-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d220a2517f5893f55daac983bfa9fe998a7dbcaee4f5d27a88500f8b7873788"}, - {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9c08c2fbc6120e70abff5d7f28ffb4d969e14294fb2143b4b5c7d20e46d1714"}, - {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7ef7d5d4bd49ec7364315167a4134a015f61e8266c6d446fc116a9ac4456e10d"}, - {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e42844ad64194fa08d5ccb75fe6a459b9b08e6d7296bd704460168d58a388f3"}, - {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cfecdaa4b19f9ca534746eb3b55a5195d5c95b88cac32a205e981ec0a22b7d31"}, - {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08df9722d9b87834a3d701f3fca570b2be115654dbfd30179f30ab2f39d606d3"}, - {file = "regex-2026.1.15-cp313-cp313-win32.whl", hash = "sha256:d426616dae0967ca225ab12c22274eb816558f2f99ccb4a1d52ca92e8baf180f"}, - {file = "regex-2026.1.15-cp313-cp313-win_amd64.whl", hash = "sha256:febd38857b09867d3ed3f4f1af7d241c5c50362e25ef43034995b77a50df494e"}, - {file = "regex-2026.1.15-cp313-cp313-win_arm64.whl", hash = "sha256:8e32f7896f83774f91499d239e24cebfadbc07639c1494bb7213983842348337"}, - {file = "regex-2026.1.15-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ec94c04149b6a7b8120f9f44565722c7ae31b7a6d2275569d2eefa76b83da3be"}, - {file = "regex-2026.1.15-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40c86d8046915bb9aeb15d3f3f15b6fd500b8ea4485b30e1bbc799dab3fe29f8"}, - {file = "regex-2026.1.15-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:726ea4e727aba21643205edad8f2187ec682d3305d790f73b7a51c7587b64bdd"}, - {file = "regex-2026.1.15-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cb740d044aff31898804e7bf1181cc72c03d11dfd19932b9911ffc19a79070a"}, - {file = "regex-2026.1.15-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05d75a668e9ea16f832390d22131fe1e8acc8389a694c8febc3e340b0f810b93"}, - {file = "regex-2026.1.15-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d991483606f3dbec93287b9f35596f41aa2e92b7c2ebbb935b63f409e243c9af"}, - {file = "regex-2026.1.15-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:194312a14819d3e44628a44ed6fea6898fdbecb0550089d84c403475138d0a09"}, - {file = "regex-2026.1.15-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe2fda4110a3d0bc163c2e0664be44657431440722c5c5315c65155cab92f9e5"}, - {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:124dc36c85d34ef2d9164da41a53c1c8c122cfb1f6e1ec377a1f27ee81deb794"}, - {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1774cd1981cd212506a23a14dba7fdeaee259f5deba2df6229966d9911e767a"}, - {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b5f7d8d2867152cdb625e72a530d2ccb48a3d199159144cbdd63870882fb6f80"}, - {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:492534a0ab925d1db998defc3c302dae3616a2fc3fe2e08db1472348f096ddf2"}, - {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c661fc820cfb33e166bf2450d3dadbda47c8d8981898adb9b6fe24e5e582ba60"}, - {file = "regex-2026.1.15-cp313-cp313t-win32.whl", hash = "sha256:99ad739c3686085e614bf77a508e26954ff1b8f14da0e3765ff7abbf7799f952"}, - {file = "regex-2026.1.15-cp313-cp313t-win_amd64.whl", hash = "sha256:32655d17905e7ff8ba5c764c43cb124e34a9245e45b83c22e81041e1071aee10"}, - {file = "regex-2026.1.15-cp313-cp313t-win_arm64.whl", hash = "sha256:b2a13dd6a95e95a489ca242319d18fc02e07ceb28fa9ad146385194d95b3c829"}, - {file = "regex-2026.1.15-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:d920392a6b1f353f4aa54328c867fec3320fa50657e25f64abf17af054fc97ac"}, - {file = "regex-2026.1.15-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b5a28980a926fa810dbbed059547b02783952e2efd9c636412345232ddb87ff6"}, - {file = "regex-2026.1.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:621f73a07595d83f28952d7bd1e91e9d1ed7625fb7af0064d3516674ec93a2a2"}, - {file = "regex-2026.1.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d7d92495f47567a9b1669c51fc8d6d809821849063d168121ef801bbc213846"}, - {file = "regex-2026.1.15-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8dd16fba2758db7a3780a051f245539c4451ca20910f5a5e6ea1c08d06d4a76b"}, - {file = "regex-2026.1.15-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1e1808471fbe44c1a63e5f577a1d5f02fe5d66031dcbdf12f093ffc1305a858e"}, - {file = "regex-2026.1.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0751a26ad39d4f2ade8fe16c59b2bf5cb19eb3d2cd543e709e583d559bd9efde"}, - {file = "regex-2026.1.15-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0c7684c7f9ca241344ff95a1de964f257a5251968484270e91c25a755532c5"}, - {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74f45d170a21df41508cb67165456538425185baaf686281fa210d7e729abc34"}, - {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f1862739a1ffb50615c0fde6bae6569b5efbe08d98e59ce009f68a336f64da75"}, - {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:453078802f1b9e2b7303fb79222c054cb18e76f7bdc220f7530fdc85d319f99e"}, - {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:a30a68e89e5a218b8b23a52292924c1f4b245cb0c68d1cce9aec9bbda6e2c160"}, - {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9479cae874c81bf610d72b85bb681a94c95722c127b55445285fb0e2c82db8e1"}, - {file = "regex-2026.1.15-cp314-cp314-win32.whl", hash = "sha256:d639a750223132afbfb8f429c60d9d318aeba03281a5f1ab49f877456448dcf1"}, - {file = "regex-2026.1.15-cp314-cp314-win_amd64.whl", hash = "sha256:4161d87f85fa831e31469bfd82c186923070fc970b9de75339b68f0c75b51903"}, - {file = "regex-2026.1.15-cp314-cp314-win_arm64.whl", hash = "sha256:91c5036ebb62663a6b3999bdd2e559fd8456d17e2b485bf509784cd31a8b1705"}, - {file = "regex-2026.1.15-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ee6854c9000a10938c79238de2379bea30c82e4925a371711af45387df35cab8"}, - {file = "regex-2026.1.15-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c2b80399a422348ce5de4fe40c418d6299a0fa2803dd61dc0b1a2f28e280fcf"}, - {file = "regex-2026.1.15-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dca3582bca82596609959ac39e12b7dad98385b4fefccb1151b937383cec547d"}, - {file = "regex-2026.1.15-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71d476caa6692eea743ae5ea23cde3260677f70122c4d258ca952e5c2d4e84"}, - {file = "regex-2026.1.15-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c243da3436354f4af6c3058a3f81a97d47ea52c9bd874b52fd30274853a1d5df"}, - {file = "regex-2026.1.15-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8355ad842a7c7e9e5e55653eade3b7d1885ba86f124dd8ab1f722f9be6627434"}, - {file = "regex-2026.1.15-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f192a831d9575271a22d804ff1a5355355723f94f31d9eef25f0d45a152fdc1a"}, - {file = "regex-2026.1.15-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:166551807ec20d47ceaeec380081f843e88c8949780cd42c40f18d16168bed10"}, - {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9ca1cbdc0fbfe5e6e6f8221ef2309988db5bcede52443aeaee9a4ad555e0dac"}, - {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b30bcbd1e1221783c721483953d9e4f3ab9c5d165aa709693d3f3946747b1aea"}, - {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2a8d7b50c34578d0d3bf7ad58cde9652b7d683691876f83aedc002862a35dc5e"}, - {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9d787e3310c6a6425eb346be4ff2ccf6eece63017916fd77fe8328c57be83521"}, - {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:619843841e220adca114118533a574a9cd183ed8a28b85627d2844c500a2b0db"}, - {file = "regex-2026.1.15-cp314-cp314t-win32.whl", hash = "sha256:e90b8db97f6f2c97eb045b51a6b2c5ed69cedd8392459e0642d4199b94fabd7e"}, - {file = "regex-2026.1.15-cp314-cp314t-win_amd64.whl", hash = "sha256:5ef19071f4ac9f0834793af85bd04a920b4407715624e40cb7a0631a11137cdf"}, - {file = "regex-2026.1.15-cp314-cp314t-win_arm64.whl", hash = "sha256:ca89c5e596fc05b015f27561b3793dc2fa0917ea0d7507eebb448efd35274a70"}, - {file = "regex-2026.1.15-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:55b4ea996a8e4458dd7b584a2f89863b1655dd3d17b88b46cbb9becc495a0ec5"}, - {file = "regex-2026.1.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e1e28be779884189cdd57735e997f282b64fd7ccf6e2eef3e16e57d7a34a815"}, - {file = "regex-2026.1.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0057de9eaef45783ff69fa94ae9f0fd906d629d0bd4c3217048f46d1daa32e9b"}, - {file = "regex-2026.1.15-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc7cd0b2be0f0269283a45c0d8b2c35e149d1319dcb4a43c9c3689fa935c1ee6"}, - {file = "regex-2026.1.15-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8db052bbd981e1666f09e957f3790ed74080c2229007c1dd67afdbf0b469c48b"}, - {file = "regex-2026.1.15-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:343db82cb3712c31ddf720f097ef17c11dab2f67f7a3e7be976c4f82eba4e6df"}, - {file = "regex-2026.1.15-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:55e9d0118d97794367309635df398bdfd7c33b93e2fdfa0b239661cd74b4c14e"}, - {file = "regex-2026.1.15-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:008b185f235acd1e53787333e5690082e4f156c44c87d894f880056089e9bc7c"}, - {file = "regex-2026.1.15-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fd65af65e2aaf9474e468f9e571bd7b189e1df3a61caa59dcbabd0000e4ea839"}, - {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f42e68301ff4afee63e365a5fc302b81bb8ba31af625a671d7acb19d10168a8c"}, - {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:f7792f27d3ee6e0244ea4697d92b825f9a329ab5230a78c1a68bd274e64b5077"}, - {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:dbaf3c3c37ef190439981648ccbf0c02ed99ae066087dd117fcb616d80b010a4"}, - {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:adc97a9077c2696501443d8ad3fa1b4fc6d131fc8fd7dfefd1a723f89071cf0a"}, - {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:069f56a7bf71d286a6ff932a9e6fb878f151c998ebb2519a9f6d1cee4bffdba3"}, - {file = "regex-2026.1.15-cp39-cp39-win32.whl", hash = "sha256:ea4e6b3566127fda5e007e90a8fd5a4169f0cf0619506ed426db647f19c8454a"}, - {file = "regex-2026.1.15-cp39-cp39-win_amd64.whl", hash = "sha256:cda1ed70d2b264952e88adaa52eea653a33a1b98ac907ae2f86508eb44f65cdc"}, - {file = "regex-2026.1.15-cp39-cp39-win_arm64.whl", hash = "sha256:b325d4714c3c48277bfea1accd94e193ad6ed42b4bad79ad64f3b8f8a31260a5"}, - {file = "regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5"}, -] - -[[package]] -name = "reportlab" -version = "4.4.10" -description = "The Reportlab Toolkit" -optional = false -python-versions = "<4,>=3.9" -groups = ["main"] -files = [ - {file = "reportlab-4.4.10-py3-none-any.whl", hash = "sha256:5abc815746ae2bc44e7ff25db96814f921349ca814c992c7eac3c26029bf7c24"}, - {file = "reportlab-4.4.10.tar.gz", hash = "sha256:5cbbb34ac3546039d0086deb2938cdec06b12da3cdb836e813258eb33cd28487"}, -] - -[package.dependencies] -charset-normalizer = "*" -pillow = ">=9.0.0" - -[package.extras] -accel = ["rl_accel (>=0.9.0,<1.1)"] -bidi = ["rlbidi"] -pycairo = ["freetype-py (>=2.3.0,<2.4)", "rlPyCairo (>=0.2.0,<1)"] -renderpm = ["rl_renderPM (>=4.0.3,<4.1)"] -shaping = ["uharfbuzz"] - -[[package]] -name = "requests" -version = "2.32.5" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, - {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset_normalizer = ">=2,<4" -idna = ">=2.5,<4" -PySocks = {version = ">=1.5.6,<1.5.7 || >1.5.7", optional = true, markers = "extra == \"socks\""} -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "requests-file" -version = "3.0.1" -description = "File transport adapter for Requests" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "requests_file-3.0.1-py2.py3-none-any.whl", hash = "sha256:d0f5eb94353986d998f80ac63c7f146a307728be051d4d1cd390dbdb59c10fa2"}, - {file = "requests_file-3.0.1.tar.gz", hash = "sha256:f14243d7796c588f3521bd423c5dea2ee4cc730e54a3cac9574d78aca1272576"}, -] - -[package.dependencies] -requests = ">=1.0.0" - -[[package]] -name = "requests-oauthlib" -version = "2.0.0" -description = "OAuthlib authentication support for Requests." -optional = false -python-versions = ">=3.4" -groups = ["main"] -files = [ - {file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"}, - {file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"}, -] - -[package.dependencies] -oauthlib = ">=3.0.0" -requests = ">=2.0.0" - -[package.extras] -rsa = ["oauthlib[signedtoken] (>=3.0.0)"] - -[[package]] -name = "requestsexceptions" -version = "1.4.0" -description = "Import exceptions from potentially bundled packages in requests." -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "requestsexceptions-1.4.0-py2.py3-none-any.whl", hash = "sha256:3083d872b6e07dc5c323563ef37671d992214ad9a32b0ca4a3d7f5500bf38ce3"}, - {file = "requestsexceptions-1.4.0.tar.gz", hash = "sha256:b095cbc77618f066d459a02b137b020c37da9f46d9b057704019c9f77dba3065"}, -] - -[[package]] -name = "retrying" -version = "1.4.2" -description = "Retrying" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "retrying-1.4.2-py3-none-any.whl", hash = "sha256:bbc004aeb542a74f3569aeddf42a2516efefcdaff90df0eb38fbfbf19f179f59"}, - {file = "retrying-1.4.2.tar.gz", hash = "sha256:d102e75d53d8d30b88562d45361d6c6c934da06fab31bd81c0420acb97a8ba39"}, -] - -[[package]] -name = "rich" -version = "14.3.2" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -optional = false -python-versions = ">=3.8.0" -groups = ["main", "dev"] -files = [ - {file = "rich-14.3.2-py3-none-any.whl", hash = "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69"}, - {file = "rich-14.3.2.tar.gz", hash = "sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8"}, -] - -[package.dependencies] -markdown-it-py = ">=2.2.0" -pygments = ">=2.13.0,<3.0.0" - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<9)"] - -[[package]] -name = "rpds-py" -version = "0.30.0" -description = "Python bindings to Rust's persistent data structures (rpds)" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288"}, - {file = "rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00"}, - {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6"}, - {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7"}, - {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324"}, - {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df"}, - {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3"}, - {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221"}, - {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7"}, - {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff"}, - {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7"}, - {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139"}, - {file = "rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464"}, - {file = "rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169"}, - {file = "rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425"}, - {file = "rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d"}, - {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4"}, - {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f"}, - {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4"}, - {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97"}, - {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89"}, - {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d"}, - {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038"}, - {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7"}, - {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed"}, - {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85"}, - {file = "rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c"}, - {file = "rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825"}, - {file = "rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229"}, - {file = "rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad"}, - {file = "rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05"}, - {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28"}, - {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd"}, - {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f"}, - {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1"}, - {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23"}, - {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6"}, - {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51"}, - {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5"}, - {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e"}, - {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394"}, - {file = "rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf"}, - {file = "rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b"}, - {file = "rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e"}, - {file = "rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2"}, - {file = "rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8"}, - {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4"}, - {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136"}, - {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7"}, - {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2"}, - {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6"}, - {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e"}, - {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d"}, - {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7"}, - {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31"}, - {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95"}, - {file = "rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d"}, - {file = "rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15"}, - {file = "rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1"}, - {file = "rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a"}, - {file = "rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e"}, - {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000"}, - {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db"}, - {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2"}, - {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa"}, - {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083"}, - {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9"}, - {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0"}, - {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94"}, - {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08"}, - {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27"}, - {file = "rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6"}, - {file = "rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d"}, - {file = "rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0"}, - {file = "rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be"}, - {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f"}, - {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f"}, - {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87"}, - {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18"}, - {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad"}, - {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07"}, - {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f"}, - {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65"}, - {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f"}, - {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53"}, - {file = "rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed"}, - {file = "rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950"}, - {file = "rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6"}, - {file = "rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb"}, - {file = "rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8"}, - {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7"}, - {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898"}, - {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e"}, - {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419"}, - {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551"}, - {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8"}, - {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5"}, - {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404"}, - {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856"}, - {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40"}, - {file = "rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0"}, - {file = "rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3"}, - {file = "rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58"}, - {file = "rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a"}, - {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb"}, - {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c"}, - {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3"}, - {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5"}, - {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738"}, - {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f"}, - {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877"}, - {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a"}, - {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4"}, - {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e"}, - {file = "rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84"}, -] - -[[package]] -name = "rsa" -version = "4.9.1" -description = "Pure-Python RSA implementation" -optional = false -python-versions = "<4,>=3.6" -groups = ["main"] -files = [ - {file = "rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762"}, - {file = "rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75"}, -] - -[package.dependencies] -pyasn1 = ">=0.1.3" - -[[package]] -name = "ruamel-yaml" -version = "0.19.1" -description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93"}, - {file = "ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993"}, -] - -[package.extras] -docs = ["mercurial (>5.7)", "ryd"] -jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] -libyaml = ["ruamel.yaml.clibz (>=0.3.7) ; platform_python_implementation == \"CPython\""] -oldlibyaml = ["ruamel.yaml.clib ; platform_python_implementation == \"CPython\""] - -[[package]] -name = "ruff" -version = "0.5.0" -description = "An extremely fast Python linter and code formatter, written in Rust." -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "ruff-0.5.0-py3-none-linux_armv6l.whl", hash = "sha256:ee770ea8ab38918f34e7560a597cc0a8c9a193aaa01bfbd879ef43cb06bd9c4c"}, - {file = "ruff-0.5.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:38f3b8327b3cb43474559d435f5fa65dacf723351c159ed0dc567f7ab735d1b6"}, - {file = "ruff-0.5.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7594f8df5404a5c5c8f64b8311169879f6cf42142da644c7e0ba3c3f14130370"}, - {file = "ruff-0.5.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:adc7012d6ec85032bc4e9065110df205752d64010bed5f958d25dbee9ce35de3"}, - {file = "ruff-0.5.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d505fb93b0fabef974b168d9b27c3960714d2ecda24b6ffa6a87ac432905ea38"}, - {file = "ruff-0.5.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dc5cfd3558f14513ed0d5b70ce531e28ea81a8a3b1b07f0f48421a3d9e7d80a"}, - {file = "ruff-0.5.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:db3ca35265de239a1176d56a464b51557fce41095c37d6c406e658cf80bbb362"}, - {file = "ruff-0.5.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b1a321c4f68809fddd9b282fab6a8d8db796b270fff44722589a8b946925a2a8"}, - {file = "ruff-0.5.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c4dfcd8d34b143916994b3876b63d53f56724c03f8c1a33a253b7b1e6bf2a7d"}, - {file = "ruff-0.5.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81e5facfc9f4a674c6a78c64d38becfbd5e4f739c31fcd9ce44c849f1fad9e4c"}, - {file = "ruff-0.5.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e589e27971c2a3efff3fadafb16e5aef7ff93250f0134ec4b52052b673cf988d"}, - {file = "ruff-0.5.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2ffbc3715a52b037bcb0f6ff524a9367f642cdc5817944f6af5479bbb2eb50e"}, - {file = "ruff-0.5.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cd096e23c6a4f9c819525a437fa0a99d1c67a1b6bb30948d46f33afbc53596cf"}, - {file = "ruff-0.5.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:46e193b36f2255729ad34a49c9a997d506e58f08555366b2108783b3064a0e1e"}, - {file = "ruff-0.5.0-py3-none-win32.whl", hash = "sha256:49141d267100f5ceff541b4e06552e98527870eafa1acc9dec9139c9ec5af64c"}, - {file = "ruff-0.5.0-py3-none-win_amd64.whl", hash = "sha256:e9118f60091047444c1b90952736ee7b1792910cab56e9b9a9ac20af94cd0440"}, - {file = "ruff-0.5.0-py3-none-win_arm64.whl", hash = "sha256:ed5c4df5c1fb4518abcb57725b576659542bdbe93366f4f329e8f398c4b71178"}, - {file = "ruff-0.5.0.tar.gz", hash = "sha256:eb641b5873492cf9bd45bc9c5ae5320648218e04386a5f0c264ad6ccce8226a1"}, -] - -[[package]] -name = "s3transfer" -version = "0.14.0" -description = "An Amazon S3 Transfer Manager" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "s3transfer-0.14.0-py3-none-any.whl", hash = "sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456"}, - {file = "s3transfer-0.14.0.tar.gz", hash = "sha256:eff12264e7c8b4985074ccce27a3b38a485bb7f7422cc8046fee9be4983e4125"}, -] - -[package.dependencies] -botocore = ">=1.37.4,<2.0a0" - -[package.extras] -crt = ["botocore[crt] (>=1.37.4,<2.0a0)"] - -[[package]] -name = "safety" -version = "3.7.0" -description = "Scan dependencies for known vulnerabilities and licenses." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "safety-3.7.0-py3-none-any.whl", hash = "sha256:65e71db45eb832e8840e3456333d44c23927423753d5610596a09e909a66d2bf"}, - {file = "safety-3.7.0.tar.gz", hash = "sha256:daec15a393cafc32b846b7ef93f9c952a1708863e242341ab5bde2e4beabb54e"}, -] - -[package.dependencies] -authlib = ">=1.2.0" -click = ">=8.0.2" -dparse = ">=0.6.4" -filelock = ">=3.16.1,<4.0" -httpx = "*" -jinja2 = ">=3.1.0" -marshmallow = ">=3.15.0" -nltk = ">=3.9" -packaging = ">=21.0" -pydantic = ">=2.6.0" -requests = "*" -ruamel-yaml = ">=0.17.21" -safety-schemas = "0.0.16" -tenacity = ">=8.1.0" -tomlkit = "*" -typer = ">=0.16.0" -typing-extensions = ">=4.7.1" - -[package.extras] -github = ["pygithub (>=1.43.3)"] -gitlab = ["python-gitlab (>=1.3.0)"] -spdx = ["spdx-tools (>=0.8.2)"] - -[[package]] -name = "safety-schemas" -version = "0.0.16" -description = "Schemas for Safety tools" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "safety_schemas-0.0.16-py3-none-any.whl", hash = "sha256:6760515d3fd1e6535b251cd73014bd431d12fe0bfb8b6e8880a9379b5ab7aa44"}, - {file = "safety_schemas-0.0.16.tar.gz", hash = "sha256:3bb04d11bd4b5cc79f9fa183c658a6a8cf827a9ceec443a5ffa6eed38a50a24e"}, -] - -[package.dependencies] -dparse = ">=0.6.4" -packaging = ">=21.0" -pydantic = ">=2.6.0" -ruamel-yaml = ">=0.17.21" -typing-extensions = ">=4.7.1" - -[[package]] -name = "scaleway" -version = "2.10.3" -description = "Scaleway SDK for Python" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "scaleway-2.10.3-py3-none-any.whl", hash = "sha256:dbf381440d6caf37c878cf16445a63f4969a4aac2257c9b72c744d10ff223a0c"}, - {file = "scaleway-2.10.3.tar.gz", hash = "sha256:b1f9dd1b1450767205234c6f5a345e5e25dc039c780253d698893b5c344ce594"}, -] - -[package.dependencies] -scaleway-core = "2.10.3" - -[[package]] -name = "scaleway-core" -version = "2.10.3" -description = "Scaleway SDK for Python" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "scaleway_core-2.10.3-py3-none-any.whl", hash = "sha256:fd4112144554d6adae22ff737555eeb0e38cb1063250b3e88c9aebc1b957793b"}, - {file = "scaleway_core-2.10.3.tar.gz", hash = "sha256:56432f755d694669429de51d51c1d0b3361b28dc2f939b28e4cb954610ee76be"}, -] - -[package.dependencies] -python-dateutil = ">=2.8.2,<3.0.0" -PyYAML = ">=6.0,<7.0" -requests = ">=2.28.1,<3.0.0" - -[[package]] -name = "schema" -version = "0.7.5" -description = "Simple data validation library" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "schema-0.7.5-py2.py3-none-any.whl", hash = "sha256:f3ffdeeada09ec34bf40d7d79996d9f7175db93b7a5065de0faa7f41083c1e6c"}, - {file = "schema-0.7.5.tar.gz", hash = "sha256:f06717112c61895cabc4707752b88716e8420a8819d71404501e114f91043197"}, -] - -[package.dependencies] -contextlib2 = ">=0.5.5" - -[[package]] -name = "sentry-sdk" -version = "2.56.0" -description = "Python client for Sentry (https://sentry.io)" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "sentry_sdk-2.56.0-py2.py3-none-any.whl", hash = "sha256:5afafb744ceb91d22f4cc650c6bd048ac6af5f7412dcc6c59305a2e36f4dbc02"}, - {file = "sentry_sdk-2.56.0.tar.gz", hash = "sha256:fdab72030b69625665b2eeb9738bdde748ad254e8073085a0ce95382678e8168"}, -] - -[package.dependencies] -certifi = "*" -django = {version = ">=1.8", optional = true, markers = "extra == \"django\""} -urllib3 = ">=1.26.11" - -[package.extras] -aiohttp = ["aiohttp (>=3.5)"] -anthropic = ["anthropic (>=0.16)"] -arq = ["arq (>=0.23)"] -asyncpg = ["asyncpg (>=0.23)"] -beam = ["apache-beam (>=2.12)"] -bottle = ["bottle (>=0.12.13)"] -celery = ["celery (>=3)"] -celery-redbeat = ["celery-redbeat (>=2)"] -chalice = ["chalice (>=1.16.0)"] -clickhouse-driver = ["clickhouse-driver (>=0.2.0)"] -django = ["django (>=1.8)"] -falcon = ["falcon (>=1.4)"] -fastapi = ["fastapi (>=0.79.0)"] -flask = ["blinker (>=1.1)", "flask (>=0.11)", "markupsafe"] -google-genai = ["google-genai (>=1.29.0)"] -grpcio = ["grpcio (>=1.21.1)", "protobuf (>=3.8.0)"] -http2 = ["httpcore[http2] (==1.*)"] -httpx = ["httpx (>=0.16.0)"] -huey = ["huey (>=2)"] -huggingface-hub = ["huggingface_hub (>=0.22)"] -langchain = ["langchain (>=0.0.210)"] -langgraph = ["langgraph (>=0.6.6)"] -launchdarkly = ["launchdarkly-server-sdk (>=9.8.0)"] -litellm = ["litellm (>=1.77.5)"] -litestar = ["litestar (>=2.0.0)"] -loguru = ["loguru (>=0.5)"] -mcp = ["mcp (>=1.15.0)"] -openai = ["openai (>=1.0.0)", "tiktoken (>=0.3.0)"] -openfeature = ["openfeature-sdk (>=0.7.1)"] -opentelemetry = ["opentelemetry-distro (>=0.35b0)"] -opentelemetry-experimental = ["opentelemetry-distro"] -opentelemetry-otlp = ["opentelemetry-distro[otlp] (>=0.35b0)"] -pure-eval = ["asttokens", "executing", "pure_eval"] -pydantic-ai = ["pydantic-ai (>=1.0.0)"] -pymongo = ["pymongo (>=3.1)"] -pyspark = ["pyspark (>=2.4.4)"] -quart = ["blinker (>=1.1)", "quart (>=0.16.1)"] -rq = ["rq (>=0.6)"] -sanic = ["sanic (>=0.8)"] -sqlalchemy = ["sqlalchemy (>=1.2)"] -starlette = ["starlette (>=0.19.1)"] -starlite = ["starlite (>=1.48)"] -statsig = ["statsig (>=0.55.3)"] -tornado = ["tornado (>=6)"] -unleash = ["UnleashClient (>=6.0.1)"] - -[[package]] -name = "setuptools" -version = "80.10.2" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "setuptools-80.10.2-py3-none-any.whl", hash = "sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173"}, - {file = "setuptools-80.10.2.tar.gz", hash = "sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] -core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] - -[[package]] -name = "shellingham" -version = "1.5.4" -description = "Tool to Detect Surrounding Shell" -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -files = [ - {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, - {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, -] - -[[package]] -name = "shodan" -version = "1.31.0" -description = "Python library and command-line utility for Shodan (https://developer.shodan.io)" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "shodan-1.31.0.tar.gz", hash = "sha256:c73275386ea02390e196c35c660706a28dd4d537c5a21eb387ab6236fac251f6"}, -] - -[package.dependencies] -click = "*" -click-plugins = "*" -colorama = "*" -requests = ">=2.2.1" -tldextract = "*" -XlsxWriter = "*" - -[[package]] -name = "six" -version = "1.17.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main", "dev"] -files = [ - {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, - {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, -] - -[[package]] -name = "slack-sdk" -version = "3.39.0" -description = "The Slack API Platform SDK for Python" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "slack_sdk-3.39.0-py2.py3-none-any.whl", hash = "sha256:b1556b2f5b8b12b94e5ea3f56c4f2c7f04462e4e1013d325c5764ff118044fa8"}, - {file = "slack_sdk-3.39.0.tar.gz", hash = "sha256:6a56be10dc155c436ff658c6b776e1c082e29eae6a771fccf8b0a235822bbcb1"}, -] - -[package.extras] -optional = ["SQLAlchemy (>=1.4,<3)", "aiodns (>1.0)", "aiohttp (>=3.7.3,<4)", "boto3 (<=2)", "websocket-client (>=1,<2)", "websockets (>=9.1,<16)"] - -[[package]] -name = "sniffio" -version = "1.3.1" -description = "Sniff out which async library your code is running under" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, -] - -[[package]] -name = "sqlparse" -version = "0.5.5" -description = "A non-validating SQL parser." -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba"}, - {file = "sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e"}, -] - -[package.extras] -dev = ["build"] -doc = ["sphinx"] - -[[package]] -name = "statsd" -version = "4.0.1" -description = "A simple statsd client." -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "statsd-4.0.1-py2.py3-none-any.whl", hash = "sha256:c2676519927f7afade3723aca9ca8ea986ef5b059556a980a867721ca69df093"}, - {file = "statsd-4.0.1.tar.gz", hash = "sha256:99763da81bfea8daf6b3d22d11aaccb01a8d0f52ea521daab37e758a4ca7d128"}, -] - -[[package]] -name = "std-uritemplate" -version = "2.0.8" -description = "std-uritemplate implementation for Python" -optional = false -python-versions = "<4.0,>=3.8" -groups = ["main"] -files = [ - {file = "std_uritemplate-2.0.8-py3-none-any.whl", hash = "sha256:839807a7f9d07f0bad1a88977c3428bd97b9ff0d229412a0bf36123d8c724257"}, - {file = "std_uritemplate-2.0.8.tar.gz", hash = "sha256:138ceff2c5bfef18a650372a5e8c82fe7f780c87235513de6c342fb5f7e18347"}, -] - -[[package]] -name = "stevedore" -version = "5.6.0" -description = "Manage dynamic plugins for Python applications" -optional = false -python-versions = ">=3.10" -groups = ["main", "dev"] -files = [ - {file = "stevedore-5.6.0-py3-none-any.whl", hash = "sha256:4a36dccefd7aeea0c70135526cecb7766c4c84c473b1af68db23d541b6dc1820"}, - {file = "stevedore-5.6.0.tar.gz", hash = "sha256:f22d15c6ead40c5bbfa9ca54aa7e7b4a07d59b36ae03ed12ced1a54cf0b51945"}, -] - -[[package]] -name = "tabulate" -version = "0.9.0" -description = "Pretty-print tabular data" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"}, - {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"}, -] - -[package.extras] -widechars = ["wcwidth"] - -[[package]] -name = "tenacity" -version = "9.1.2" -description = "Retry code until it succeeds" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138"}, - {file = "tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb"}, -] - -[package.extras] -doc = ["reno", "sphinx"] -test = ["pytest", "tornado (>=4.5)", "typeguard"] - -[[package]] -name = "tldextract" -version = "5.3.1" -description = "Accurately separates a URL's subdomain, domain, and public suffix, using the Public Suffix List (PSL). By default, this includes the public ICANN TLDs and their exceptions. You can optionally support the Public Suffix List's private domains as well." -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "tldextract-5.3.1-py3-none-any.whl", hash = "sha256:6bfe36d518de569c572062b788e16a659ccaceffc486d243af0484e8ecf432d9"}, - {file = "tldextract-5.3.1.tar.gz", hash = "sha256:a72756ca170b2510315076383ea2993478f7da6f897eef1f4a5400735d5057fb"}, -] - -[package.dependencies] -filelock = ">=3.0.8" -idna = "*" -requests = ">=2.1.0" -requests-file = ">=1.4" - -[package.extras] -release = ["build", "twine"] -testing = ["mypy", "pytest", "pytest-gitignore", "pytest-mock", "responses", "ruff", "syrupy", "tox", "tox-uv", "types-filelock", "types-requests"] - -[[package]] -name = "tomlkit" -version = "0.14.0" -description = "Style preserving TOML library" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680"}, - {file = "tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064"}, -] - -[[package]] -name = "tqdm" -version = "4.67.1" -description = "Fast, Extensible Progress Meter" -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -files = [ - {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, - {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[package.extras] -dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"] -discord = ["requests"] -notebook = ["ipywidgets (>=6)"] -slack = ["slack-sdk"] -telegram = ["requests"] - -[[package]] -name = "typer" -version = "0.21.1" -description = "Typer, build great CLIs. Easy to code. Based on Python type hints." -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01"}, - {file = "typer-0.21.1.tar.gz", hash = "sha256:ea835607cd752343b6b2b7ce676893e5a0324082268b48f27aa058bdb7d2145d"}, -] - -[package.dependencies] -click = ">=8.0.0" -rich = ">=10.11.0" -shellingham = ">=1.3.0" -typing-extensions = ">=3.7.4.3" - -[[package]] -name = "types-aiobotocore-ecr" -version = "3.1.1" -description = "Type annotations for aiobotocore ECR 3.1.1 service generated with mypy-boto3-builder 8.12.0" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "types_aiobotocore_ecr-3.1.1-py3-none-any.whl", hash = "sha256:e5c02e06ff057bbe7821fb40ac7de67d2335fdc7987ea31392051efe81ceb69c"}, - {file = "types_aiobotocore_ecr-3.1.1.tar.gz", hash = "sha256:155edc63c612e1a7861fa746376a5143cc4f3ca05b60c27d68ced23e8567a344"}, -] - -[package.dependencies] -typing-extensions = {version = "*", markers = "python_version < \"3.12\""} - -[[package]] -name = "typing-extensions" -version = "4.15.0" -description = "Backported and Experimental Type Hints for Python 3.9+" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, - {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, -] - -[[package]] -name = "typing-inspection" -version = "0.4.2" -description = "Runtime typing introspection tools" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, - {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, -] - -[package.dependencies] -typing-extensions = ">=4.12.0" - -[[package]] -name = "tzdata" -version = "2025.3" -description = "Provider of IANA time zone data" -optional = false -python-versions = ">=2" -groups = ["main", "dev"] -files = [ - {file = "tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1"}, - {file = "tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7"}, -] -markers = {dev = "sys_platform == \"win32\""} - -[[package]] -name = "tzlocal" -version = "5.3.1" -description = "tzinfo object for the local timezone" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d"}, - {file = "tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd"}, -] - -[package.dependencies] -tzdata = {version = "*", markers = "platform_system == \"Windows\""} - -[package.extras] -devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] - -[[package]] -name = "uritemplate" -version = "4.2.0" -description = "Implementation of RFC 6570 URI Templates" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686"}, - {file = "uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e"}, -] - -[[package]] -name = "urllib3" -version = "2.6.3" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, - {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, -] - -[package.extras] -brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] - -[[package]] -name = "uuid6" -version = "2024.7.10" -description = "New time-based UUID formats which are suited for use as a database key" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "uuid6-2024.7.10-py3-none-any.whl", hash = "sha256:93432c00ba403751f722829ad21759ff9db051dea140bf81493271e8e4dd18b7"}, - {file = "uuid6-2024.7.10.tar.gz", hash = "sha256:2d29d7f63f593caaeea0e0d0dd0ad8129c9c663b29e19bdf882e864bedf18fb0"}, -] - -[[package]] -name = "vine" -version = "5.1.0" -description = "Python promises." -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc"}, - {file = "vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0"}, -] - -[[package]] -name = "vulture" -version = "2.14" -description = "Find dead code" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "vulture-2.14-py2.py3-none-any.whl", hash = "sha256:d9a90dba89607489548a49d557f8bac8112bd25d3cbc8aeef23e860811bd5ed9"}, - {file = "vulture-2.14.tar.gz", hash = "sha256:cb8277902a1138deeab796ec5bef7076a6e0248ca3607a3f3dee0b6d9e9b8415"}, -] - -[[package]] -name = "wcwidth" -version = "0.5.3" -description = "Measures the displayed width of unicode strings in a terminal" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "wcwidth-0.5.3-py3-none-any.whl", hash = "sha256:d584eff31cd4753e1e5ff6c12e1edfdb324c995713f75d26c29807bb84bf649e"}, - {file = "wcwidth-0.5.3.tar.gz", hash = "sha256:53123b7af053c74e9fe2e92ac810301f6139e64379031f7124574212fb3b4091"}, -] - -[[package]] -name = "websocket-client" -version = "1.9.0" -description = "WebSocket client for Python with low level API options" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef"}, - {file = "websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98"}, -] - -[package.extras] -docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx_rtd_theme (>=1.1.0)"] -optional = ["python-socks", "wsaccel"] -test = ["pytest", "websockets"] - -[[package]] -name = "werkzeug" -version = "3.1.7" -description = "The comprehensive WSGI web application library." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "werkzeug-3.1.7-py3-none-any.whl", hash = "sha256:4b314d81163a3e1a169b6a0be2a000a0e204e8873c5de6586f453c55688d422f"}, - {file = "werkzeug-3.1.7.tar.gz", hash = "sha256:fb8c01fe6ab13b9b7cdb46892b99b1d66754e1d7ab8e542e865ec13f526b5351"}, -] - -[package.dependencies] -markupsafe = ">=2.1.1" - -[package.extras] -watchdog = ["watchdog (>=2.3)"] - -[[package]] -name = "workos" -version = "6.0.4" -description = "WorkOS Python Client" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "workos-6.0.4-py3-none-any.whl", hash = "sha256:548668b3702673536f853ba72a7b5bbbc269e467aaf9ac4f477b6e0177df5e21"}, - {file = "workos-6.0.4.tar.gz", hash = "sha256:b0bfe8fd212b8567422c4ea3732eb33608794033eb3a69900c6b04db183c32d6"}, -] - -[package.dependencies] -cryptography = ">=46.0,<47.0" -httpx = ">=0.28,<1.0" -pyjwt = ">=2.12,<3.0" - -[[package]] -name = "wrapt" -version = "1.17.3" -description = "Module for decorators, wrappers and monkey patching." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04"}, - {file = "wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2"}, - {file = "wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c"}, - {file = "wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775"}, - {file = "wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd"}, - {file = "wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05"}, - {file = "wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418"}, - {file = "wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390"}, - {file = "wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6"}, - {file = "wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18"}, - {file = "wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7"}, - {file = "wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85"}, - {file = "wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f"}, - {file = "wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311"}, - {file = "wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1"}, - {file = "wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5"}, - {file = "wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2"}, - {file = "wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89"}, - {file = "wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77"}, - {file = "wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a"}, - {file = "wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0"}, - {file = "wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba"}, - {file = "wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd"}, - {file = "wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828"}, - {file = "wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9"}, - {file = "wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396"}, - {file = "wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc"}, - {file = "wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe"}, - {file = "wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c"}, - {file = "wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6"}, - {file = "wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0"}, - {file = "wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77"}, - {file = "wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7"}, - {file = "wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277"}, - {file = "wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d"}, - {file = "wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa"}, - {file = "wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050"}, - {file = "wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8"}, - {file = "wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb"}, - {file = "wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16"}, - {file = "wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39"}, - {file = "wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235"}, - {file = "wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c"}, - {file = "wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b"}, - {file = "wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa"}, - {file = "wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7"}, - {file = "wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4"}, - {file = "wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10"}, - {file = "wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6"}, - {file = "wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58"}, - {file = "wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a"}, - {file = "wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067"}, - {file = "wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454"}, - {file = "wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e"}, - {file = "wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f"}, - {file = "wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056"}, - {file = "wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804"}, - {file = "wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977"}, - {file = "wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116"}, - {file = "wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6"}, - {file = "wrapt-1.17.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:70d86fa5197b8947a2fa70260b48e400bf2ccacdcab97bb7de47e3d1e6312225"}, - {file = "wrapt-1.17.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:df7d30371a2accfe4013e90445f6388c570f103d61019b6b7c57e0265250072a"}, - {file = "wrapt-1.17.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:caea3e9c79d5f0d2c6d9ab96111601797ea5da8e6d0723f77eabb0d4068d2b2f"}, - {file = "wrapt-1.17.3-cp38-cp38-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:758895b01d546812d1f42204bd443b8c433c44d090248bf22689df673ccafe00"}, - {file = "wrapt-1.17.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02b551d101f31694fc785e58e0720ef7d9a10c4e62c1c9358ce6f63f23e30a56"}, - {file = "wrapt-1.17.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:656873859b3b50eeebe6db8b1455e99d90c26ab058db8e427046dbc35c3140a5"}, - {file = "wrapt-1.17.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a9a2203361a6e6404f80b99234fe7fb37d1fc73487b5a78dc1aa5b97201e0f22"}, - {file = "wrapt-1.17.3-cp38-cp38-win32.whl", hash = "sha256:55cbbc356c2842f39bcc553cf695932e8b30e30e797f961860afb308e6b1bb7c"}, - {file = "wrapt-1.17.3-cp38-cp38-win_amd64.whl", hash = "sha256:ad85e269fe54d506b240d2d7b9f5f2057c2aa9a2ea5b32c66f8902f768117ed2"}, - {file = "wrapt-1.17.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:30ce38e66630599e1193798285706903110d4f057aab3168a34b7fdc85569afc"}, - {file = "wrapt-1.17.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:65d1d00fbfb3ea5f20add88bbc0f815150dbbde3b026e6c24759466c8b5a9ef9"}, - {file = "wrapt-1.17.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a7c06742645f914f26c7f1fa47b8bc4c91d222f76ee20116c43d5ef0912bba2d"}, - {file = "wrapt-1.17.3-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7e18f01b0c3e4a07fe6dfdb00e29049ba17eadbc5e7609a2a3a4af83ab7d710a"}, - {file = "wrapt-1.17.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f5f51a6466667a5a356e6381d362d259125b57f059103dd9fdc8c0cf1d14139"}, - {file = "wrapt-1.17.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:59923aa12d0157f6b82d686c3fd8e1166fa8cdfb3e17b42ce3b6147ff81528df"}, - {file = "wrapt-1.17.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:46acc57b331e0b3bcb3e1ca3b421d65637915cfcd65eb783cb2f78a511193f9b"}, - {file = "wrapt-1.17.3-cp39-cp39-win32.whl", hash = "sha256:3e62d15d3cfa26e3d0788094de7b64efa75f3a53875cdbccdf78547aed547a81"}, - {file = "wrapt-1.17.3-cp39-cp39-win_amd64.whl", hash = "sha256:1f23fa283f51c890eda8e34e4937079114c74b4c81d2b2f1f1d94948f5cc3d7f"}, - {file = "wrapt-1.17.3-cp39-cp39-win_arm64.whl", hash = "sha256:24c2ed34dc222ed754247a2702b1e1e89fdbaa4016f324b4b8f1a802d4ffe87f"}, - {file = "wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22"}, - {file = "wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0"}, -] - -[[package]] -name = "xlsxwriter" -version = "3.2.9" -description = "A Python module for creating Excel XLSX files." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "xlsxwriter-3.2.9-py3-none-any.whl", hash = "sha256:9a5db42bc5dff014806c58a20b9eae7322a134abb6fce3c92c181bfb275ec5b3"}, - {file = "xlsxwriter-3.2.9.tar.gz", hash = "sha256:254b1c37a368c444eac6e2f867405cc9e461b0ed97a3233b2ac1e574efb4140c"}, -] - -[[package]] -name = "xmlsec" -version = "1.3.14" -description = "Python bindings for the XML Security Library" -optional = false -python-versions = ">=3.5" -groups = ["main"] -files = [ - {file = "xmlsec-1.3.14-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4dea6df3ffcb65d0b215678c3a0fe7bbc66785d6eae81291296e372498bad43a"}, - {file = "xmlsec-1.3.14-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fa1311f7489d050dde9028f5a2b5849c2927bb09c9a93491cb2f28fdc563912"}, - {file = "xmlsec-1.3.14-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28cd9f513cf01dc0c5b9d9f0728714ecde2e7f46b3b6f63de91f4ae32f3008b3"}, - {file = "xmlsec-1.3.14-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:77749b338503fb6e151052c664064b34264f4168e2cb0cca1de78b7e5312a783"}, - {file = "xmlsec-1.3.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4af81ce8044862ec865782efd353d22abdcd95b92364eef3c934de57ae6d5852"}, - {file = "xmlsec-1.3.14-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cf35a25be3eb6263b2e0544ba26294651113fab79064f994d347a2ca5973e8e2"}, - {file = "xmlsec-1.3.14-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:004e8a82e26728bf8a60f8ece1ef3ffafdac30ef538139dfe28870e8503ca64a"}, - {file = "xmlsec-1.3.14-cp310-cp310-win32.whl", hash = "sha256:e6cbc914d77678db0c8bc39e723d994174633d18f9d6be4665ec29cce978a96d"}, - {file = "xmlsec-1.3.14-cp310-cp310-win_amd64.whl", hash = "sha256:4922afa9234d1c5763950b26c328a5320019e55eb6000272a79dfe54fee8e704"}, - {file = "xmlsec-1.3.14-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7799a9ff3593f9dd43464e18b1a621640bffc40456c47c23383727f937dca7fc"}, - {file = "xmlsec-1.3.14-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1fe23c2dd5f5dbcb24f40e2c1061e2672a32aabee7cf8ac5337036a485607d72"}, - {file = "xmlsec-1.3.14-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0be3b7a28e54a03b87faf07fb3c6dc3e50a2c79b686718c3ad08300b8bf6bb67"}, - {file = "xmlsec-1.3.14-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48e894ad3e7de373f56efc09d6a56f7eae73a8dd4cec8943313134849e9c6607"}, - {file = "xmlsec-1.3.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:204d3c586b8bd6f02a5d4c59850a8157205569d40c32567f49576fa5795d897d"}, - {file = "xmlsec-1.3.14-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6679cec780386d848e7351d4b0de92c4483289ea4f0a2187e216159f939a4c6b"}, - {file = "xmlsec-1.3.14-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c4d41c83c8a2b8d8030204391ebeb6174fbdb044f0331653c4b5a4ce4150bcc0"}, - {file = "xmlsec-1.3.14-cp311-cp311-win32.whl", hash = "sha256:df4aa0782a53032fd35e18dcd6d328d6126324bfcfdef0cb5c2856f25b4b6f94"}, - {file = "xmlsec-1.3.14-cp311-cp311-win_amd64.whl", hash = "sha256:1072878301cb9243a54679e0520e6a5be2266c07a28b0ecef9e029d05a90ffcd"}, - {file = "xmlsec-1.3.14-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1eb3dcf244a52f796377112d8f238dbb522eb87facffb498425dc8582a84a6bf"}, - {file = "xmlsec-1.3.14-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:330147ce59fbe56a9be5b2085d739c55a569f112576b3f1b33681f87416eaf33"}, - {file = "xmlsec-1.3.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed4034939d8566ccdcd3b4e4f23c63fd807fb8763ae5668d59a19e11640a8242"}, - {file = "xmlsec-1.3.14-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a98eadfcb0c3b23ccceb7a2f245811f8d784bd287640dcfe696a26b9db1e2fc0"}, - {file = "xmlsec-1.3.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86ff7b2711557c1087b72b0a1a88d82eafbf2a6d38b97309a6f7101d4a7041c3"}, - {file = "xmlsec-1.3.14-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:774d5d1e45f07f953c1cc14fd055c1063f0725f7248b6b0e681f59fd8638934d"}, - {file = "xmlsec-1.3.14-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bd10ca3201f164482775a7ce61bf7ee9aade2e7d032046044dd0f6f52c91d79d"}, - {file = "xmlsec-1.3.14-cp312-cp312-win32.whl", hash = "sha256:19c86bab1498e4c2e56d8e2c878f461ccb6e56b67fd7522b0c8fda46d8910781"}, - {file = "xmlsec-1.3.14-cp312-cp312-win_amd64.whl", hash = "sha256:d0762f4232bce2c7f6c0af329db8b821b4460bbe123a2528fb5677d03db7a4b5"}, - {file = "xmlsec-1.3.14-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:03ccba7dacf197850de954666af0221c740a5de631a80136362a1559223fab75"}, - {file = "xmlsec-1.3.14-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c12900e1903e289deb84eb893dca88591d6884d3e3cda4fb711b8812118416e8"}, - {file = "xmlsec-1.3.14-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6566434e2e5c58e472362a6187f208601f1627a148683a6f92bd16479f1d9e20"}, - {file = "xmlsec-1.3.14-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:2401e162aaab7d9416c3405bac7a270e5f370988a0f1f46f0f29b735edba87e1"}, - {file = "xmlsec-1.3.14-cp36-cp36m-win32.whl", hash = "sha256:ba3b39c493e3b04354615068a3218f30897fcc2f42c6d8986d0c1d63aca87782"}, - {file = "xmlsec-1.3.14-cp36-cp36m-win_amd64.whl", hash = "sha256:4edd8db4df04bbac9c4a5ab4af855b74fe2bf2c248d07cac2e6d92a485f1a685"}, - {file = "xmlsec-1.3.14-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b6dd86f440fec9242515c64f0be93fec8b4289287db1f6de2651eee9995aaecb"}, - {file = "xmlsec-1.3.14-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad1634cabe0915fe2a12e142db0ed2daf5be80cbe3891a2cecbba0750195cc6b"}, - {file = "xmlsec-1.3.14-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dba457ff87c39cbae3c5020475a728d24bbd9d00376df9af9724cd3bb59ff07a"}, - {file = "xmlsec-1.3.14-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12d90059308bb0c1b94bde065784e6852999d08b91bcb2048c17e62b954acb07"}, - {file = "xmlsec-1.3.14-cp37-cp37m-win32.whl", hash = "sha256:ce4e165a1436697e5e39587c4fba24db4545a5c9801e0d749f1afd09ad3ab901"}, - {file = "xmlsec-1.3.14-cp37-cp37m-win_amd64.whl", hash = "sha256:7e8e0171916026cbe8e2022c959558d02086655fd3c3466f2bc0451b09cf9ee8"}, - {file = "xmlsec-1.3.14-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c42735cc68fdb4c6065cf0a0701dfff3a12a1734c63a36376349af9a5481f27b"}, - {file = "xmlsec-1.3.14-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:38e035bf48300b7dbde2dd01d3b8569f8584fc9c73809be13886e6b6c77b74fb"}, - {file = "xmlsec-1.3.14-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73eabf5ef58189d81655058cf328c1dfa9893d89f1bff5fc941481f08533f338"}, - {file = "xmlsec-1.3.14-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bddd2a2328b4e08c8a112e06cf2cd2b4d281f4ad94df15b4cef18f06cdc49d78"}, - {file = "xmlsec-1.3.14-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57fed3bc7943681c9ed4d2221600ab440f060d8d1a8f92f346f2b41effe175b8"}, - {file = "xmlsec-1.3.14-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:147934bd39dfd840663fb6b920ea9201455fa886427975713f1b42d9f20b9b29"}, - {file = "xmlsec-1.3.14-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e732a75fcb6b84872b168f972fbbf3749baf76308635f14015d1d35ed0c5719c"}, - {file = "xmlsec-1.3.14-cp38-cp38-win32.whl", hash = "sha256:b109cdf717257fd4daa77c1d3ec8a3fb2a81318a6d06a36c55a8a53ae381ae5e"}, - {file = "xmlsec-1.3.14-cp38-cp38-win_amd64.whl", hash = "sha256:b7ba2ea38e3d9efa520b14f3c0b7d99a7c055244ae5ba8bc9f4ca73b18f3a215"}, - {file = "xmlsec-1.3.14-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1b9b5de6bc69fdec23147e5f712cb05dc86df105462f254f140d743cc680cc7b"}, - {file = "xmlsec-1.3.14-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:82ac81deb7d7bf5cc8a748148948e5df5386597ff43fb92ec651cc5c7addb0e7"}, - {file = "xmlsec-1.3.14-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bae37b2920115cf00759ee9fb7841cbdebcef3a8a92734ab93ae8fa41ac581d"}, - {file = "xmlsec-1.3.14-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4fac2a787ae3b9fb761f9aec6b9f10f2d1c1b87abb574ebd8ff68435bdc97e3d"}, - {file = "xmlsec-1.3.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34c61ec0c0e70fda710290ae74b9efe1928d9242ed82c4eecf97aa696cff68e6"}, - {file = "xmlsec-1.3.14-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:995e87acecc263a2f6f2aa3cc204268f651cac8f4d7a2047f11b2cd49979cc38"}, - {file = "xmlsec-1.3.14-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2f84a1c509c52773365645a87949081ee9ea9c535cd452048cc8ca4ad3b45666"}, - {file = "xmlsec-1.3.14-cp39-cp39-win32.whl", hash = "sha256:7882963e9cb9c0bd0e8c2715a29159a366417ff4a30d8baf42b05bc5cf249446"}, - {file = "xmlsec-1.3.14-cp39-cp39-win_amd64.whl", hash = "sha256:a487c3d144f791c32f5e560aa27a705fba23171728b8a8511f36de053ff6bc93"}, - {file = "xmlsec-1.3.14.tar.gz", hash = "sha256:934f804f2f895bcdb86f1eaee236b661013560ee69ec108d29cdd6e5f292a2d9"}, -] - -[package.dependencies] -lxml = ">=3.8" - -[[package]] -name = "xmltodict" -version = "1.0.2" -description = "Makes working with XML feel like you are working with JSON" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "xmltodict-1.0.2-py3-none-any.whl", hash = "sha256:62d0fddb0dcbc9f642745d8bbf4d81fd17d6dfaec5a15b5c1876300aad92af0d"}, - {file = "xmltodict-1.0.2.tar.gz", hash = "sha256:54306780b7c2175a3967cad1db92f218207e5bc1aba697d887807c0fb68b7649"}, -] - -[package.extras] -test = ["pytest", "pytest-cov"] - -[[package]] -name = "yarl" -version = "1.22.0" -description = "Yet another URL library" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "yarl-1.22.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e"}, - {file = "yarl-1.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f"}, - {file = "yarl-1.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf"}, - {file = "yarl-1.22.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a"}, - {file = "yarl-1.22.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c"}, - {file = "yarl-1.22.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147"}, - {file = "yarl-1.22.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb"}, - {file = "yarl-1.22.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6"}, - {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0"}, - {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda"}, - {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc"}, - {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737"}, - {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467"}, - {file = "yarl-1.22.0-cp310-cp310-win32.whl", hash = "sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea"}, - {file = "yarl-1.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca"}, - {file = "yarl-1.22.0-cp310-cp310-win_arm64.whl", hash = "sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b"}, - {file = "yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511"}, - {file = "yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6"}, - {file = "yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028"}, - {file = "yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d"}, - {file = "yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503"}, - {file = "yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65"}, - {file = "yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e"}, - {file = "yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d"}, - {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7"}, - {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967"}, - {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed"}, - {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6"}, - {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e"}, - {file = "yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca"}, - {file = "yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b"}, - {file = "yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376"}, - {file = "yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f"}, - {file = "yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2"}, - {file = "yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74"}, - {file = "yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df"}, - {file = "yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb"}, - {file = "yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2"}, - {file = "yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82"}, - {file = "yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a"}, - {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124"}, - {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa"}, - {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7"}, - {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d"}, - {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520"}, - {file = "yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8"}, - {file = "yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c"}, - {file = "yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74"}, - {file = "yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53"}, - {file = "yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a"}, - {file = "yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c"}, - {file = "yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601"}, - {file = "yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a"}, - {file = "yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df"}, - {file = "yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2"}, - {file = "yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b"}, - {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273"}, - {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a"}, - {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d"}, - {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02"}, - {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67"}, - {file = "yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95"}, - {file = "yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d"}, - {file = "yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b"}, - {file = "yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10"}, - {file = "yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3"}, - {file = "yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9"}, - {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f"}, - {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0"}, - {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e"}, - {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708"}, - {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f"}, - {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d"}, - {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8"}, - {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5"}, - {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f"}, - {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62"}, - {file = "yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03"}, - {file = "yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249"}, - {file = "yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b"}, - {file = "yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4"}, - {file = "yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683"}, - {file = "yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b"}, - {file = "yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e"}, - {file = "yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590"}, - {file = "yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2"}, - {file = "yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da"}, - {file = "yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784"}, - {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b"}, - {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694"}, - {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d"}, - {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd"}, - {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da"}, - {file = "yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2"}, - {file = "yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79"}, - {file = "yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33"}, - {file = "yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1"}, - {file = "yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca"}, - {file = "yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53"}, - {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c"}, - {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf"}, - {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face"}, - {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b"}, - {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486"}, - {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138"}, - {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a"}, - {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529"}, - {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093"}, - {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c"}, - {file = "yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e"}, - {file = "yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27"}, - {file = "yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1"}, - {file = "yarl-1.22.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3aa27acb6de7a23785d81557577491f6c38a5209a254d1191519d07d8fe51748"}, - {file = "yarl-1.22.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:af74f05666a5e531289cb1cc9c883d1de2088b8e5b4de48004e5ca8a830ac859"}, - {file = "yarl-1.22.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:62441e55958977b8167b2709c164c91a6363e25da322d87ae6dd9c6019ceecf9"}, - {file = "yarl-1.22.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b580e71cac3f8113d3135888770903eaf2f507e9421e5697d6ee6d8cd1c7f054"}, - {file = "yarl-1.22.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e81fda2fb4a07eda1a2252b216aa0df23ebcd4d584894e9612e80999a78fd95b"}, - {file = "yarl-1.22.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:99b6fc1d55782461b78221e95fc357b47ad98b041e8e20f47c1411d0aacddc60"}, - {file = "yarl-1.22.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:088e4e08f033db4be2ccd1f34cf29fe994772fb54cfe004bbf54db320af56890"}, - {file = "yarl-1.22.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e4e1f6f0b4da23e61188676e3ed027ef0baa833a2e633c29ff8530800edccba"}, - {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:84fc3ec96fce86ce5aa305eb4aa9358279d1aa644b71fab7b8ed33fe3ba1a7ca"}, - {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5dbeefd6ca588b33576a01b0ad58aa934bc1b41ef89dee505bf2932b22ddffba"}, - {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:14291620375b1060613f4aab9ebf21850058b6b1b438f386cc814813d901c60b"}, - {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:a4fcfc8eb2c34148c118dfa02e6427ca278bfd0f3df7c5f99e33d2c0e81eae3e"}, - {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:029866bde8d7b0878b9c160e72305bbf0a7342bcd20b9999381704ae03308dc8"}, - {file = "yarl-1.22.0-cp39-cp39-win32.whl", hash = "sha256:4dcc74149ccc8bba31ce1944acee24813e93cfdee2acda3c172df844948ddf7b"}, - {file = "yarl-1.22.0-cp39-cp39-win_amd64.whl", hash = "sha256:10619d9fdee46d20edc49d3479e2f8269d0779f1b031e6f7c2aa1c76be04b7ed"}, - {file = "yarl-1.22.0-cp39-cp39-win_arm64.whl", hash = "sha256:dd7afd3f8b0bfb4e0d9fc3c31bfe8a4ec7debe124cfd90619305def3c8ca8cd2"}, - {file = "yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff"}, - {file = "yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71"}, -] - -[package.dependencies] -idna = ">=2.0" -multidict = ">=4.0" -propcache = ">=0.2.1" - -[[package]] -name = "zipp" -version = "3.23.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, - {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] -type = ["pytest-mypy"] - -[[package]] -name = "zope-event" -version = "6.1" -description = "Very basic event publishing system" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "zope_event-6.1-py3-none-any.whl", hash = "sha256:0ca78b6391b694272b23ec1335c0294cc471065ed10f7f606858fc54566c25a0"}, - {file = "zope_event-6.1.tar.gz", hash = "sha256:6052a3e0cb8565d3d4ef1a3a7809336ac519bc4fe38398cb8d466db09adef4f0"}, -] - -[package.extras] -docs = ["Sphinx"] -test = ["zope.testrunner (>=6.4)"] - -[[package]] -name = "zope-interface" -version = "8.2" -description = "Interfaces for Python" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "zope_interface-8.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:788c293f3165964ec6527b2d861072c68eef53425213f36d3893ebee89a89623"}, - {file = "zope_interface-8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9a4e785097e741a1c953b3970ce28f2823bd63c00adc5d276f2981dd66c96c15"}, - {file = "zope_interface-8.2-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:16c69da19a06566664ddd4785f37cad5693a51d48df1515d264c20d005d322e2"}, - {file = "zope_interface-8.2-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c31acfa3d7cde48bec45701b0e1f4698daffc378f559bfb296837d8c834732f6"}, - {file = "zope_interface-8.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0723507127f8269b8f3f22663168f717e9c9742107d1b6c9f419df561b71aa6d"}, - {file = "zope_interface-8.2-cp310-cp310-win_amd64.whl", hash = "sha256:3bf73a910bb27344def2d301a03329c559a79b308e1e584686b74171d736be4e"}, - {file = "zope_interface-8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c65ade7ea85516e428651048489f5e689e695c79188761de8c622594d1e13322"}, - {file = "zope_interface-8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1ef4b43659e1348f35f38e7d1a6bbc1682efde239761f335ffc7e31e798b65b"}, - {file = "zope_interface-8.2-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:dfc4f44e8de2ff4eba20af4f0a3ca42d3c43ab24a08e49ccd8558b7a4185b466"}, - {file = "zope_interface-8.2-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8f094bfb49179ec5dc9981cb769af1275702bd64720ef94874d9e34da1390d4c"}, - {file = "zope_interface-8.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d2bb8e7364e18f083bf6744ccf30433b2a5f236c39c95df8514e3c13007098ce"}, - {file = "zope_interface-8.2-cp311-cp311-win_amd64.whl", hash = "sha256:6f4b4dfcfdfaa9177a600bb31cebf711fdb8c8e9ed84f14c61c420c6aa398489"}, - {file = "zope_interface-8.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:624b6787fc7c3e45fa401984f6add2c736b70a7506518c3b537ffaacc4b29d4c"}, - {file = "zope_interface-8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bc9ded9e97a0ed17731d479596ed1071e53b18e6fdb2fc33af1e43f5fd2d3aaa"}, - {file = "zope_interface-8.2-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:532367553e4420c80c0fc0cabcc2c74080d495573706f66723edee6eae53361d"}, - {file = "zope_interface-8.2-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2bf9cf275468bafa3c72688aad8cfcbe3d28ee792baf0b228a1b2d93bd1d541a"}, - {file = "zope_interface-8.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0009d2d3c02ea783045d7804da4fd016245e5c5de31a86cebba66dd6914d59a2"}, - {file = "zope_interface-8.2-cp312-cp312-win_amd64.whl", hash = "sha256:845d14e580220ae4544bd4d7eb800f0b6034fe5585fc2536806e0a26c2ee6640"}, - {file = "zope_interface-8.2-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:6068322004a0158c80dfd4708dfb103a899635408c67c3b10e9acec4dbacefec"}, - {file = "zope_interface-8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2499de92e8275d0dd68f84425b3e19e9268cd1fa8507997900fa4175f157733c"}, - {file = "zope_interface-8.2-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f777e68c76208503609c83ca021a6864902b646530a1a39abb9ed310d1100664"}, - {file = "zope_interface-8.2-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b05a919fdb0ed6ea942e5a7800e09a8b6cdae6f98fee1bef1c9d1a3fc43aaa0"}, - {file = "zope_interface-8.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ccc62b5712dd7bd64cfba3ee63089fb11e840f5914b990033beeae3b2180b6cb"}, - {file = "zope_interface-8.2-cp313-cp313-win_amd64.whl", hash = "sha256:34f877d1d3bb7565c494ed93828fa6417641ca26faf6e8f044e0d0d500807028"}, - {file = "zope_interface-8.2-cp314-cp314-macosx_10_9_x86_64.whl", hash = "sha256:46c7e4e8cbc698398a67e56ca985d19cb92365b4aafbeb6a712e8c101090f4cb"}, - {file = "zope_interface-8.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a87fc7517f825a97ff4a4ca4c8a950593c59e0f8e7bfe1b6f898a38d5ba9f9cf"}, - {file = "zope_interface-8.2-cp314-cp314-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:ccf52f7d44d669203c2096c1a0c2c15d52e36b2e7a9413df50f48392c7d4d080"}, - {file = "zope_interface-8.2-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:aae807efc7bd26302eb2fea05cd6de7d59269ed6ae23a6de1ee47add6de99b8c"}, - {file = "zope_interface-8.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05a0e42d6d830f547e114de2e7cd15750dc6c0c78f8138e6c5035e51ddfff37c"}, - {file = "zope_interface-8.2-cp314-cp314-win_amd64.whl", hash = "sha256:561ce42390bee90bae51cf1c012902a8033b2aaefbd0deed81e877562a116d48"}, - {file = "zope_interface-8.2.tar.gz", hash = "sha256:afb20c371a601d261b4f6edb53c3c418c249db1a9717b0baafc9a9bb39ba1224"}, -] - -[package.extras] -docs = ["Sphinx", "furo", "repoze.sphinx.autointerface"] -test = ["coverage[toml]", "zope.event", "zope.testing"] -testing = ["coverage[toml]", "zope.event", "zope.testing"] - -[[package]] -name = "zstd" -version = "1.5.7.3" -description = "ZSTD Bindings for Python" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "zstd-1.5.7.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e72b353870286648a63261437b75f297e2967a26f210da4dfa4c08949935de7a"}, - {file = "zstd-1.5.7.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:26aff5f24caeffde35f1b757499e935bc60a8e0d9e1ea8bde05dcf7d53df9325"}, - {file = "zstd-1.5.7.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:586a820fbd06e3d9a9d9def572e779254bf8dee7406b8c6dc44eff6807d60c6d"}, - {file = "zstd-1.5.7.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:35a147b10fd16ebb3a2595e361780388feb8f336d70772a05dfb7a8348a47bfd"}, - {file = "zstd-1.5.7.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:c2a80c51e2175ffcd6f08b2a4c9fbc121aad69fbbcebb3364e783a96d0488fda"}, - {file = "zstd-1.5.7.3-cp27-cp27mu-manylinux_2_4_i686.whl", hash = "sha256:5f20f74a782f3296d1585d9bbc49d422e339b154c66398c74537e433446c51ba"}, - {file = "zstd-1.5.7.3-cp27-cp27mu-manylinux_2_4_x86_64.whl", hash = "sha256:2550c2e6bfbff0904f28821005f176bfdaec1872d60053665a284fb0254a10e7"}, - {file = "zstd-1.5.7.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:76f3535616887a1a38e8c6d0de693a23c5bb1f190651eb20d96bfc8e4ab706a0"}, - {file = "zstd-1.5.7.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:67507937e8e4c2a8dfed8e7fa77f4043ec9e6e831a5faebf0f99138b1a25ccbd"}, - {file = "zstd-1.5.7.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:bd0a2309c524608ce7b940abcc9f8eb5447c6ea2c834a630e0081211ab9d40ec"}, - {file = "zstd-1.5.7.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:2b497306580d544406b5414c8485c4037a9283ad2ca6ae4ccdf3732c9563141d"}, - {file = "zstd-1.5.7.3-cp310-cp310-manylinux_2_4_i686.whl", hash = "sha256:e9939a98ea946d1f9e8f9fecc940ae939b8e9e5ef9d71b104f7843567d764f30"}, - {file = "zstd-1.5.7.3-cp310-cp310-manylinux_2_4_x86_64.whl", hash = "sha256:d32c0fe8f6b805b7cbeaade462b094a843e84d893d8c6f66ab705e8777cc1850"}, - {file = "zstd-1.5.7.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:8aa33b1ef24602b2ef1e8aa67ea3c8f821854a4dbf70c3c8c46b96b54b6ceb5d"}, - {file = "zstd-1.5.7.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1bd69fa9c4c97fd04206c919dedbf9f75f544ebb77880db51a13c1e3802cd655"}, - {file = "zstd-1.5.7.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:aee96742a64ede2e35dc0316ef0cd1e50089e889ce77e82ca8edf40174a1439c"}, - {file = "zstd-1.5.7.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5ac207573d2815a51f4f4fd4e255408396491729a01f690b9f5fb672d39e5610"}, - {file = "zstd-1.5.7.3-cp310-cp310-win32.whl", hash = "sha256:04e62e4f9eba79699d072d3c96731ed4aff99f1d334eb967489b091186a6078f"}, - {file = "zstd-1.5.7.3-cp310-cp310-win_amd64.whl", hash = "sha256:0794b23b9950af240888087d2bd5943aa4be67273ba32cdafabdc5704778b90e"}, - {file = "zstd-1.5.7.3-cp310-cp310-win_arm64.whl", hash = "sha256:7827fd4901f3e71a7a755d26719549658f08e04fdf0870a952ed08e71b484435"}, - {file = "zstd-1.5.7.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a3c1781a24e2ced2c0ddee11d45b1f04018b03615eeb622a62eca4d56d3358a"}, - {file = "zstd-1.5.7.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a6c7c81056362b60a04baa34632e713d596662a860ec34efd8e9b109c10e6ec7"}, - {file = "zstd-1.5.7.3-cp311-cp311-manylinux_2_14_x86_64.whl", hash = "sha256:e564f34a55effc7d654eb293468edc80b64d476b0f899f82760ecd8323223ff5"}, - {file = "zstd-1.5.7.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:fbc49a57188184931d5e3c9f1133cad7eea5a370a9e9418fb8122d58c14340a5"}, - {file = "zstd-1.5.7.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:d121d3e63722819e1fe5effbcd9628d8a7cfea0cddabcc5bb37ea861a6a83424"}, - {file = "zstd-1.5.7.3-cp311-cp311-manylinux_2_4_i686.whl", hash = "sha256:621f2e7ca8e9eb52a83eb9c91ec3cd283d87591bf75cc658de486b65f44742c7"}, - {file = "zstd-1.5.7.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:c1950fcae690ba32d0f31702b335c548fb42547821565925e48576afdad774a5"}, - {file = "zstd-1.5.7.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bac4f0d03da69115878bedbfa03c4a3f64364e8396b432028c4ce0f05141a0fb"}, - {file = "zstd-1.5.7.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:da0ab134b7fd28023dedf013751ca850de300a090eb11f689d2a1c178c87d9dc"}, - {file = "zstd-1.5.7.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b9923175842ee8f7602ec9cc578f5fc396896f0e8460d3ac9a5adc3cea77244e"}, - {file = "zstd-1.5.7.3-cp311-cp311-win32.whl", hash = "sha256:0612b604948d7b58aecc6788c7ceb53c5f21d94a155bb6ea9bd0f54ffa43725d"}, - {file = "zstd-1.5.7.3-cp311-cp311-win_amd64.whl", hash = "sha256:5b7f8c81b2bd3b62c0345242247d484cafa4b518d59d18619813d9225af5c5c3"}, - {file = "zstd-1.5.7.3-cp311-cp311-win_arm64.whl", hash = "sha256:ea112e3acd9e1765adca35df7b54ac75b36194290f64ea03a3a59664209c8527"}, - {file = "zstd-1.5.7.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:01a39efb0eeab7cc45cb308618233b624b0840d5e16dcf85456b6cca0592f203"}, - {file = "zstd-1.5.7.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7a8e8838cf35fa3987bfe1958584cc22e1797efce8e155a63544b4144fc671f8"}, - {file = "zstd-1.5.7.3-cp312-cp312-manylinux_2_14_i686.whl", hash = "sha256:f3920ac1d1cc7e9f252f3e29f217fe3cd36f2191bb3dbcae826c29e189b7ad54"}, - {file = "zstd-1.5.7.3-cp312-cp312-manylinux_2_14_x86_64.whl", hash = "sha256:143f9062953fb5590cbd47c1040d357336742c79696bf90b6d5b835279a68304"}, - {file = "zstd-1.5.7.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36d1fd8647e47e1f21b345e192f1a279e925678c23dad8236b547d04456cd699"}, - {file = "zstd-1.5.7.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1538db419afa62773cf534fc7f3009ff59ecf55ecee4e889587ac2ef0010ed8"}, - {file = "zstd-1.5.7.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c5efd16adb092e2a547a7d51cfdaf6fd5680528227684c5bafc7669ab4a55f41"}, - {file = "zstd-1.5.7.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:39b3438e64637d80a5b1860526903b92020acb9bae9ceb5adffd9838c1441328"}, - {file = "zstd-1.5.7.3-cp312-cp312-win32.whl", hash = "sha256:cbf48c53461e224ffc2490cfe5120a1ff40d14c84d2b512c6d6d99fc91685cf3"}, - {file = "zstd-1.5.7.3-cp312-cp312-win_amd64.whl", hash = "sha256:943a189910f2fea997462e3e4d7fbf727a06d231ef801ebee557b1c87568981c"}, - {file = "zstd-1.5.7.3-cp312-cp312-win_arm64.whl", hash = "sha256:85c4d508f8109afa7c51c4960626c3325af2cf1e442c6c36ebfea15d04757e3f"}, - {file = "zstd-1.5.7.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b2455e56f1d265dacbd450510b8c2f632a5d8d92c23282e7723fb04af37001a2"}, - {file = "zstd-1.5.7.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3486dc4f1b4e52bb059f8eec1f31daa3e540062c0f522f221782cf132a8bc9a8"}, - {file = "zstd-1.5.7.3-cp313-cp313-manylinux_2_14_i686.whl", hash = "sha256:1cb47bf10ffcb6a782edacfe758da2c94879f7e89c6628feb3f1254daf8cc596"}, - {file = "zstd-1.5.7.3-cp313-cp313-manylinux_2_14_x86_64.whl", hash = "sha256:07b1378d1230ddeea8773f99d7518a3060e6468c76edd502057cb795fe278d7e"}, - {file = "zstd-1.5.7.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ee34317f013e3405108f5baea53502159809cfc4510598d614257525500c70d"}, - {file = "zstd-1.5.7.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c19127ca2c79855376a34a2d7a6969408094b25c1f44485b0373eba4be851b98"}, - {file = "zstd-1.5.7.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e79cae70dd08cb247391312463085c624c0302e8c860d13f87f4c76502d8202"}, - {file = "zstd-1.5.7.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0e83e91e5daf89037c737f5529da0f80da80a78a6ad0b1d70a09860eb267dea4"}, - {file = "zstd-1.5.7.3-cp313-cp313-win32.whl", hash = "sha256:2283f3bb910c028e1b9fe76b834016012ab021025a0ea197e27a1333f85e3031"}, - {file = "zstd-1.5.7.3-cp313-cp313-win_amd64.whl", hash = "sha256:3ad5fe4c36bab5dfa5a4b8d050bd07c50c1e69f94d381bc65337ab14cd69e5b1"}, - {file = "zstd-1.5.7.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e878172b0eb69ac2edc6576eb862e00747c7c25e638fb354630a1ea7cfddf49"}, - {file = "zstd-1.5.7.3-cp313-cp313t-manylinux_2_14_x86_64.whl", hash = "sha256:7e0a7e94d5b63b4cacf2396079ca9584d11f49f87cb4e5aa21f126a8f6b83446"}, - {file = "zstd-1.5.7.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5412c86c34cbaf6906433ef3f2c96c407f208782f06cd3e5f01f066788adb3b8"}, - {file = "zstd-1.5.7.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f94246befb1e473211a298c96e5768f3c63eaad814ac14d160d79ae9858e1d03"}, - {file = "zstd-1.5.7.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:31050e17a1a546fb82c90eee8ee3c30d22b9d0594b5937e69d38b7a5084af2a2"}, - {file = "zstd-1.5.7.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ba8ec5dfd48c86d19f880713246f85d09ee06e8cd17141956258650878000d6"}, - {file = "zstd-1.5.7.3-cp314-cp314-manylinux_2_14_i686.whl", hash = "sha256:3005540ba406157f3e205c998709ab5f8e68b390c658c7c238eb8986092089d5"}, - {file = "zstd-1.5.7.3-cp314-cp314-manylinux_2_14_x86_64.whl", hash = "sha256:3934b54a3b7df039fcd4cf7b0f0a38c86ce44d26321255ffc3fac73d6cdcc59d"}, - {file = "zstd-1.5.7.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e9230cd3e9153e2bed16f332558f8f3f7d869f4d15e8fa3f9c360bfa163a8b4a"}, - {file = "zstd-1.5.7.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bffba70af539f14f9df5367b1add9119f14d5e35b658aef7b765417ea461e0e"}, - {file = "zstd-1.5.7.3-cp314-cp314-win32.whl", hash = "sha256:a006e70c88ab67bb56989e11d820adc7601a6a7ad5558b3c6c690b19a1dadc5b"}, - {file = "zstd-1.5.7.3-cp314-cp314-win_amd64.whl", hash = "sha256:cb4957c330c7b94b0546c7b9529723b49e865608683b9503a251fe793da9d4db"}, - {file = "zstd-1.5.7.3-cp314-cp314-win_arm64.whl", hash = "sha256:a785426081ab7cafe4522876ac771d701766deea9a6d8352e87744da00e6637f"}, - {file = "zstd-1.5.7.3-cp314-cp314t-manylinux_2_14_i686.whl", hash = "sha256:b52ef154793be0399befd742328ec6f5dff95154248d6d18dd65851cf22a1a5f"}, - {file = "zstd-1.5.7.3-cp314-cp314t-manylinux_2_14_x86_64.whl", hash = "sha256:8024a8ba9156b1b2e64e69d147df5ddedeaed107f9da02a3428fd7baf3e5b920"}, - {file = "zstd-1.5.7.3-cp315-cp315-manylinux_2_14_i686.whl", hash = "sha256:31ac7fbacca4759aad4b6abc13bbc05e68788e9e85a968255f7624b3b8db31df"}, - {file = "zstd-1.5.7.3-cp315-cp315-manylinux_2_14_x86_64.whl", hash = "sha256:d03b2927c5843ded4d1319836a33a9c21675d2f86f916a2f234a060d4c67d87c"}, - {file = "zstd-1.5.7.3-cp315-cp315t-manylinux_2_14_i686.whl", hash = "sha256:5dfbf2564eb574fc1f45613ecf28036a82533c3dd70e7bb1c9854168c638da7a"}, - {file = "zstd-1.5.7.3-cp315-cp315t-manylinux_2_14_x86_64.whl", hash = "sha256:7f2f5776b902f41daf7b63e75a9384b0d7c855f824f14dabefc67814b8fa5611"}, - {file = "zstd-1.5.7.3-cp34-cp34m-manylinux_2_4_i686.whl", hash = "sha256:ffbeabcabcb644d29289277f9023aa51c04de71935695f5388da9c8428c81e0f"}, - {file = "zstd-1.5.7.3-cp34-cp34m-manylinux_2_4_x86_64.whl", hash = "sha256:0b891ca9ad84562941367ab7be817b8748df75eb6b7ced23d5b082b4602c1c6e"}, - {file = "zstd-1.5.7.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:925f83e2e749cd7109985bc96835cd2fd814435d74f0d9a1d7c8506166e97592"}, - {file = "zstd-1.5.7.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:57d2ff6b96886aaec2aa4721f7c8e890a8b43b5c4ae4f3737a0733b55cd82daa"}, - {file = "zstd-1.5.7.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:8cd516ba02e0f9e6df1b4a6dc0cd5e66ac6eeb55b15833a70d529aa32eddaa91"}, - {file = "zstd-1.5.7.3-cp35-cp35m-manylinux_2_14_x86_64.whl", hash = "sha256:9f6ea980866f43ff7ef5e41eac54b94f9159b9807f32f691b02ca381b50b76af"}, - {file = "zstd-1.5.7.3-cp35-cp35m-manylinux_2_4_i686.whl", hash = "sha256:3e650ed68b655d55556099aa62f168a352396139a879a94312322a1d02502491"}, - {file = "zstd-1.5.7.3-cp35-cp35m-win32.whl", hash = "sha256:da88b288a2844f04713df89a514dd9dc0e925ee63e119c845aef14ccbcc9183e"}, - {file = "zstd-1.5.7.3-cp35-cp35m-win_amd64.whl", hash = "sha256:96c949e8508f2d4dced3444a3bfb99d51653ac6f28ef0aa1561f5758adc8afed"}, - {file = "zstd-1.5.7.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:7509b11b5f8313e87cce16269e222f89e7e49b51f1e6a3e7454b7c7b599d3211"}, - {file = "zstd-1.5.7.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:fb8aafd47ba73ff50a7994668dbec5c97f26ddcd28c03242d8f8b4138d8c723c"}, - {file = "zstd-1.5.7.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:586efc62d7e93d52d0b3951ef48a4b5181866152061bda1bef49f7ea85ec0d7f"}, - {file = "zstd-1.5.7.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:5030d51631a09a0d7b3e47f928b6234bd78ce8b897a255fc1146e8cf772a8f4d"}, - {file = "zstd-1.5.7.3-cp36-cp36m-manylinux_2_14_x86_64.whl", hash = "sha256:a8d1ee9faa89b21ff03ae3fe8d969e850c60b8c3f8a1389fa585c10eddaa2bb4"}, - {file = "zstd-1.5.7.3-cp36-cp36m-manylinux_2_4_i686.whl", hash = "sha256:4504ba7a9ddd1919e919f81d3ec541313e6826f1f3cad8e3a7ebe29a3ae5cda6"}, - {file = "zstd-1.5.7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:aca7d1fef13f412168ac524307586f0d57f96a89bd7e0620b2f60df3b0066c8d"}, - {file = "zstd-1.5.7.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:12d2925424d02add2f835c7549106151ece9eae262e96aee34af5d84178ba824"}, - {file = "zstd-1.5.7.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:30512cce4108b26ede395ac521c0997c340bd19f177a1c0260bbffcb64861d30"}, - {file = "zstd-1.5.7.3-cp37-cp37m-manylinux_2_4_i686.whl", hash = "sha256:2e6caf5f3084e6473a6dfd15285c47122ba92f4fb97ecfca855adf415603532a"}, - {file = "zstd-1.5.7.3-cp37-cp37m-manylinux_2_4_x86_64.whl", hash = "sha256:927c95b991e81f39b02e42c9b391f2b3569e6dbe29d7fc2dce6ca778475c0934"}, - {file = "zstd-1.5.7.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:2174fd7f588b2eb95a402c3d40f4676370eb50292362a0995295084b8f5d521e"}, - {file = "zstd-1.5.7.3-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:3b05817bfdfc395999b6b3c9ea4f7c05e91bceafc3fc819906d5f0445afa4335"}, - {file = "zstd-1.5.7.3-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:c67f0fcf4348343d25ecd35a44d33b6d31814e9ab3ee8676039de809579905a4"}, - {file = "zstd-1.5.7.3-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:40195c0056841aad6553172963adecf31b6ae1fdb9778d657ce9a2493d1791ee"}, - {file = "zstd-1.5.7.3-cp37-cp37m-win32.whl", hash = "sha256:b6ac3ae562758184fc1570399ea9d269163b488dbb0c4a44701e89f61ca6d1d6"}, - {file = "zstd-1.5.7.3-cp37-cp37m-win_amd64.whl", hash = "sha256:e9f059d9c9f6f13ae78bfa9778755462b3ea53e4a5185941169422dd97c9fd22"}, - {file = "zstd-1.5.7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:99e92b97c97d83e403615c12b644e8616fc7e8a8b4fa0c0558bcb9980baf5c92"}, - {file = "zstd-1.5.7.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a6b4ff0d5704994eb0d7ba2ea0b25acd749bb78a1c325289a8cba7651f0cbbff"}, - {file = "zstd-1.5.7.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:edf4b595ab29a980f6f60fa71c64ab029d9ced97fb9c7c9ae555fe1159d8379d"}, - {file = "zstd-1.5.7.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:3cd48ec1dce8a8a06a3978225b20f28b7764e4191c436277e0abc60539e040da"}, - {file = "zstd-1.5.7.3-cp38-cp38-manylinux_2_4_i686.whl", hash = "sha256:1380ecc510a3885fad326863a7f42b3391560b471aeea60b04f9c1ece439b198"}, - {file = "zstd-1.5.7.3-cp38-cp38-manylinux_2_4_x86_64.whl", hash = "sha256:5fdff5190698e6d48a3facb58085a6c33b62be610f40e80299d975dbc75b32c8"}, - {file = "zstd-1.5.7.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:595d6495e96744fa5c9b78f38e8379f9eebfb97ae4f7ecc2639af4fd51459e07"}, - {file = "zstd-1.5.7.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:9bc3d6b7f2dec391b7539a0f43deb07bca1d68867082a07a286c2237f16390fd"}, - {file = "zstd-1.5.7.3-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:b8e62d533281946100c023a1168bd8935db6452bdd0f0b776afe8e80255e74c3"}, - {file = "zstd-1.5.7.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3a5dcc7ddcd56f131bee612b5feadd9b65e3996c0f4c6a485e2b2f20e7a324de"}, - {file = "zstd-1.5.7.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dbb497482dd63abe72a209345dbafa52817bd484c1d08139da080c14b1dadc7b"}, - {file = "zstd-1.5.7.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a599489d4e7e794981536521ee5dcfa61b0a641996409669b9aba5400b5cff83"}, - {file = "zstd-1.5.7.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:4a7ec28ca27fc347d7325eeb06d66cd2649846d5bfe77b18beed38d1870dd876"}, - {file = "zstd-1.5.7.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:703481b41e5b3d33cd4e6a0b7116e8bc33a712aba1526d5fcad3e4303dd70fa1"}, - {file = "zstd-1.5.7.3-cp39-cp39-manylinux_2_4_i686.whl", hash = "sha256:61b0707c090d59ba879eac4b475562c5b9c1b375d0419d78fb398f156037f7df"}, - {file = "zstd-1.5.7.3-cp39-cp39-manylinux_2_4_x86_64.whl", hash = "sha256:7090ac97b14dea2969ba1ed427b38efe137efcdf556dc8740d3e035b04cbc8b4"}, - {file = "zstd-1.5.7.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:5204bf9f3f2936ee3a28bfe43a57b78f88439c1777197295a0661d6de38caa80"}, - {file = "zstd-1.5.7.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:431d4fecf764c305f29c1b9117d0d2ec5eb5523fc81516f1ee82509cb3b8e088"}, - {file = "zstd-1.5.7.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:c2f213a32ab5e90bf165717f05fc1e3c214eeca7b6a33311e2397d89879c2f87"}, - {file = "zstd-1.5.7.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3f87d617dac84b571bb74dc9d6905c66906dca982143adbe8e497ba2ce888cca"}, - {file = "zstd-1.5.7.3-cp39-cp39-win32.whl", hash = "sha256:9511957b5b8b5c0d4e737dff3a330a445a44005e09278bb8c799a76eb7f99d90"}, - {file = "zstd-1.5.7.3-cp39-cp39-win_amd64.whl", hash = "sha256:9389848cc8297199b0fe2cd2985e5944f611ed518aa508136065ea0159051904"}, - {file = "zstd-1.5.7.3-cp39-cp39-win_arm64.whl", hash = "sha256:0cdf00f53cd38ce1f9edc79f68727150b9e65f4b33a3e8b59d94d0886cf43dbf"}, - {file = "zstd-1.5.7.3-pp27-pypy_73-manylinux1_x86_64.whl", hash = "sha256:c5ac39836233356d32d0fe3d2f9525373c47c19f75fde68c16cf2293b7648b86"}, - {file = "zstd-1.5.7.3-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:62fe5b560f389fdb40384a1711b7737bd9e27861f248cb89f19fed90a4cf0830"}, - {file = "zstd-1.5.7.3-pp27-pypy_73-manylinux_2_14_x86_64.whl", hash = "sha256:55fb8ac423800811f8b0c896b9617ecc91a1d4da15f66fb42ba162bfa5aa5a2d"}, - {file = "zstd-1.5.7.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:2b9ec4d5ba8c170d3fdf21ae5da3c15eaea2beef9c419a5f3274a6f9e03c412a"}, - {file = "zstd-1.5.7.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:a7ab69fc4d90eeb64b98a567751f8e48373f4bcf301597fca344b8e8342e1d5e"}, - {file = "zstd-1.5.7.3-pp310-pypy310_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da70f0918bf739bc75d7770410c9b94ea0dcb6f02d7ef70598b464bd5fcb193a"}, - {file = "zstd-1.5.7.3-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3dd5c069d0409284f1963b0b6b119f21b1da9e22a503e88933eb0696249d87d3"}, - {file = "zstd-1.5.7.3-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46ca4a075f36f118e2ce07ba07d9ece7aeda193cea6f50b82aaee635df7b5fc2"}, - {file = "zstd-1.5.7.3-pp310-pypy310_pp73-manylinux_2_14_x86_64.whl", hash = "sha256:4a521cb7615fc61bfe9514bea182e224894b5987fc7843b6d6da20a61206ef24"}, - {file = "zstd-1.5.7.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:71ea22c953a164f34eb4b8c2c3b97eaa22da6a75296ea80b3ba4473187f15046"}, - {file = "zstd-1.5.7.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:76c49ea969bc08389ea59155cea7c5dea224522ffc62f443f3c0a915f5fd184d"}, - {file = "zstd-1.5.7.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:6b1a638ff3dfce8f4cb1203c662fb5606dd99b4a62c5ddc4c406d2d1326bcfdd"}, - {file = "zstd-1.5.7.3-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5e96a5cb100a0edc162935227f2d9784b1031ce4a8a83e96e66eae2673c10143"}, - {file = "zstd-1.5.7.3-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bda0bbf3a9553720cd33f1f85940a259656c7ffba4be717ff82b7f062052188"}, - {file = "zstd-1.5.7.3-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac36e4022422f6e49b3f07bdbb8a964fd348223d3dc9c82ad5398a4f0432a719"}, - {file = "zstd-1.5.7.3-pp311-pypy311_pp73-manylinux_2_14_x86_64.whl", hash = "sha256:fa4d760a220541b18ce732a3a2cf7547ea05afc76d05b3b39edebfeb721f6079"}, - {file = "zstd-1.5.7.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a69e60146bf8aaa6a0e6c9a94a7c5f3133d68091e2e5c5a3c5ababf71fd5ec7a"}, - {file = "zstd-1.5.7.3-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:781ec2644a3ce84c1cc19b0e057e1e8ea45260a8871eb6524614be75c9b432b9"}, - {file = "zstd-1.5.7.3-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:ab74f37f2832d4a7c89d877ed9a70b1ef988fc2353678a122427039eb1dc6e36"}, - {file = "zstd-1.5.7.3-pp36-pypy36_pp73-manylinux_2_14_x86_64.whl", hash = "sha256:521a3072fedcce025515d99242e346318d1815789033b7c0108796e151c42deb"}, - {file = "zstd-1.5.7.3-pp36-pypy36_pp73-win32.whl", hash = "sha256:94d404fd56765ff2952053cb2f6f980b88e3384a71af147c3ede9f6c6bea32d6"}, - {file = "zstd-1.5.7.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:33f7e24d626938234c3c33df1988b79846628cf08dfab216bb19f85e7fcad65b"}, - {file = "zstd-1.5.7.3-pp37-pypy37_pp73-manylinux_2_14_x86_64.whl", hash = "sha256:c0c84fd4a87f28b8bed01cbaf128d33dfa209f03df2890dbc8c01e17a109c2d4"}, - {file = "zstd-1.5.7.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:0e334e45becf5a4844c8d64593eb358585e1553a7355f2172c865efc639ac051"}, - {file = "zstd-1.5.7.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:15523e289509d7792418edb8c255cc1dacc65cda000428424c988208a682b8be"}, - {file = "zstd-1.5.7.3-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:2924befc3cb1a2310e1c03bd93469a2de8f0703e8805fe1f40367fbc2cece472"}, - {file = "zstd-1.5.7.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:173680156dbe959c80d72a1f15ef2034fd414b9d1ee507df152e416bc37665ef"}, - {file = "zstd-1.5.7.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:31d66b73a9861ee61bc6486fb9d1d33eabc86e506e49a210f30a91a241b8e643"}, - {file = "zstd-1.5.7.3-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:a820a67491c1cf7a66698478a28b7d2517b0ae2e2775d834ca4f2624ba859e72"}, - {file = "zstd-1.5.7.3-pp38-pypy38_pp73-manylinux_2_14_x86_64.whl", hash = "sha256:c385f92c37f4275d477388e46af8941580d7eeaad4c524c8f9aa50d016acbc7e"}, - {file = "zstd-1.5.7.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:cecce78a3d639a3c439b1e355791e0f1ddbe8ed63d94f34c7973e92d384e6fc0"}, - {file = "zstd-1.5.7.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e769fc830f5e2079612a27d6540e4147cd8dc8beacfaf73a48152f30a191e979"}, - {file = "zstd-1.5.7.3-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:37a6750c25b561b05110313fdde4acd51246075a317e1c7a2491c96d2d863282"}, - {file = "zstd-1.5.7.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:0e95265e22f07cea6675baab762c9c4577a40d47824b01e0dcdf1a18b46aa041"}, - {file = "zstd-1.5.7.3-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:878d859a7e1ebc078e0a575c05bcf3b0682b77cabd65bdbdd5e93c137ff1799b"}, - {file = "zstd-1.5.7.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7efcf83189be9d842b9392ffd821b317cbd9447a49c590659abd3311e82c1676"}, - {file = "zstd-1.5.7.3-pp39-pypy39_pp73-manylinux_2_14_x86_64.whl", hash = "sha256:a75dfdbca7dc01e7b35ca9b22e5b9792037b1515857e67b34bd737b213e49432"}, - {file = "zstd-1.5.7.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:5235dde49df717e5ca58f689e110bf1c4ed578170ab59e77f8a7a5055e4d8c07"}, - {file = "zstd-1.5.7.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:f876acad51d2184269ee6fd7e4c4aad9b7a0eca174d7d8db981ea079b57cbaf4"}, - {file = "zstd-1.5.7.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:2920e90ef200c7b2cbc73b4271c2271abf6195877b813ede0b5b76289e32fc8e"}, - {file = "zstd-1.5.7.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1f6dd0f2845a9817f0d0920eb0efd2d8a0168b71b8d8c85d2655d9d997f127ba"}, - {file = "zstd-1.5.7.3.tar.gz", hash = "sha256:403e5205f4ac04b92e6b0cda654be2f51de268228a0db0067bc087faacf2f495"}, -] - -[metadata] -lock-version = "2.1" -python-versions = ">=3.11,<3.13" -content-hash = "a3ab982d11a87d951ff15694d2ca7fd51f1f51a451abb0baa067ccf6966367a8" diff --git a/api/pyproject.toml b/api/pyproject.toml index 838a30fdf7..6d4c452188 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -1,6 +1,24 @@ -[build-system] -build-backend = "poetry.core.masonry.api" -requires = ["poetry-core"] +[dependency-groups] +dev = [ + "bandit==1.7.9", + "coverage==7.5.4", + "django-silk==5.3.2", + "docker==7.1.0", + "filelock==3.20.3", + "freezegun==1.5.1", + "mypy==1.10.1", + "pylint==3.2.5", + "pytest==9.0.3", + "pytest-cov==5.0.0", + "pytest-django==4.8.0", + "pytest-env==1.1.3", + "pytest-randomly==3.15.0", + "pytest-xdist==3.6.1", + "ruff==0.5.0", + "tqdm==4.67.1", + "vulture==2.14", + "prek==0.3.9" +] [project] authors = [{name = "Prowler Engineering", email = "engineering@prowler.com"}] @@ -24,14 +42,14 @@ dependencies = [ "drf-spectacular-jsonapi==0.5.1", "defusedxml==0.7.1", "gunicorn==23.0.0", - "lxml==5.3.2", + "lxml==6.1.0", "prowler @ git+https://github.com/prowler-cloud/prowler.git@master", "psycopg2-binary==2.9.9", "pytest-celery[redis] (==1.3.0)", "sentry-sdk[django] (==2.56.0)", "uuid6==2024.7.10", "openai (==1.109.1)", - "xmlsec==1.3.14", + "xmlsec==1.3.17", "h2 (==4.3.0)", "markdown (==3.10.2)", "drf-simple-apikey (==2.2.1)", @@ -50,28 +68,382 @@ name = "prowler-api" package-mode = false # Needed for the SDK compatibility requires-python = ">=3.11,<3.13" -version = "1.27.0" +version = "1.30.0" -[project.scripts] -celery = "src.backend.config.settings.celery" - -[tool.poetry.group.dev.dependencies] -bandit = "1.7.9" -coverage = "7.5.4" -django-silk = "5.3.2" -docker = "7.1.0" -filelock = "3.20.3" -freezegun = "1.5.1" -mypy = "1.10.1" -pylint = "3.2.5" -pytest = "9.0.3" -pytest-cov = "5.0.0" -pytest-django = "4.8.0" -pytest-env = "1.1.3" -pytest-randomly = "3.15.0" -pytest-xdist = "3.6.1" -ruff = "0.5.0" -safety = "3.7.0" -tqdm = "4.67.1" -vulture = "2.14" -prek = "0.3.9" +[tool.uv] +# Transitive pins matching master to avoid silent drift; bump deliberately. +constraint-dependencies = [ + "about-time==4.2.1", + "adal==1.2.7", + "aioboto3==15.5.0", + "aiobotocore==2.25.1", + "aiofiles==24.1.0", + "aiohappyeyeballs==2.6.1", + "aiohttp==3.13.5", + "aioitertools==0.13.0", + "aiosignal==1.4.0", + "alibabacloud-actiontrail20200706==2.4.1", + "alibabacloud-credentials==1.0.3", + "alibabacloud-credentials-api==1.0.0", + "alibabacloud-cs20151215==6.1.0", + "alibabacloud-darabonba-array==0.1.0", + "alibabacloud-darabonba-encode-util==0.0.2", + "alibabacloud-darabonba-map==0.0.1", + "alibabacloud-darabonba-signature-util==0.0.4", + "alibabacloud-darabonba-string==0.0.4", + "alibabacloud-darabonba-time==0.0.1", + "alibabacloud-ecs20140526==7.2.5", + "alibabacloud-endpoint-util==0.0.4", + "alibabacloud-gateway-oss==0.0.17", + "alibabacloud-gateway-oss-util==0.0.3", + "alibabacloud-gateway-sls==0.4.0", + "alibabacloud-gateway-sls-util==0.4.0", + "alibabacloud-gateway-spi==0.0.3", + "alibabacloud-openapi-util==0.2.4", + "alibabacloud-oss-util==0.0.6", + "alibabacloud-oss20190517==1.0.6", + "alibabacloud-ram20150501==1.2.0", + "alibabacloud-rds20140815==12.0.0", + "alibabacloud-sas20181203==6.1.0", + "alibabacloud-sls20201230==5.9.0", + "alibabacloud-sts20150401==1.1.6", + "alibabacloud-tea==0.4.3", + "alibabacloud-tea-openapi==0.4.4", + "alibabacloud-tea-util==0.3.14", + "alibabacloud-tea-xml==0.0.3", + "alibabacloud-vpc20160428==6.13.0", + "alive-progress==3.3.0", + "aliyun-log-fastpb==0.2.0", + "amqp==5.3.1", + "annotated-types==0.7.0", + "anyio==4.12.1", + "applicationinsights==0.11.10", + "apscheduler==3.11.2", + "argcomplete==3.5.3", + "asgiref==3.11.0", + "astroid==3.2.4", + "async-timeout==5.0.1", + "attrs==25.4.0", + "authlib==1.6.9", + "autopep8==2.3.2", + "awsipranges==0.3.3", + "azure-cli-core==2.83.0", + "azure-cli-telemetry==1.1.0", + "azure-common==1.1.28", + "azure-core==1.38.1", + "azure-identity==1.21.0", + "azure-keyvault-certificates==4.10.0", + "azure-keyvault-keys==4.10.0", + "azure-keyvault-secrets==4.10.0", + "azure-mgmt-apimanagement==5.0.0", + "azure-mgmt-applicationinsights==4.1.0", + "azure-mgmt-authorization==4.0.0", + "azure-mgmt-compute==34.0.0", + "azure-mgmt-containerinstance==10.1.0", + "azure-mgmt-containerregistry==12.0.0", + "azure-mgmt-containerservice==34.1.0", + "azure-mgmt-core==1.6.0", + "azure-mgmt-cosmosdb==9.7.0", + "azure-mgmt-databricks==2.0.0", + "azure-mgmt-datafactory==9.2.0", + "azure-mgmt-eventgrid==10.4.0", + "azure-mgmt-eventhub==11.2.0", + "azure-mgmt-keyvault==10.3.1", + "azure-mgmt-loganalytics==12.0.0", + "azure-mgmt-logic==10.0.0", + "azure-mgmt-monitor==6.0.2", + "azure-mgmt-network==28.1.0", + "azure-mgmt-postgresqlflexibleservers==1.1.0", + "azure-mgmt-rdbms==10.1.0", + "azure-mgmt-recoveryservices==3.1.0", + "azure-mgmt-recoveryservicesbackup==9.2.0", + "azure-mgmt-resource==24.0.0", + "azure-mgmt-search==9.1.0", + "azure-mgmt-security==7.0.0", + "azure-mgmt-sql==3.0.1", + "azure-mgmt-storage==22.1.1", + "azure-mgmt-subscription==3.1.1", + "azure-mgmt-synapse==2.0.0", + "azure-mgmt-web==8.0.0", + "azure-monitor-query==2.0.0", + "azure-storage-blob==12.24.1", + "azure-synapse-artifacts==0.21.0", + "backoff==2.2.1", + "bandit==1.7.9", + "billiard==4.2.4", + "blinker==1.9.0", + "boto3==1.40.61", + "botocore==1.40.61", + "cartography==0.135.0", + "celery==5.6.2", + "certifi==2026.1.4", + "cffi==2.0.0", + "charset-normalizer==3.4.4", + "circuitbreaker==2.1.3", + "click==8.3.1", + "click-didyoumean==0.3.1", + "click-plugins==1.1.1.2", + "click-repl==0.3.0", + "cloudflare==4.3.1", + "colorama==0.4.6", + "contextlib2==21.6.0", + "contourpy==1.3.3", + "coverage==7.5.4", + "cron-descriptor==1.4.5", + "crowdstrike-falconpy==1.6.0", + "cryptography==46.0.7", + "cycler==0.12.1", + "darabonba-core==1.0.5", + "dash==3.1.1", + "dash-bootstrap-components==2.0.3", + "debugpy==1.8.20", + "decorator==5.2.1", + "defusedxml==0.7.1", + "detect-secrets==1.5.0", + "dill==0.4.1", + "distro==1.9.0", + "dj-rest-auth==7.0.1", + "django==5.1.15", + "django-allauth==65.15.0", + "django-celery-beat==2.9.0", + "django-celery-results==2.6.0", + "django-cors-headers==4.4.0", + "django-environ==0.11.2", + "django-filter==24.3", + "django-guid==3.5.0", + "django-postgres-extra==2.0.9", + "django-silk==5.3.2", + "django-timezone-field==7.2.1", + "djangorestframework==3.15.2", + "djangorestframework-jsonapi==7.0.2", + "djangorestframework-simplejwt==5.5.1", + "dnspython==2.8.0", + "docker==7.1.0", + "dogpile-cache==1.5.0", + "dparse==0.6.4", + "drf-extensions==0.8.0", + "drf-nested-routers==0.95.0", + "drf-simple-apikey==2.2.1", + "drf-spectacular==0.27.2", + "drf-spectacular-jsonapi==0.5.1", + "dulwich==0.23.0", + "duo-client==5.5.0", + "durationpy==0.10", + "email-validator==2.2.0", + "execnet==2.1.2", + "filelock==3.20.3", + "flask==3.1.3", + "fonttools==4.62.1", + "freezegun==1.5.1", + "frozenlist==1.8.0", + "gevent==25.9.1", + "google-api-core==2.29.0", + "google-api-python-client==2.163.0", + "google-auth==2.48.0", + "google-auth-httplib2==0.2.0", + "google-cloud-access-context-manager==0.3.0", + "google-cloud-asset==4.2.0", + "google-cloud-org-policy==1.16.0", + "google-cloud-os-config==1.23.0", + "google-cloud-resource-manager==1.16.0", + "googleapis-common-protos==1.72.0", + "gprof2dot==2025.4.14", + "graphemeu==0.7.2", + "greenlet==3.3.1", + "grpc-google-iam-v1==0.14.3", + "grpcio==1.76.0", + "grpcio-status==1.76.0", + "gunicorn==23.0.0", + "h11==0.16.0", + "h2==4.3.0", + "hpack==4.1.0", + "httpcore==1.0.9", + "httplib2==0.31.2", + "httpx==0.28.1", + "humanfriendly==10.0", + "hyperframe==6.1.0", + "iamdata==0.1.202602021", + "idna==3.11", + "importlib-metadata==8.7.1", + "inflection==0.5.1", + "iniconfig==2.3.0", + "iso8601==2.1.0", + "isodate==0.7.2", + "isort==5.13.2", + "itsdangerous==2.2.0", + "jinja2==3.1.6", + "jiter==0.13.0", + "jmespath==1.1.0", + "joblib==1.5.3", + "jsonpatch==1.33", + "jsonpickle==4.1.1", + "jsonpointer==3.0.0", + "jsonschema==4.23.0", + "jsonschema-specifications==2025.9.1", + "keystoneauth1==5.13.0", + "kiwisolver==1.4.9", + "knack==0.11.0", + "kombu==5.6.2", + "kubernetes==32.0.1", + "lxml==6.1.0", + "lz4==4.4.5", + "markdown==3.10.2", + "markdown-it-py==4.0.0", + "markupsafe==3.0.3", + "marshmallow==4.3.0", + "matplotlib==3.10.8", + "mccabe==0.7.0", + "mdurl==0.1.2", + "microsoft-kiota-abstractions==1.9.9", + "microsoft-kiota-authentication-azure==1.9.9", + "microsoft-kiota-http==1.9.9", + "microsoft-kiota-serialization-form==1.9.9", + "microsoft-kiota-serialization-json==1.9.9", + "microsoft-kiota-serialization-multipart==1.9.9", + "microsoft-kiota-serialization-text==1.9.9", + "microsoft-security-utilities-secret-masker==1.0.0b4", + "msal==1.35.0b1", + "msal-extensions==1.2.0", + "msgraph-core==1.3.8", + "msgraph-sdk==1.55.0", + "msrest==0.7.1", + "msrestazure==0.6.4.post1", + "multidict==6.7.1", + "mypy==1.10.1", + "mypy-extensions==1.1.0", + "narwhals==2.16.0", + "neo4j==6.1.0", + "nest-asyncio==1.6.0", + "nltk==3.9.4", + "numpy==2.0.2", + "oauthlib==3.3.1", + "oci==2.169.0", + "openai==1.109.1", + "openstacksdk==4.2.0", + "opentelemetry-api==1.39.1", + "opentelemetry-sdk==1.39.1", + "opentelemetry-semantic-conventions==0.60b1", + "os-service-types==1.8.2", + "packageurl-python==0.17.6", + "packaging==26.0", + "pagerduty==6.1.0", + "pandas==2.2.3", + "pbr==7.0.3", + "pillow==12.2.0", + "pkginfo==1.12.1.2", + "platformdirs==4.5.1", + "plotly==6.5.2", + "pluggy==1.6.0", + "policyuniverse==1.5.1.20231109", + "portalocker==2.10.1", + "prek==0.3.9", + "prompt-toolkit==3.0.52", + "propcache==0.4.1", + "proto-plus==1.27.0", + "protobuf==6.33.5", + "psutil==7.2.2", + "psycopg2-binary==2.9.9", + "py-deviceid==0.1.1", + "py-iam-expand==0.1.0", + "py-ocsf-models==0.8.1", + "pyasn1==0.6.3", + "pyasn1-modules==0.4.2", + "pycodestyle==2.14.0", + "pycparser==3.0", + "pydantic==2.12.5", + "pydantic-core==2.41.5", + "pygithub==2.8.0", + "pygments==2.20.0", + "pyjwt==2.12.1", + "pylint==3.2.5", + "pymsalruntime==0.18.1", + "pynacl==1.6.2", + "pyopenssl==26.0.0", + "pyparsing==3.3.2", + "pyreadline3==3.5.4", + "pysocks==1.7.1", + "pytest==9.0.3", + "pytest-celery==1.3.0", + "pytest-cov==5.0.0", + "pytest-django==4.8.0", + "pytest-docker-tools==3.1.9", + "pytest-env==1.1.3", + "pytest-randomly==3.15.0", + "pytest-xdist==3.6.1", + "python-crontab==3.3.0", + "python-dateutil==2.9.0.post0", + "python-digitalocean==1.17.0", + "python3-saml==1.16.0", + "pytz==2025.1", + "pywin32==311", + "pyyaml==6.0.3", + "redis==7.1.0", + "referencing==0.37.0", + "regex==2026.1.15", + "reportlab==4.4.10", + "requests==2.33.1", + "requests-file==3.0.1", + "requests-oauthlib==2.0.0", + "requestsexceptions==1.4.0", + "retrying==1.4.2", + "rich==14.3.2", + "rpds-py==0.30.0", + "rsa==4.9.1", + "ruamel-yaml==0.19.1", + "ruff==0.5.0", + "s3transfer==0.14.0", + "scaleway==2.10.3", + "scaleway-core==2.10.3", + "schema==0.7.5", + "sentry-sdk==2.56.0", + "setuptools==80.10.2", + "shellingham==1.5.4", + "shodan==1.31.0", + "six==1.17.0", + "slack-sdk==3.39.0", + "sniffio==1.3.1", + "sqlparse==0.5.5", + "statsd==4.0.1", + "std-uritemplate==2.0.8", + "stevedore==5.6.0", + "tabulate==0.9.0", + "tenacity==9.1.2", + "tldextract==5.3.1", + "tomlkit==0.14.0", + "tqdm==4.67.1", + "typer==0.21.1", + "types-aiobotocore-ecr==3.1.1", + "typing-extensions==4.15.0", + "typing-inspection==0.4.2", + "tzdata==2025.3", + "tzlocal==5.3.1", + "uritemplate==4.2.0", + "urllib3==2.7.0", + "uuid6==2024.7.10", + "vine==5.1.0", + "vulture==2.14", + "wcwidth==0.5.3", + "websocket-client==1.9.0", + "werkzeug==3.1.7", + "workos==6.0.4", + "wrapt==1.17.3", + "xlsxwriter==3.2.9", + "xmlsec==1.3.17", + "xmltodict==1.0.2", + "yarl==1.22.0", + "zipp==3.23.0", + "zope-event==6.1", + "zope-interface==8.2", + "zstd==1.5.7.3" +] +# prowler@master needs okta==3.4.2; cartography 0.135.0 declares okta<1.0.0 for an +# integration prowler does not import. +# +# prowler@master hard-pins microsoft-kiota-abstractions==1.9.2 in [project.dependencies]. +# The microsoft-kiota-http security bump to 1.9.9 (GHSA-7j59-v9qr-6fq9) requires +# microsoft-kiota-abstractions>=1.9.9, which a constraint cannot satisfy against the +# SDK's hard pin; override it to the patched, kiota-aligned version. +override-dependencies = [ + "okta==3.4.2", + "microsoft-kiota-abstractions==1.9.9" +] diff --git a/api/src/backend/api/attack_paths/queries/aws.py b/api/src/backend/api/attack_paths/queries/aws.py index f50935c49f..81f91de24f 100644 --- a/api/src/backend/api/attack_paths/queries/aws.py +++ b/api/src/backend/api/attack_paths/queries/aws.py @@ -484,8 +484,8 @@ AWS_BEDROCK_PRIVESC_PASSROLE_CODE_INTERPRETER = AttackPathsQueryDefinition( OR action = '*' ) - // Find roles that trust Bedrock service (can be passed to Bedrock) - MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'bedrock.amazonaws.com'}}) + // Find roles that trust the Bedrock AgentCore service (can be passed to a code interpreter) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'bedrock-agentcore.amazonaws.com'}}) WHERE any(resource IN stmt_passrole.resource WHERE resource = '*' OR target_role.arn CONTAINS resource @@ -536,8 +536,8 @@ AWS_BEDROCK_PRIVESC_INVOKE_CODE_INTERPRETER = AttackPathsQueryDefinition( OR action = '*' ) - // Find roles that trust Bedrock service (already attached to existing code interpreters) - MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'bedrock.amazonaws.com'}}) + // Find roles that trust the Bedrock AgentCore service (already attached to existing code interpreters) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'bedrock-agentcore.amazonaws.com'}}) WITH collect(path_principal) + collect(path_target) AS paths UNWIND paths AS p diff --git a/api/src/backend/api/health.py b/api/src/backend/api/health.py new file mode 100644 index 0000000000..8ca3936f94 --- /dev/null +++ b/api/src/backend/api/health.py @@ -0,0 +1,254 @@ +"""Liveness and readiness endpoints following the IETF Health Check Response +Format (draft-inadarei-api-health-check-06). + +Liveness reports only process status. Readiness verifies that PostgreSQL, +Valkey and Neo4j are reachable and returns per-dependency detail when any +of them is unreachable. +""" + +from __future__ import annotations + +import logging +import threading +import time +from contextlib import suppress +from datetime import datetime, timezone +from typing import Any + +import redis +from config.version import API_VERSION, RELEASE_ID +from django.conf import settings +from django.db import connections +from drf_spectacular.utils import extend_schema +from rest_framework import status +from rest_framework.renderers import JSONRenderer +from rest_framework.response import Response +from rest_framework.throttling import ScopedRateThrottle +from rest_framework.views import APIView + +logger = logging.getLogger(__name__) + +SERVICE_ID = "prowler-api" +SERVICE_DESCRIPTION = "Prowler API" + +# Status vocabulary from the IETF draft (section 3.1). +STATUS_PASS = "pass" +STATUS_FAIL = "fail" +STATUS_WARN = "warn" + +# Short socket timeout so a stuck Valkey cannot stall the probe. +# Neo4j inherits its driver-level ``connection_acquisition_timeout``. +VALKEY_PROBE_TIMEOUT_SECONDS = 2 + +# Brief cache window so high-frequency probes (ALB target groups, scrapers) +# do not stampede the actual dependency checks. +CACHE_CONTROL_HEADER = "max-age=3, must-revalidate" + +# In-process readiness cache. Caps real dependency hits to roughly +# (gunicorn workers / TTL) per second regardless of incoming RPS or the +# source-IP distribution. Kept in sync with the Cache-Control max-age. +# Access is guarded by a lock so concurrent readers do not race on the +# read-decide-write cycle of the double-checked locking pattern below. +READINESS_CACHE_TTL_SECONDS = 3.0 +_readiness_cache: tuple[float, dict[str, Any], int] | None = None +_readiness_cache_lock = threading.Lock() + + +class HealthJSONRenderer(JSONRenderer): + """Emits responses with the ``application/health+json`` content type.""" + + media_type = "application/health+json" + format = "health" + + +def _now_iso() -> str: + return ( + datetime.now(timezone.utc) + .isoformat(timespec="milliseconds") + .replace("+00:00", "Z") + ) + + +def _measure(name: str, check_fn) -> tuple[dict[str, Any], float]: + """Time ``check_fn`` and return ``(result, elapsed_ms)``. + + ``check_fn`` returns ``None`` on success or raises on failure. The full + exception is logged for operator diagnostics under ``name``; the + response payload intentionally omits the error detail to avoid leaking + infrastructure information (DNS names, ports, credentials, certificate + chains) to anonymous clients. + """ + started = time.perf_counter() + try: + check_fn() + except Exception: + elapsed_ms = (time.perf_counter() - started) * 1000 + logger.warning("Health probe '%s' failed", name, exc_info=True) + return ({"status": STATUS_FAIL}, elapsed_ms) + elapsed_ms = (time.perf_counter() - started) * 1000 + return ({"status": STATUS_PASS}, elapsed_ms) + + +def _probe_postgres() -> None: + with connections["default"].cursor() as cursor: + cursor.execute("SELECT 1") + cursor.fetchone() + + +def _probe_valkey() -> None: + client = redis.Redis.from_url( + settings.CELERY_BROKER_URL, + socket_connect_timeout=VALKEY_PROBE_TIMEOUT_SECONDS, + socket_timeout=VALKEY_PROBE_TIMEOUT_SECONDS, + ) + try: + if not client.ping(): + raise RuntimeError("PING did not return PONG") + finally: + # Best-effort cleanup: a failure releasing the socket (e.g. broken + # connection, half-closed by the server) must not mask the probe + # result. Narrowed to the exception types redis-py and the stdlib + # socket layer can raise on close. + with suppress(redis.RedisError, OSError): + client.close() + + +def _probe_neo4j() -> None: + # Lazy import: avoids pulling attack_paths into the boot import graph. + from api.attack_paths.database import get_driver + + get_driver().verify_connectivity() + + +def _build_check_entry( + component_id: str, + component_type: str, + result: dict[str, Any], + elapsed_ms: float, +) -> dict[str, Any]: + entry: dict[str, Any] = { + "componentId": component_id, + "componentType": component_type, + "observedValue": round(elapsed_ms, 2), + "observedUnit": "ms", + "status": result["status"], + "time": _now_iso(), + } + if "output" in result: + entry["output"] = result["output"] + return entry + + +def _aggregate_status(check_entries: list[dict[str, Any]]) -> str: + statuses = {entry["status"] for entry in check_entries} + if STATUS_FAIL in statuses: + return STATUS_FAIL + if STATUS_WARN in statuses: + return STATUS_WARN + return STATUS_PASS + + +def _base_payload(overall_status: str) -> dict[str, Any]: + return { + "status": overall_status, + "version": API_VERSION, + "releaseId": RELEASE_ID, + "serviceId": SERVICE_ID, + "description": SERVICE_DESCRIPTION, + } + + +def _readiness_payload() -> tuple[dict[str, Any], int]: + global _readiness_cache + + # Lock-free fast path: a stale snapshot still satisfies the freshness + # check correctly because we re-check after acquiring the lock below. + snapshot = _readiness_cache + if ( + snapshot is not None + and time.monotonic() - snapshot[0] < READINESS_CACHE_TTL_SECONDS + ): + return snapshot[1], snapshot[2] + + with _readiness_cache_lock: + # Double-checked locking: another thread may have refreshed while + # we were waiting on the lock. + snapshot = _readiness_cache + if ( + snapshot is not None + and time.monotonic() - snapshot[0] < READINESS_CACHE_TTL_SECONDS + ): + return snapshot[1], snapshot[2] + + postgres_result, postgres_ms = _measure("postgres", _probe_postgres) + valkey_result, valkey_ms = _measure("valkey", _probe_valkey) + neo4j_result, neo4j_ms = _measure("neo4j", _probe_neo4j) + + entries = [ + _build_check_entry("postgres", "datastore", postgres_result, postgres_ms), + _build_check_entry("valkey", "datastore", valkey_result, valkey_ms), + _build_check_entry("neo4j", "datastore", neo4j_result, neo4j_ms), + ] + overall = _aggregate_status(entries) + + payload = _base_payload(overall) + payload["checks"] = { + "postgres:responseTime": [entries[0]], + "valkey:responseTime": [entries[1]], + "neo4j:responseTime": [entries[2]], + } + + http_status = ( + status.HTTP_503_SERVICE_UNAVAILABLE + if overall == STATUS_FAIL + else status.HTTP_200_OK + ) + _readiness_cache = (time.monotonic(), payload, http_status) + return payload, http_status + + +def _health_response(payload: dict[str, Any], http_status: int) -> Response: + response = Response(payload, status=http_status) + response["Cache-Control"] = CACHE_CONTROL_HEADER + return response + + +@extend_schema(exclude=True) +class LivenessView(APIView): + """Liveness probe. Always 200 when the process can serve requests. + + Dependencies are intentionally not consulted: a failing liveness probe + triggers a container restart, which must not happen for transient + dependency outages. Throttled per-IP so the endpoint cannot be used as + a cheap availability oracle for the process. + """ + + authentication_classes: list = [] + permission_classes: list = [] + renderer_classes = [HealthJSONRenderer] + throttle_classes = [ScopedRateThrottle] + throttle_scope = "health-live" + + def get(self, _request, *_args, **_kwargs): + return _health_response(_base_payload(STATUS_PASS), status.HTTP_200_OK) + + +@extend_schema(exclude=True) +class ReadinessView(APIView): + """Readiness probe. + + Returns 200 when PostgreSQL, Valkey and Neo4j all respond, or 503 with + per-dependency detail when any of them is unreachable. Per-IP throttle + plus the short in-process result cache cap the real dependency hits + regardless of inbound traffic shape. + """ + + authentication_classes: list = [] + permission_classes: list = [] + renderer_classes = [HealthJSONRenderer] + throttle_classes = [ScopedRateThrottle] + throttle_scope = "health-ready" + + def get(self, _request, *_args, **_kwargs): + payload, http_status = _readiness_payload() + return _health_response(payload, http_status) diff --git a/api/src/backend/api/migrations/0091_findings_arrays_gin_index_partitions.py b/api/src/backend/api/migrations/0091_findings_arrays_gin_index_partitions.py new file mode 100644 index 0000000000..fc5716f828 --- /dev/null +++ b/api/src/backend/api/migrations/0091_findings_arrays_gin_index_partitions.py @@ -0,0 +1,31 @@ +from functools import partial + +from django.db import migrations + +from api.db_utils import create_index_on_partitions, drop_index_on_partitions + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ("api", "0090_attack_paths_cleanup_priority"), + ] + + operations = [ + migrations.RunPython( + partial( + create_index_on_partitions, + parent_table="findings", + index_name="gin_find_arrays_idx", + columns="categories, resource_services, resource_regions, resource_types", + method="GIN", + all_partitions=True, + ), + reverse_code=partial( + drop_index_on_partitions, + parent_table="findings", + index_name="gin_find_arrays_idx", + ), + ) + ] diff --git a/api/src/backend/api/migrations/0092_findings_arrays_gin_index_parent.py b/api/src/backend/api/migrations/0092_findings_arrays_gin_index_parent.py new file mode 100644 index 0000000000..fe49ae6f10 --- /dev/null +++ b/api/src/backend/api/migrations/0092_findings_arrays_gin_index_parent.py @@ -0,0 +1,73 @@ +import django.contrib.postgres.indexes +from django.db import migrations + +INDEX_NAME = "gin_find_arrays_idx" +PARENT_TABLE = "findings" + + +def create_parent_and_attach(apps, schema_editor): + with schema_editor.connection.cursor() as cursor: + # Idempotent: the parent index may already exist if it was created + # manually on an environment before this migration ran. + cursor.execute( + f"CREATE INDEX IF NOT EXISTS {INDEX_NAME} ON ONLY {PARENT_TABLE} " + f"USING gin (categories, resource_services, resource_regions, resource_types)" + ) + cursor.execute( + "SELECT inhrelid::regclass::text " + "FROM pg_inherits " + "WHERE inhparent = %s::regclass", + [PARENT_TABLE], + ) + for (partition,) in cursor.fetchall(): + child_idx = f"{partition.replace('.', '_')}_{INDEX_NAME}" + # ALTER INDEX ... ATTACH PARTITION has no IF NOT ATTACHED clause, + # so check pg_inherits first to keep the migration re-runnable. + cursor.execute( + """ + SELECT 1 + FROM pg_inherits i + JOIN pg_class p ON p.oid = i.inhparent + JOIN pg_class c ON c.oid = i.inhrelid + WHERE p.relname = %s AND c.relname = %s + """, + [INDEX_NAME, child_idx], + ) + if cursor.fetchone() is None: + cursor.execute(f"ALTER INDEX {INDEX_NAME} ATTACH PARTITION {child_idx}") + + +def drop_parent_index(apps, schema_editor): + with schema_editor.connection.cursor() as cursor: + cursor.execute(f"DROP INDEX IF EXISTS {INDEX_NAME}") + + +class Migration(migrations.Migration): + dependencies = [ + ("api", "0091_findings_arrays_gin_index_partitions"), + ] + + operations = [ + migrations.SeparateDatabaseAndState( + state_operations=[ + migrations.AddIndex( + model_name="finding", + index=django.contrib.postgres.indexes.GinIndex( + fields=[ + "categories", + "resource_services", + "resource_regions", + "resource_types", + ], + name=INDEX_NAME, + ), + ), + ], + database_operations=[ + migrations.RunPython( + create_parent_and_attach, + reverse_code=drop_parent_index, + ), + ], + ), + ] diff --git a/api/src/backend/api/migrations/0093_okta_provider.py b/api/src/backend/api/migrations/0093_okta_provider.py new file mode 100644 index 0000000000..d3e4a9e397 --- /dev/null +++ b/api/src/backend/api/migrations/0093_okta_provider.py @@ -0,0 +1,41 @@ +from django.db import migrations + +import api.db_utils + + +class Migration(migrations.Migration): + dependencies = [ + ("api", "0092_findings_arrays_gin_index_parent"), + ] + + operations = [ + migrations.AlterField( + model_name="provider", + name="provider", + field=api.db_utils.ProviderEnumField( + choices=[ + ("aws", "AWS"), + ("azure", "Azure"), + ("gcp", "GCP"), + ("kubernetes", "Kubernetes"), + ("m365", "M365"), + ("github", "GitHub"), + ("mongodbatlas", "MongoDB Atlas"), + ("iac", "IaC"), + ("oraclecloud", "Oracle Cloud Infrastructure"), + ("alibabacloud", "Alibaba Cloud"), + ("cloudflare", "Cloudflare"), + ("openstack", "OpenStack"), + ("image", "Image"), + ("googleworkspace", "Google Workspace"), + ("vercel", "Vercel"), + ("okta", "Okta"), + ], + default="aws", + ), + ), + migrations.RunSQL( + "ALTER TYPE provider ADD VALUE IF NOT EXISTS 'okta';", + reverse_sql=migrations.RunSQL.noop, + ), + ] diff --git a/api/src/backend/api/models.py b/api/src/backend/api/models.py index e7195cff5f..3d9a26698e 100644 --- a/api/src/backend/api/models.py +++ b/api/src/backend/api/models.py @@ -296,6 +296,7 @@ class Provider(RowLevelSecurityProtectedModel): IMAGE = "image", _("Image") GOOGLEWORKSPACE = "googleworkspace", _("Google Workspace") VERCEL = "vercel", _("Vercel") + OKTA = "okta", _("Okta") @staticmethod def validate_aws_uid(value): @@ -354,6 +355,26 @@ class Provider(RowLevelSecurityProtectedModel): pointer="/data/attributes/uid", ) + @staticmethod + def validate_okta_uid(value): + if not re.match( + r"^[a-z0-9][a-z0-9-]*\.(" + r"okta\.com|oktapreview\.com|okta-emea\.com|" + r"okta-gov\.com|okta\.mil|okta-miltest\.com|trex-govcloud\.com" + r")$", + value, + ): + raise ModelValidationError( + detail=( + "Okta provider ID must be a valid Okta-managed org domain " + "(e.g., acme.okta.com, also .oktapreview.com / .okta-emea.com " + "/ .okta-gov.com / .okta.mil / .okta-miltest.com / " + ".trex-govcloud.com), without scheme or path." + ), + code="okta-uid", + pointer="/data/attributes/uid", + ) + @staticmethod def validate_kubernetes_uid(value): if not re.match( @@ -480,6 +501,12 @@ class Provider(RowLevelSecurityProtectedModel): def clean(self): super().clean() + if self.provider == self.ProviderChoices.OKTA and self.uid: + # Mirror the SDK, which lowercases the org domain before connecting. + # Without this the API would reject Acme.okta.com even though the + # SDK would accept it, and stored uids could disagree with the + # authenticated org domain. + self.uid = self.uid.strip().lower() getattr(self, f"validate_{self.provider}_uid")(self.uid) def save(self, *args, **kwargs): @@ -946,7 +973,6 @@ class Resource(RowLevelSecurityProtectedModel): OpClass(Upper("name"), name="gin_trgm_ops"), name="res_name_trgm_idx", ), - GinIndex(fields=["text_search"], name="gin_resources_search_idx"), models.Index(fields=["tenant_id", "id"], name="resources_tenant_id_idx"), models.Index( fields=["tenant_id", "provider_id"], @@ -1152,6 +1178,15 @@ class Finding(PostgresPartitionedModel, RowLevelSecurityProtectedModel): fields=["tenant_id", "scan_id", "check_id"], name="find_tenant_scan_check_idx", ), + GinIndex( + fields=[ + "categories", + "resource_services", + "resource_regions", + "resource_types", + ], + name="gin_find_arrays_idx", + ), ] class JSONAPIMeta: diff --git a/api/src/backend/api/specs/v1.yaml b/api/src/backend/api/specs/v1.yaml index c436c4cd8f..c4f8c969dd 100644 --- a/api/src/backend/api/specs/v1.yaml +++ b/api/src/backend/api/specs/v1.yaml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: Prowler API - version: 1.27.0 + version: 1.30.0 description: |- Prowler API specification. @@ -373,6 +373,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- * `aws` - AWS * `azure` - Azure @@ -389,6 +390,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta - in: query name: filter[provider_type__in] schema: @@ -412,6 +414,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- Multiple values may be separated by commas. @@ -430,6 +433,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta explode: false style: form - in: query @@ -1453,6 +1457,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- * `aws` - AWS * `azure` - Azure @@ -1469,6 +1474,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta - in: query name: filter[provider_type__in] schema: @@ -1491,6 +1497,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- Multiple values may be separated by commas. @@ -1509,6 +1516,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta explode: false style: form - in: query @@ -1997,6 +2005,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- * `aws` - AWS * `azure` - Azure @@ -2013,6 +2022,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta - in: query name: filter[provider_type__in] schema: @@ -2035,6 +2045,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- Multiple values may be separated by commas. @@ -2053,6 +2064,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta explode: false style: form - in: query @@ -2584,6 +2596,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- * `aws` - AWS * `azure` - Azure @@ -2600,6 +2613,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta - in: query name: filter[provider_type__in] schema: @@ -2622,6 +2636,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- Multiple values may be separated by commas. @@ -2640,6 +2655,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta explode: false style: form - in: query @@ -3134,6 +3150,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- * `aws` - AWS * `azure` - Azure @@ -3150,6 +3167,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta - in: query name: filter[provider_type__in] schema: @@ -3173,6 +3191,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- Multiple values may be separated by commas. @@ -3191,6 +3210,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta explode: false style: form - in: query @@ -3740,6 +3760,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- * `aws` - AWS * `azure` - Azure @@ -3756,6 +3777,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta - in: query name: filter[provider_type__in] schema: @@ -3779,6 +3801,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- Multiple values may be separated by commas. @@ -3797,6 +3820,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta explode: false style: form - in: query @@ -4254,6 +4278,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- * `aws` - AWS * `azure` - Azure @@ -4270,6 +4295,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta - in: query name: filter[provider_type__in] schema: @@ -4293,6 +4319,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- Multiple values may be separated by commas. @@ -4311,6 +4338,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta explode: false style: form - in: query @@ -4766,6 +4794,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- * `aws` - AWS * `azure` - Azure @@ -4782,6 +4811,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta - in: query name: filter[provider_type__in] schema: @@ -4805,6 +4835,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- Multiple values may be separated by commas. @@ -4823,6 +4854,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta explode: false style: form - in: query @@ -5266,6 +5298,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- * `aws` - AWS * `azure` - Azure @@ -5282,6 +5315,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta - in: query name: filter[provider_type__in] schema: @@ -5305,6 +5339,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- Multiple values may be separated by commas. @@ -5323,6 +5358,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta explode: false style: form - in: query @@ -7156,6 +7192,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- * `aws` - AWS * `azure` - Azure @@ -7172,6 +7209,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta - in: query name: filter[provider_type__in] schema: @@ -7195,6 +7233,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- Multiple values may be separated by commas. @@ -7213,6 +7252,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta explode: false style: form - name: filter[search] @@ -7335,6 +7375,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- * `aws` - AWS * `azure` - Azure @@ -7351,6 +7392,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta - in: query name: filter[provider_type__in] schema: @@ -7374,6 +7416,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- Multiple values may be separated by commas. @@ -7392,6 +7435,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta explode: false style: form - name: filter[search] @@ -7503,6 +7547,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- * `aws` - AWS * `azure` - Azure @@ -7519,6 +7564,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta - in: query name: filter[provider_type__in] schema: @@ -7541,6 +7587,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- Multiple values may be separated by commas. @@ -7559,6 +7606,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta explode: false style: form - name: filter[search] @@ -7702,6 +7750,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- * `aws` - AWS * `azure` - Azure @@ -7718,6 +7767,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta - in: query name: filter[provider_type__in] schema: @@ -7741,6 +7791,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- Multiple values may be separated by commas. @@ -7759,6 +7810,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta explode: false style: form - in: query @@ -7915,6 +7967,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- * `aws` - AWS * `azure` - Azure @@ -7931,6 +7984,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta - in: query name: filter[provider_type__in] schema: @@ -7954,6 +8008,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- Multiple values may be separated by commas. @@ -7972,6 +8027,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta explode: false style: form - in: query @@ -8122,6 +8178,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- * `aws` - AWS * `azure` - Azure @@ -8138,6 +8195,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta - in: query name: filter[provider_type__in] schema: @@ -8160,6 +8218,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- Multiple values may be separated by commas. @@ -8178,6 +8237,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta explode: false style: form - name: filter[search] @@ -8370,6 +8430,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- * `aws` - AWS * `azure` - Azure @@ -8386,6 +8447,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta - in: query name: filter[provider_type__in] schema: @@ -8409,6 +8471,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- Multiple values may be separated by commas. @@ -8427,6 +8490,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta explode: false style: form - in: query @@ -8548,6 +8612,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- * `aws` - AWS * `azure` - Azure @@ -8564,6 +8629,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta - in: query name: filter[provider_type__in] schema: @@ -8587,6 +8653,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- Multiple values may be separated by commas. @@ -8605,6 +8672,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta explode: false style: form - in: query @@ -8750,6 +8818,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- * `aws` - AWS * `azure` - Azure @@ -8766,6 +8835,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta - in: query name: filter[provider_type__in] schema: @@ -8789,6 +8859,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- Multiple values may be separated by commas. @@ -8807,6 +8878,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta explode: false style: form - in: query @@ -9593,6 +9665,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- * `aws` - AWS * `azure` - Azure @@ -9609,6 +9682,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta - in: query name: filter[provider__in] schema: @@ -9632,6 +9706,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- Multiple values may be separated by commas. @@ -9650,6 +9725,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta explode: false style: form - in: query @@ -9673,6 +9749,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- * `aws` - AWS * `azure` - Azure @@ -9689,6 +9766,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta - in: query name: filter[provider_type__in] schema: @@ -9712,6 +9790,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- Multiple values may be separated by commas. @@ -9730,6 +9809,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta explode: false style: form - name: filter[search] @@ -10400,6 +10480,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- * `aws` - AWS * `azure` - Azure @@ -10416,6 +10497,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta - in: query name: filter[provider_type__in] schema: @@ -10439,6 +10521,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- Multiple values may be separated by commas. @@ -10457,6 +10540,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta explode: false style: form - in: query @@ -10951,6 +11035,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- * `aws` - AWS * `azure` - Azure @@ -10967,6 +11052,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta - in: query name: filter[provider_type__in] schema: @@ -10990,6 +11076,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- Multiple values may be separated by commas. @@ -11008,6 +11095,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta explode: false style: form - in: query @@ -11315,6 +11403,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- * `aws` - AWS * `azure` - Azure @@ -11331,6 +11420,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta - in: query name: filter[provider_type__in] schema: @@ -11354,6 +11444,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- Multiple values may be separated by commas. @@ -11372,6 +11463,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta explode: false style: form - in: query @@ -11685,6 +11777,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- * `aws` - AWS * `azure` - Azure @@ -11701,6 +11794,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta - in: query name: filter[provider_type__in] schema: @@ -11724,6 +11818,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- Multiple values may be separated by commas. @@ -11742,6 +11837,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta explode: false style: form - in: query @@ -12580,6 +12676,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- * `aws` - AWS * `azure` - Azure @@ -12596,6 +12693,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta - in: query name: filter[provider_type__in] schema: @@ -12619,6 +12717,7 @@ paths: - openstack - oraclecloud - vercel + - okta description: |- Multiple values may be separated by commas. @@ -12637,6 +12736,7 @@ paths: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta explode: false style: form - in: query @@ -20115,6 +20215,23 @@ components: required: - clouds_yaml_content - clouds_yaml_cloud + - type: object + title: Okta OAuth Credentials + properties: + okta_client_id: + type: string + description: Client ID of the Okta API Services app used for OAuth 2.0 private-key JWT authentication. + okta_private_key: + type: string + description: PEM-encoded private key whose matching public key (JWK) is registered on the Okta service app. + okta_scopes: + type: array + items: + type: string + description: OAuth scopes to request. Optional; defaults to the minimum set required to run the currently enabled Okta checks. + required: + - okta_client_id + - okta_private_key - type: object title: Vercel API Token properties: @@ -21127,6 +21244,7 @@ components: - image - googleworkspace - vercel + - okta type: string description: |- * `aws` - AWS @@ -21144,6 +21262,7 @@ components: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta x-spec-enum-id: 91f917e0c3ab97e8 uid: type: string @@ -21265,6 +21384,7 @@ components: - image - googleworkspace - vercel + - okta type: string x-spec-enum-id: 91f917e0c3ab97e8 description: |- @@ -21285,6 +21405,7 @@ components: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta uid: type: string title: Unique identifier for the provider, set by the provider @@ -21337,6 +21458,7 @@ components: - image - googleworkspace - vercel + - okta type: string x-spec-enum-id: 91f917e0c3ab97e8 description: |- @@ -21357,6 +21479,7 @@ components: * `image` - Image * `googleworkspace` - Google Workspace * `vercel` - Vercel + * `okta` - Okta uid: type: string minLength: 3 @@ -22206,6 +22329,23 @@ components: required: - clouds_yaml_content - clouds_yaml_cloud + - type: object + title: Okta OAuth Credentials + properties: + okta_client_id: + type: string + description: Client ID of the Okta API Services app used for OAuth 2.0 private-key JWT authentication. + okta_private_key: + type: string + description: PEM-encoded private key whose matching public key (JWK) is registered on the Okta service app. + okta_scopes: + type: array + items: + type: string + description: OAuth scopes to request. Optional; defaults to the minimum set required to run the currently enabled Okta checks. + required: + - okta_client_id + - okta_private_key - type: object title: Vercel API Token properties: @@ -22631,6 +22771,23 @@ components: required: - clouds_yaml_content - clouds_yaml_cloud + - type: object + title: Okta OAuth Credentials + properties: + okta_client_id: + type: string + description: Client ID of the Okta API Services app used for OAuth 2.0 private-key JWT authentication. + okta_private_key: + type: string + description: PEM-encoded private key whose matching public key (JWK) is registered on the Okta service app. + okta_scopes: + type: array + items: + type: string + description: OAuth scopes to request. Optional; defaults to the minimum set required to run the currently enabled Okta checks. + required: + - okta_client_id + - okta_private_key - type: object title: Vercel API Token properties: @@ -23066,6 +23223,23 @@ components: required: - clouds_yaml_content - clouds_yaml_cloud + - type: object + title: Okta OAuth Credentials + properties: + okta_client_id: + type: string + description: Client ID of the Okta API Services app used for OAuth 2.0 private-key JWT authentication. + okta_private_key: + type: string + description: PEM-encoded private key whose matching public key (JWK) is registered on the Okta service app. + okta_scopes: + type: array + items: + type: string + description: OAuth scopes to request. Optional; defaults to the minimum set required to run the currently enabled Okta checks. + required: + - okta_client_id + - okta_private_key - type: object title: Vercel API Token properties: diff --git a/api/src/backend/api/tests/test_health.py b/api/src/backend/api/tests/test_health.py new file mode 100644 index 0000000000..76b72c0dc7 --- /dev/null +++ b/api/src/backend/api/tests/test_health.py @@ -0,0 +1,445 @@ +"""Tests for the health endpoints. + +Cover the IETF response envelope, status code mapping (200 / 503), the +``application/health+json`` media type and per-probe failure modes. +""" + +from unittest.mock import patch + +import pytest +from config import version as config_version +from django.core.cache import cache +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APIClient + +from api import health + + +HEALTH_MEDIA_TYPE = "application/health+json" + + +@pytest.fixture(autouse=True) +def _reset_health_state(): + """Per-test isolation: clear throttle counters and the readiness cache. + + DRF's ScopedRateThrottle persists state in Django's cache; without + clearing it the throttle budget would be shared across tests and trip + midway through the suite. + """ + cache.clear() + health._readiness_cache = None + yield + cache.clear() + health._readiness_cache = None + + +@pytest.fixture +def api_client(): + return APIClient() + + +def _assert_health_envelope(body): + """Every health response must carry the RFC top-level descriptors.""" + assert body["version"] == config_version.API_VERSION + assert body["releaseId"] == config_version.RELEASE_ID + assert body["serviceId"] == health.SERVICE_ID + assert body["description"] == health.SERVICE_DESCRIPTION + + +class TestLivenessEndpoint: + def test_returns_200_with_pass_status(self, api_client): + response = api_client.get(reverse("health-live")) + + assert response.status_code == status.HTTP_200_OK + assert response["Content-Type"].startswith(HEALTH_MEDIA_TYPE) + assert response["Cache-Control"] == health.CACHE_CONTROL_HEADER + body = response.json() + assert body["status"] == "pass" + _assert_health_envelope(body) + + def test_does_not_require_authentication(self, api_client): + api_client.credentials() + + response = api_client.get(reverse("health-live")) + + assert response.status_code == status.HTTP_200_OK + + def test_does_not_run_dependency_checks(self, api_client): + with ( + patch("api.health._probe_postgres") as mock_pg, + patch("api.health._probe_valkey") as mock_vk, + patch("api.health._probe_neo4j") as mock_neo, + ): + response = api_client.get(reverse("health-live")) + + assert response.status_code == status.HTTP_200_OK + mock_pg.assert_not_called() + mock_vk.assert_not_called() + mock_neo.assert_not_called() + + +class TestReadinessEndpoint: + @staticmethod + def _patch_probes(): + return ( + patch("api.health._probe_postgres", return_value=None), + patch("api.health._probe_valkey", return_value=None), + patch("api.health._probe_neo4j", return_value=None), + ) + + def test_returns_200_and_pass_when_all_dependencies_healthy(self, api_client): + with ( + patch("api.health._probe_postgres"), + patch("api.health._probe_valkey"), + patch("api.health._probe_neo4j"), + ): + response = api_client.get(reverse("health-ready")) + + assert response.status_code == status.HTTP_200_OK + assert response["Content-Type"].startswith(HEALTH_MEDIA_TYPE) + assert response["Cache-Control"] == health.CACHE_CONTROL_HEADER + + body = response.json() + _assert_health_envelope(body) + assert body["status"] == "pass" + + # Per RFC, `checks` values are arrays of one or more measurement + # objects. We use a single measurement per dependency. + assert set(body["checks"].keys()) == { + "postgres:responseTime", + "valkey:responseTime", + "neo4j:responseTime", + } + for key in body["checks"]: + entries = body["checks"][key] + assert isinstance(entries, list) and len(entries) == 1 + entry = entries[0] + assert entry["status"] == "pass" + assert entry["componentType"] == "datastore" + assert entry["observedUnit"] == "ms" + assert isinstance(entry["observedValue"], (int, float)) + assert entry["observedValue"] >= 0 + assert "time" in entry + # `output` must not leak when the check passed. + assert "output" not in entry + + def test_returns_503_and_fail_when_postgres_is_down(self, api_client): + with ( + patch( + "api.health._probe_postgres", + side_effect=RuntimeError("connection refused"), + ), + patch("api.health._probe_valkey"), + patch("api.health._probe_neo4j"), + ): + response = api_client.get(reverse("health-ready")) + + assert response.status_code == status.HTTP_503_SERVICE_UNAVAILABLE + body = response.json() + assert body["status"] == "fail" + pg_entry = body["checks"]["postgres:responseTime"][0] + assert pg_entry["status"] == "fail" + # Exception detail is never echoed in the response, only logged. + assert "output" not in pg_entry + assert body["checks"]["valkey:responseTime"][0]["status"] == "pass" + assert body["checks"]["neo4j:responseTime"][0]["status"] == "pass" + + def test_returns_503_and_fail_when_valkey_is_down(self, api_client): + with ( + patch("api.health._probe_postgres"), + patch("api.health._probe_valkey", side_effect=ConnectionError("timeout")), + patch("api.health._probe_neo4j"), + ): + response = api_client.get(reverse("health-ready")) + + assert response.status_code == status.HTTP_503_SERVICE_UNAVAILABLE + body = response.json() + assert body["status"] == "fail" + vk_entry = body["checks"]["valkey:responseTime"][0] + assert vk_entry["status"] == "fail" + assert "output" not in vk_entry + + def test_returns_503_and_fail_when_neo4j_is_down(self, api_client): + with ( + patch("api.health._probe_postgres"), + patch("api.health._probe_valkey"), + patch( + "api.health._probe_neo4j", + side_effect=RuntimeError("ServiceUnavailable"), + ), + ): + response = api_client.get(reverse("health-ready")) + + assert response.status_code == status.HTTP_503_SERVICE_UNAVAILABLE + body = response.json() + assert body["status"] == "fail" + neo_entry = body["checks"]["neo4j:responseTime"][0] + assert neo_entry["status"] == "fail" + assert "output" not in neo_entry + + def test_reports_all_failures_simultaneously(self, api_client): + with ( + patch("api.health._probe_postgres", side_effect=RuntimeError("pg down")), + patch("api.health._probe_valkey", side_effect=RuntimeError("vk down")), + patch("api.health._probe_neo4j", side_effect=RuntimeError("neo down")), + ): + response = api_client.get(reverse("health-ready")) + + assert response.status_code == status.HTTP_503_SERVICE_UNAVAILABLE + body = response.json() + assert body["status"] == "fail" + for key in ( + "postgres:responseTime", + "valkey:responseTime", + "neo4j:responseTime", + ): + entry = body["checks"][key][0] + assert entry["status"] == "fail" + # No dependency-specific error string leaks into the payload. + assert "output" not in entry + + def test_does_not_leak_exception_detail_on_failure(self, api_client): + # Sanity check: an exception message resembling infra detail + # (host, port, credentials) must not surface in the response under + # any field. + sensitive = ( + "connection to server at " + '"postgres-rw.prod.svc.cluster.local" (10.0.0.5), port 5432 ' + 'failed: FATAL: password authentication failed for user "prowler_user"' + ) + with ( + patch("api.health._probe_postgres", side_effect=RuntimeError(sensitive)), + patch("api.health._probe_valkey"), + patch("api.health._probe_neo4j"), + ): + response = api_client.get(reverse("health-ready")) + + body = response.json() + assert "output" not in body["checks"]["postgres:responseTime"][0] + payload_text = response.content.decode() + for token in ( + "postgres-rw", + "10.0.0.5", + "5432", + "prowler_user", + "password authentication failed", + ): + assert token not in payload_text + + def test_does_not_require_authentication(self, api_client): + with ( + patch("api.health._probe_postgres"), + patch("api.health._probe_valkey"), + patch("api.health._probe_neo4j"), + ): + api_client.credentials() + response = api_client.get(reverse("health-ready")) + + assert response.status_code == status.HTTP_200_OK + + +class TestReadinessCache: + """In-process cache caps the rate at which real probes hit the deps.""" + + def test_result_is_cached_for_ttl_seconds(self, api_client): + with ( + patch("api.health._probe_postgres") as pg, + patch("api.health._probe_valkey") as vk, + patch("api.health._probe_neo4j") as neo, + ): + r1 = api_client.get(reverse("health-ready")) + r2 = api_client.get(reverse("health-ready")) + + assert r1.status_code == status.HTTP_200_OK + assert r2.status_code == status.HTTP_200_OK + # Second request must not trigger fresh dep checks within the TTL. + assert pg.call_count == 1 + assert vk.call_count == 1 + assert neo.call_count == 1 + # The cached payload is returned verbatim (same timestamps too). + assert r1.json() == r2.json() + + def test_re_probes_after_cache_ttl_expires(self, api_client): + with ( + patch("api.health._probe_postgres") as pg, + patch("api.health._probe_valkey"), + patch("api.health._probe_neo4j"), + ): + api_client.get(reverse("health-ready")) + assert pg.call_count == 1 + + # Rewind the cached timestamp past the TTL so the next request + # is forced to recompute. + cached_ts, payload, http_status_code = health._readiness_cache + health._readiness_cache = ( + cached_ts - health.READINESS_CACHE_TTL_SECONDS - 0.1, + payload, + http_status_code, + ) + api_client.get(reverse("health-ready")) + + assert pg.call_count == 2 + + def test_cache_persists_a_failing_result(self, api_client): + # A failing readiness result is cached too; this is intentional so + # an attacker spamming the endpoint during an outage cannot amplify + # the dependency load. + with ( + patch("api.health._probe_postgres", side_effect=RuntimeError("down")) as pg, + patch("api.health._probe_valkey"), + patch("api.health._probe_neo4j"), + ): + r1 = api_client.get(reverse("health-ready")) + r2 = api_client.get(reverse("health-ready")) + + assert r1.status_code == status.HTTP_503_SERVICE_UNAVAILABLE + assert r2.status_code == status.HTTP_503_SERVICE_UNAVAILABLE + assert pg.call_count == 1 + + +class TestRateLimiting: + """The endpoints are unauthenticated and exposed; per-IP throttle caps + naive single-source floods.""" + + def test_live_blocks_after_budget_exhausted(self, api_client): + # Shrink the budget to 3 req per window so the test stays fast and + # deterministic. parse_rate runs once per throttle instance and + # each request gets a fresh instance, so this patch propagates. + from rest_framework.throttling import ScopedRateThrottle + + with patch.object(ScopedRateThrottle, "parse_rate", return_value=(3, 60)): + statuses = [ + api_client.get(reverse("health-live")).status_code for _ in range(4) + ] + + assert statuses[:3] == [status.HTTP_200_OK] * 3 + assert statuses[3] == status.HTTP_429_TOO_MANY_REQUESTS + + def test_ready_blocks_after_budget_exhausted(self, api_client): + from rest_framework.throttling import ScopedRateThrottle + + with ( + patch("api.health._probe_postgres"), + patch("api.health._probe_valkey"), + patch("api.health._probe_neo4j"), + patch.object(ScopedRateThrottle, "parse_rate", return_value=(2, 60)), + ): + statuses = [ + api_client.get(reverse("health-ready")).status_code for _ in range(3) + ] + + assert statuses[:2] == [status.HTTP_200_OK] * 2 + assert statuses[2] == status.HTTP_429_TOO_MANY_REQUESTS + + +class TestProbeImplementations: + """Smoke tests for each probe primitive.""" + + @pytest.mark.django_db + def test_postgres_probe_succeeds_against_real_db(self): + assert health._probe_postgres() is None + + def test_postgres_probe_propagates_db_errors(self): + class _BoomCursor: + def __enter__(self): + return self + + def __exit__(self, *_): + return False + + def execute(self, *_args, **_kwargs): + raise RuntimeError("boom") + + def fetchone(self): # pragma: no cover - never reached + return None + + with patch("api.health.connections") as mock_connections: + mock_connections.__getitem__.return_value.cursor.return_value = ( + _BoomCursor() + ) + with pytest.raises(RuntimeError, match="boom"): + health._probe_postgres() + + def test_valkey_probe_succeeds_when_ping_returns_true(self): + with patch("api.health.redis.Redis.from_url") as mock_from_url: + mock_from_url.return_value.ping.return_value = True + assert health._probe_valkey() is None + + def test_valkey_probe_raises_when_ping_returns_false(self): + with patch("api.health.redis.Redis.from_url") as mock_from_url: + mock_from_url.return_value.ping.return_value = False + with pytest.raises(RuntimeError, match="PING"): + health._probe_valkey() + + def test_valkey_probe_propagates_connection_errors(self): + with patch("api.health.redis.Redis.from_url") as mock_from_url: + mock_from_url.return_value.ping.side_effect = ConnectionError("nope") + with pytest.raises(ConnectionError, match="nope"): + health._probe_valkey() + + def test_valkey_probe_suppresses_redis_error_on_close(self): + # A redis-py-level failure releasing the socket must not mask a + # successful PING (best-effort cleanup contract). + import redis as redis_pkg + + with patch("api.health.redis.Redis.from_url") as mock_from_url: + client = mock_from_url.return_value + client.ping.return_value = True + client.close.side_effect = redis_pkg.RedisError("connection reset") + + assert health._probe_valkey() is None + + client.close.assert_called_once_with() + + def test_valkey_probe_suppresses_oserror_on_close(self): + # Socket-layer failures (OSError family) on close are also part of + # the swallowed scope. + with patch("api.health.redis.Redis.from_url") as mock_from_url: + client = mock_from_url.return_value + client.ping.return_value = True + client.close.side_effect = OSError("EBADF") + + assert health._probe_valkey() is None + + client.close.assert_called_once_with() + + def test_valkey_probe_lets_unexpected_close_errors_propagate(self): + # The suppress() is deliberately narrow: anything outside + # (redis.RedisError, OSError) must surface so it is not silently + # hidden. + with patch("api.health.redis.Redis.from_url") as mock_from_url: + client = mock_from_url.return_value + client.ping.return_value = True + client.close.side_effect = RuntimeError("bug") + + with pytest.raises(RuntimeError, match="bug"): + health._probe_valkey() + + def test_neo4j_probe_calls_verify_connectivity(self): + with patch("api.attack_paths.database.get_driver") as mock_get_driver: + mock_get_driver.return_value.verify_connectivity.return_value = None + assert health._probe_neo4j() is None + mock_get_driver.return_value.verify_connectivity.assert_called_once_with() + + def test_neo4j_probe_propagates_driver_errors(self): + with patch("api.attack_paths.database.get_driver") as mock_get_driver: + mock_get_driver.return_value.verify_connectivity.side_effect = RuntimeError( + "unreachable" + ) + with pytest.raises(RuntimeError, match="unreachable"): + health._probe_neo4j() + + +class TestStatusAggregation: + def test_pass_when_all_checks_pass(self): + entries = [{"status": "pass"}, {"status": "pass"}] + assert health._aggregate_status(entries) == "pass" + + def test_warn_when_any_check_warns_and_none_fail(self): + entries = [{"status": "pass"}, {"status": "warn"}] + assert health._aggregate_status(entries) == "warn" + + def test_fail_when_any_check_fails(self): + entries = [{"status": "pass"}, {"status": "warn"}, {"status": "fail"}] + assert health._aggregate_status(entries) == "fail" diff --git a/api/src/backend/api/tests/test_utils.py b/api/src/backend/api/tests/test_utils.py index 86da5567da..3a7f030a9c 100644 --- a/api/src/backend/api/tests/test_utils.py +++ b/api/src/backend/api/tests/test_utils.py @@ -31,6 +31,7 @@ from prowler.providers.image.image_provider import ImageProvider from prowler.providers.kubernetes.kubernetes_provider import KubernetesProvider from prowler.providers.m365.m365_provider import M365Provider from prowler.providers.mongodbatlas.mongodbatlas_provider import MongodbatlasProvider +from prowler.providers.okta.okta_provider import OktaProvider from prowler.providers.openstack.openstack_provider import OpenstackProvider from prowler.providers.oraclecloud.oraclecloud_provider import OraclecloudProvider from prowler.providers.vercel.vercel_provider import VercelProvider @@ -130,6 +131,7 @@ class TestReturnProwlerProvider: (Provider.ProviderChoices.OPENSTACK.value, OpenstackProvider), (Provider.ProviderChoices.IMAGE.value, ImageProvider), (Provider.ProviderChoices.VERCEL.value, VercelProvider), + (Provider.ProviderChoices.OKTA.value, OktaProvider), ], ) def test_return_prowler_provider(self, provider_type, expected_provider): @@ -238,6 +240,31 @@ class TestProwlerProviderConnectionTest: raise_on_exception=False, ) + @patch("api.utils.return_prowler_provider") + def test_prowler_provider_connection_test_okta_provider( + self, mock_return_prowler_provider + ): + """Test connection test for Okta provider passes org domain and provider_id.""" + provider = MagicMock() + provider.uid = "acme.okta.com" + provider.provider = Provider.ProviderChoices.OKTA.value + provider.secret.secret = { + "okta_client_id": "0oa123456789abcdef", + "okta_private_key": "-----BEGIN PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----", + "okta_scopes": ["okta.policies.read"], + } + mock_return_prowler_provider.return_value = MagicMock() + + prowler_provider_connection_test(provider) + mock_return_prowler_provider.return_value.test_connection.assert_called_once_with( + okta_client_id="0oa123456789abcdef", + okta_private_key="-----BEGIN PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----", + okta_scopes=["okta.policies.read"], + okta_org_domain="acme.okta.com", + provider_id="acme.okta.com", + raise_on_exception=False, + ) + @patch("api.utils.return_prowler_provider") def test_prowler_provider_connection_test_image_provider_no_creds( self, mock_return_prowler_provider @@ -308,6 +335,10 @@ class TestGetProwlerProviderKwargs: Provider.ProviderChoices.VERCEL.value, {"team_id": "provider_uid"}, ), + ( + Provider.ProviderChoices.OKTA.value, + {"okta_org_domain": "provider_uid"}, + ), ], ) def test_get_prowler_provider_kwargs(self, provider_type, expected_extra_kwargs): diff --git a/api/src/backend/api/tests/test_version.py b/api/src/backend/api/tests/test_version.py new file mode 100644 index 0000000000..ee5d167736 --- /dev/null +++ b/api/src/backend/api/tests/test_version.py @@ -0,0 +1,40 @@ +"""Drift checks for the API version constants. + +Guarantee that ``config.version`` always reflects the canonical +``[project].version`` declared in ``api/pyproject.toml``. +""" + +import tomllib +from pathlib import Path + +import pytest +from config import version as config_version + + +@pytest.fixture(scope="module") +def pyproject_data(): + here = Path(__file__).resolve() + for directory in here.parents: + candidate = directory / "pyproject.toml" + if not candidate.is_file(): + continue + with candidate.open("rb") as f: + data = tomllib.load(f) + if data.get("project", {}).get("name") == "prowler-api": + return data + raise AssertionError("api/pyproject.toml not reachable from the test runner") + + +def test_release_id_matches_pyproject(pyproject_data): + assert config_version.RELEASE_ID == pyproject_data["project"]["version"] + + +def test_api_version_is_major_of_release_id(): + assert config_version.API_VERSION == config_version.RELEASE_ID.split(".", 1)[0] + assert config_version.API_VERSION.isdigit() + + +def test_api_version_matches_v1_url_prefix(): + # The public contract version surfaced in the health payload must match + # the URL namespace the API is published under. + assert config_version.API_VERSION == "1" diff --git a/api/src/backend/api/tests/test_views.py b/api/src/backend/api/tests/test_views.py index 97796c97ca..ebd9128407 100644 --- a/api/src/backend/api/tests/test_views.py +++ b/api/src/backend/api/tests/test_views.py @@ -1625,6 +1625,21 @@ class TestProviderViewSet: "uid": "C12", "alias": "Google Workspace Minimum Length", }, + { + "provider": "okta", + "uid": "acme.okta.com", + "alias": "Okta Org", + }, + { + "provider": "okta", + "uid": "agency.okta-gov.com", + "alias": "Okta Gov Org", + }, + { + "provider": "okta", + "uid": "agency.okta.mil", + "alias": "Okta Mil Org", + }, ] ), ) @@ -2143,6 +2158,24 @@ class TestProviderViewSet: "googleworkspace-uid", "uid", ), + ( + { + "provider": "okta", + "uid": "https://acme.okta.com", + "alias": "test", + }, + "okta-uid", + "uid", + ), + ( + { + "provider": "okta", + "uid": "acme.example.com", + "alias": "test", + }, + "okta-uid", + "uid", + ), ] ), ) @@ -2163,6 +2196,25 @@ class TestProviderViewSet: == f"/data/attributes/{error_pointer}" ) + @pytest.mark.parametrize( + "input_uid,stored_uid", + [ + ("Acme.okta.com", "acme.okta.com"), + (" ACME.OKTA.COM ", "acme.okta.com"), + ("Agency.Okta-Gov.com", "agency.okta-gov.com"), + ], + ) + def test_providers_create_okta_uid_normalized( + self, authenticated_client, input_uid, stored_uid + ): + response = authenticated_client.post( + reverse("provider-list"), + data={"provider": "okta", "uid": input_uid, "alias": "Okta"}, + format="json", + ) + assert response.status_code == status.HTTP_201_CREATED + assert Provider.objects.get().uid == stored_uid + def test_providers_partial_update(self, authenticated_client, providers_fixture): provider1, *_ = providers_fixture new_alias = "This is the new name" @@ -2320,17 +2372,17 @@ class TestProviderViewSet: ), ("alias", "aws_testing_1", 1), ("alias.icontains", "aws", 2), - ("inserted_at", TODAY, 13), + ("inserted_at", TODAY, 14), ( "inserted_at.gte", "2024-01-01", - 13, + 14, ), ("inserted_at.lte", "2024-01-01", 0), ( "updated_at.gte", "2024-01-01", - 13, + 14, ), ("updated_at.lte", "2024-01-01", 0), ] @@ -2963,6 +3015,19 @@ class TestProviderSecretViewSet: "api_token": "fake-vercel-api-token-for-testing", }, ), + # Okta with inline private key credentials + ( + Provider.ProviderChoices.OKTA.value, + ProviderSecret.TypeChoices.STATIC, + { + "okta_client_id": "0oa123456789abcdef", + "okta_private_key": "-----BEGIN PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----", + "okta_scopes": [ + "okta.policies.read", + "okta.groups.read", + ], + }, + ), ], ) def test_provider_secrets_create_valid( @@ -3075,6 +3140,46 @@ class TestProviderSecretViewSet: == f"/data/attributes/{error_pointer}" ) + def test_provider_secrets_invalid_create_okta_missing_private_key( + self, + providers_fixture, + authenticated_client, + ): + okta_provider = next( + provider + for provider in providers_fixture + if provider.provider == Provider.ProviderChoices.OKTA.value + ) + data = { + "data": { + "type": "provider-secrets", + "attributes": { + "name": "Okta Secret", + "secret_type": ProviderSecret.TypeChoices.STATIC, + "secret": { + "okta_client_id": "0oa123456789abcdef", + }, + }, + "relationships": { + "provider": { + "data": {"type": "providers", "id": str(okta_provider.id)} + } + }, + } + } + + response = authenticated_client.post( + reverse("providersecret-list"), + data=json.dumps(data), + content_type="application/vnd.api+json", + ) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json()["errors"][0]["code"] == "required" + assert response.json()["errors"][0]["source"]["pointer"] == ( + "/data/attributes/secret/okta_private_key" + ) + def test_provider_secrets_partial_update( self, authenticated_client, provider_secret_fixture ): @@ -7053,6 +7158,32 @@ class TestFindingViewSet: "id" ] == str(finding_1.resources.first().id) + def test_findings_retrieve_include_resource_metadata( + self, authenticated_client, findings_fixture + ): + finding_1, *_ = findings_fixture + resource = finding_1.resources.first() + resource.metadata = '{"VulnerabilityID": "CVE-2026-0001"}' + resource.details = "Python 3.12 base image" + resource.save() + + response = authenticated_client.get( + reverse("finding-detail", kwargs={"pk": finding_1.id}), + {"include": "resources"}, + ) + assert response.status_code == status.HTTP_200_OK + + included_resource = next( + item + for item in response.json()["included"] + if item["type"] == "resources" and item["id"] == str(resource.id) + ) + assert ( + included_resource["attributes"]["metadata"] + == '{"VulnerabilityID": "CVE-2026-0001"}' + ) + assert included_resource["attributes"]["details"] == "Python 3.12 base image" + def test_findings_invalid_retrieve(self, authenticated_client): response = authenticated_client.get( reverse("finding-detail", kwargs={"pk": "random_id"}), @@ -15790,6 +15921,12 @@ class TestFindingGroupViewSet: assert attrs["fail_count"] == 0 assert attrs["resources_total"] == 1 assert attrs["resources_fail"] == 0 + # check_title / check_description are resolved post-pagination from the + # summary table, not from the finding's check_metadata. + assert attrs["check_title"] == "Ensure EC2 instances do not have public IPs" + assert ( + attrs["check_description"] == "EC2 instances should use private IPs only." + ) def test_finding_groups_status_pass_when_no_fail( self, authenticated_client, finding_groups_fixture @@ -17031,6 +17168,12 @@ class TestFindingGroupViewSet: assert attrs["fail_count"] == 0 assert attrs["resources_total"] == 1 assert attrs["resources_fail"] == 0 + # check_title / check_description are resolved post-pagination from the + # summary table, not from the finding's check_metadata. + assert attrs["check_title"] == "Ensure EC2 instances do not have public IPs" + assert ( + attrs["check_description"] == "EC2 instances should use private IPs only." + ) def test_finding_groups_latest_status_in_filter( self, authenticated_client, finding_groups_fixture @@ -17288,18 +17431,20 @@ class TestFindingGroupViewSet: check_ids = [item["id"] for item in data] assert check_ids == sorted(check_ids) - def test_finding_groups_latest_sort_by_check_title( + def test_finding_groups_latest_sort_by_check_title_not_supported( self, authenticated_client, finding_groups_fixture ): - """Test /latest supports sorting by check_title.""" + """check_title is not a sortable field for finding groups. + + Titles live in the TOASTed check_metadata blob and are resolved after + pagination from the summary table, so they cannot drive DB-level + ordering. Requesting that sort is rejected. + """ response = authenticated_client.get( reverse("finding-group-latest"), {"sort": "check_title"}, ) - assert response.status_code == status.HTTP_200_OK - data = response.json()["data"] - check_titles = [item["attributes"]["check_title"] for item in data] - assert check_titles == sorted(check_titles) + assert response.status_code == status.HTTP_400_BAD_REQUEST @pytest.mark.parametrize( "endpoint_name", ["finding-group-list", "finding-group-latest"] diff --git a/api/src/backend/api/utils.py b/api/src/backend/api/utils.py index b80d54b08a..8da8d91226 100644 --- a/api/src/backend/api/utils.py +++ b/api/src/backend/api/utils.py @@ -37,6 +37,7 @@ if TYPE_CHECKING: from prowler.providers.mongodbatlas.mongodbatlas_provider import ( MongodbatlasProvider, ) + from prowler.providers.okta.okta_provider import OktaProvider from prowler.providers.openstack.openstack_provider import OpenstackProvider from prowler.providers.oraclecloud.oraclecloud_provider import OraclecloudProvider from prowler.providers.vercel.vercel_provider import VercelProvider @@ -93,6 +94,7 @@ def return_prowler_provider( | KubernetesProvider | M365Provider | MongodbatlasProvider + | OktaProvider | OpenstackProvider | OraclecloudProvider | VercelProvider @@ -181,6 +183,10 @@ def return_prowler_provider( from prowler.providers.vercel.vercel_provider import VercelProvider prowler_provider = VercelProvider + case Provider.ProviderChoices.OKTA.value: + from prowler.providers.okta.okta_provider import OktaProvider + + prowler_provider = OktaProvider case _: raise ValueError(f"Provider type {provider.provider} not supported") return prowler_provider @@ -246,6 +252,11 @@ def get_prowler_provider_kwargs( **prowler_provider_kwargs, "team_id": provider.uid, } + elif provider.provider == Provider.ProviderChoices.OKTA.value: + prowler_provider_kwargs = { + **prowler_provider_kwargs, + "okta_org_domain": provider.uid, + } elif provider.provider == Provider.ProviderChoices.IMAGE.value: # Detect whether uid is a registry URL (e.g. "docker.io/andoniaf") or # a concrete image reference (e.g. "docker.io/andoniaf/myimage:latest"). @@ -290,6 +301,7 @@ def initialize_prowler_provider( | KubernetesProvider | M365Provider | MongodbatlasProvider + | OktaProvider | OpenstackProvider | OraclecloudProvider | VercelProvider @@ -351,6 +363,14 @@ def prowler_provider_connection_test(provider: Provider) -> Connection: "raise_on_exception": False, } return prowler_provider.test_connection(**vercel_kwargs) + elif provider.provider == Provider.ProviderChoices.OKTA.value: + okta_kwargs = { + **prowler_provider_kwargs, + "okta_org_domain": provider.uid, + "provider_id": provider.uid, + "raise_on_exception": False, + } + return prowler_provider.test_connection(**okta_kwargs) elif provider.provider == Provider.ProviderChoices.IMAGE.value: image_kwargs = { "image": provider.uid, diff --git a/api/src/backend/api/v1/serializer_utils/providers.py b/api/src/backend/api/v1/serializer_utils/providers.py index f8d67f0e3b..0b8b4eacf4 100644 --- a/api/src/backend/api/v1/serializer_utils/providers.py +++ b/api/src/backend/api/v1/serializer_utils/providers.py @@ -404,6 +404,26 @@ from rest_framework_json_api import serializers }, "required": ["clouds_yaml_content", "clouds_yaml_cloud"], }, + { + "type": "object", + "title": "Okta OAuth Credentials", + "properties": { + "okta_client_id": { + "type": "string", + "description": "Client ID of the Okta API Services app used for OAuth 2.0 private-key JWT authentication.", + }, + "okta_private_key": { + "type": "string", + "description": "PEM-encoded private key whose matching public key (JWK) is registered on the Okta service app.", + }, + "okta_scopes": { + "type": "array", + "items": {"type": "string"}, + "description": "OAuth scopes to request. Optional; defaults to the minimum set required to run the currently enabled Okta checks.", + }, + }, + "required": ["okta_client_id", "okta_private_key"], + }, { "type": "object", "title": "Vercel API Token", diff --git a/api/src/backend/api/v1/serializers.py b/api/src/backend/api/v1/serializers.py index 51cb95fbff..03d2fecad2 100644 --- a/api/src/backend/api/v1/serializers.py +++ b/api/src/backend/api/v1/serializers.py @@ -1397,6 +1397,7 @@ class ResourceIncludeSerializer(RLSSerializer): "service", "type_", "tags", + "metadata", "details", "partition", ] @@ -1404,6 +1405,7 @@ class ResourceIncludeSerializer(RLSSerializer): "id": {"read_only": True}, "inserted_at": {"read_only": True}, "updated_at": {"read_only": True}, + "metadata": {"read_only": True}, "details": {"read_only": True}, "partition": {"read_only": True}, } @@ -1543,6 +1545,8 @@ class BaseWriteProviderSecretSerializer(BaseWriteSerializer): serializer = GCPProviderSecret(data=secret) elif provider_type == Provider.ProviderChoices.GOOGLEWORKSPACE.value: serializer = GoogleWorkspaceProviderSecret(data=secret) + elif provider_type == Provider.ProviderChoices.OKTA.value: + serializer = OktaProviderSecret(data=secret) elif provider_type == Provider.ProviderChoices.GITHUB.value: serializer = GithubProviderSecret(data=secret) elif provider_type == Provider.ProviderChoices.IAC.value: @@ -1688,6 +1692,15 @@ class GoogleWorkspaceProviderSecret(serializers.Serializer): resource_name = "provider-secrets" +class OktaProviderSecret(serializers.Serializer): + okta_client_id = serializers.CharField() + okta_private_key = serializers.CharField() + okta_scopes = serializers.ListField(child=serializers.CharField(), required=False) + + class Meta: + resource_name = "provider-secrets" + + class MongoDBAtlasProviderSecret(serializers.Serializer): atlas_public_key = serializers.CharField() atlas_private_key = serializers.CharField() diff --git a/api/src/backend/api/v1/views.py b/api/src/backend/api/v1/views.py index 261b931297..a611d873da 100644 --- a/api/src/backend/api/v1/views.py +++ b/api/src/backend/api/v1/views.py @@ -4,6 +4,7 @@ import json import logging import os import time +import uuid from collections import defaultdict from copy import deepcopy from datetime import datetime, timedelta, timezone @@ -16,10 +17,11 @@ from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter from allauth.socialaccount.providers.saml.views import FinishACSView, LoginView from botocore.exceptions import ClientError, NoCredentialsError, ParamValidationError -from celery import chain +from celery import chain, states from celery.result import AsyncResult from config.custom_logging import BackendLogger from config.env import env +from config.version import RELEASE_ID from config.settings.social_login import ( GITHUB_OAUTH_CALLBACK_URL, GOOGLE_OAUTH_CALLBACK_URL, @@ -60,6 +62,7 @@ from django.utils.dateparse import parse_date from django.utils.decorators import method_decorator from django.views.decorators.cache import cache_control from django_celery_beat.models import PeriodicTask +from django_celery_results.models import TaskResult from drf_spectacular.settings import spectacular_settings from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import ( @@ -422,7 +425,7 @@ class SchemaView(SpectacularAPIView): def get(self, request, *args, **kwargs): spectacular_settings.TITLE = "Prowler API" - spectacular_settings.VERSION = "1.27.0" + spectacular_settings.VERSION = RELEASE_ID spectacular_settings.DESCRIPTION = ( "Prowler API specification.\n\nThis file is auto-generated." ) @@ -2534,28 +2537,45 @@ class ScanViewSet(BaseRLSViewSet): def create(self, request, *args, **kwargs): input_serializer = self.get_serializer(data=request.data) input_serializer.is_valid(raise_exception=True) + + # Broker publish is deferred to on_commit so the worker cannot read + # Scan before BaseRLSViewSet's dispatch-wide atomic commits. + pre_task_id = str(uuid.uuid4()) + with transaction.atomic(): scan = input_serializer.save() - with transaction.atomic(): - task = perform_scan_task.apply_async( - kwargs={ - "tenant_id": self.request.tenant_id, - "scan_id": str(scan.id), - "provider_id": str(scan.provider_id), - # Disabled for now - # checks_to_execute=scan.scanner_args.get("checks_to_execute") - }, + scan.task_id = pre_task_id + scan.save(update_fields=["task_id"]) + + attack_paths_db_utils.create_attack_paths_scan( + tenant_id=self.request.tenant_id, + scan_id=str(scan.id), + provider_id=str(scan.provider_id), ) - attack_paths_db_utils.create_attack_paths_scan( - tenant_id=self.request.tenant_id, - scan_id=str(scan.id), - provider_id=str(scan.provider_id), - ) + task_result, _ = TaskResult.objects.get_or_create( + task_id=pre_task_id, + defaults={"status": states.PENDING, "task_name": "scan-perform"}, + ) + prowler_task, _ = Task.objects.update_or_create( + id=pre_task_id, + tenant_id=self.request.tenant_id, + defaults={"task_runner_task": task_result}, + ) - prowler_task = Task.objects.get(id=task.id) - scan.task_id = task.id - scan.save(update_fields=["task_id"]) + scan_kwargs = { + "tenant_id": self.request.tenant_id, + "scan_id": str(scan.id), + "provider_id": str(scan.provider_id), + # Disabled for now + # checks_to_execute=scan.scanner_args.get("checks_to_execute") + } + + transaction.on_commit( + lambda: perform_scan_task.apply_async( + kwargs=scan_kwargs, task_id=pre_task_id + ) + ) self.response_serializer_class = TaskSerializer output_serializer = self.get_serializer(prowler_task) @@ -7349,6 +7369,15 @@ class FindingGroupViewSet(BaseRLSViewSet): output_field=IntegerField(), ) + # `check_title` / `check_description` are intentionally NOT resolved + # here. They live in the large JSONB `check_metadata` blob (TOASTed), + # so reading them per finding row is very expensive, and pulling them + # in via a correlated subquery makes Django add the subquery to GROUP + # BY, which re-evaluates it once per input row. They are identical for + # every finding of a `check_id`, so `_post_process_aggregation` fills + # them from the summary table's plain columns in a single batched + # lookup scoped to the paginated page. + # `pass_count`, `fail_count` and `manual_count` only count non-muted # findings. Muted findings are tracked separately via the # `*_muted_count` fields. @@ -7419,15 +7448,6 @@ class FindingGroupViewSet(BaseRLSViewSet): agg_failing_since=Min( "first_seen_at", filter=Q(status="FAIL", muted=False) ), - check_title=Coalesce( - Max(KeyTextTransform("checktitle", "check_metadata")), - Max(KeyTextTransform("CheckTitle", "check_metadata")), - Max(KeyTextTransform("Checktitle", "check_metadata")), - ), - check_description=Coalesce( - Max(KeyTextTransform("description", "check_metadata")), - Max(KeyTextTransform("Description", "check_metadata")), - ), ) .annotate( # Group is muted only if it has zero non-muted findings. @@ -7483,9 +7503,38 @@ class FindingGroupViewSet(BaseRLSViewSet): - Computes aggregated status (FAIL > PASS > MANUAL); the orthogonal ``muted`` boolean is already on the row from the SQL aggregation - Converts provider string to list + - Fills check_title / check_description for the findings path """ + rows = list(aggregated_data) + + # The findings-aggregation path omits check_title / check_description + # (they sit in TOASTed JSONB; see _aggregate_findings). Fill them from + # the summary table's plain columns in one query scoped to this page. + # The summary-aggregation path already carries them, so skip it there. + if rows and "check_title" not in rows[0]: + check_ids = [row["check_id"] for row in rows] + role = get_role(self.request.user, self.request.tenant_id) + summaries = FindingGroupDailySummary.objects.filter( + tenant_id=self.request.tenant_id, + check_id__in=check_ids, + ) + # Scope to the user's providers, mirroring get_queryset(), so titles + # are read only from providers the user can see. + if not role.unlimited_visibility: + summaries = summaries.filter(provider__in=get_providers(role)) + metadata_by_check = { + item["check_id"]: item + for item in summaries.order_by("check_id", "-inserted_at") + .distinct("check_id") + .values("check_id", "check_title", "check_description") + } + for row in rows: + metadata = metadata_by_check.get(row["check_id"], {}) + row["check_title"] = metadata.get("check_title") + row["check_description"] = metadata.get("check_description") + results = [] - for row in aggregated_data: + for row in rows: # Convert severity order back to string severity_order = row.get("severity_order", 1) row["severity"] = SEVERITY_ORDER_REVERSE.get( @@ -7531,7 +7580,6 @@ class FindingGroupViewSet(BaseRLSViewSet): _FINDING_GROUP_SORT_MAP = { "check_id": "check_id", - "check_title": "check_title", "severity": "severity_order", "status": "status_order", "muted": "muted", diff --git a/api/src/backend/config/django/base.py b/api/src/backend/config/django/base.py index 238825591f..317fe4fbfb 100644 --- a/api/src/backend/config/django/base.py +++ b/api/src/backend/config/django/base.py @@ -118,6 +118,8 @@ REST_FRAMEWORK = { "attack-paths-custom-query": env( "DJANGO_THROTTLE_ATTACK_PATHS_CUSTOM_QUERY", default="10/min" ), + "health-live": env("DJANGO_THROTTLE_HEALTH_LIVE", default="120/min"), + "health-ready": env("DJANGO_THROTTLE_HEALTH_READY", default="60/min"), }, } diff --git a/api/src/backend/config/urls.py b/api/src/backend/config/urls.py index c307bcc310..113f8bf5fc 100644 --- a/api/src/backend/config/urls.py +++ b/api/src/backend/config/urls.py @@ -1,5 +1,9 @@ from django.urls import include, path +from api.health import LivenessView, ReadinessView + urlpatterns = [ path("api/v1/", include("api.v1.urls")), + path("health/live", LivenessView.as_view(), name="health-live"), + path("health/ready", ReadinessView.as_view(), name="health-ready"), ] diff --git a/api/src/backend/config/version.py b/api/src/backend/config/version.py new file mode 100644 index 0000000000..86f14c53e2 --- /dev/null +++ b/api/src/backend/config/version.py @@ -0,0 +1,41 @@ +"""Single source of truth for the API version. + +The semantic version is read once from ``api/pyproject.toml`` at module +import; consumers (health payload, OpenAPI schema) read the resulting +constants. Fails fast at boot if the file cannot be located, so a +packaging mistake surfaces immediately rather than serving stale data. +""" + +from __future__ import annotations + +import tomllib +from pathlib import Path + +_PROJECT_NAME = "prowler-api" + + +def _discover_release_id() -> str: + here = Path(__file__).resolve() + for directory in here.parents: + candidate = directory / "pyproject.toml" + if not candidate.is_file(): + continue + with candidate.open("rb") as f: + data = tomllib.load(f) + project = data.get("project") or {} + if project.get("name") != _PROJECT_NAME: + continue + version = project.get("version") + if not isinstance(version, str) or not version: + raise RuntimeError( + f"{candidate} declares an empty or invalid [project].version" + ) + return version + raise RuntimeError( + f"Could not locate the {_PROJECT_NAME} pyproject.toml from {here}" + ) + + +RELEASE_ID: str = _discover_release_id() +# Public contract major (e.g. "1"); matches the /api/v1/ namespace. +API_VERSION: str = RELEASE_ID.split(".", 1)[0] diff --git a/api/src/backend/conftest.py b/api/src/backend/conftest.py index c5228c77be..eb73b0c51f 100644 --- a/api/src/backend/conftest.py +++ b/api/src/backend/conftest.py @@ -571,6 +571,12 @@ def providers_fixture(tenants_fixture): alias="vercel_testing", tenant_id=tenant.id, ) + provider14 = Provider.objects.create( + provider="okta", + uid="acme.okta.com", + alias="okta_testing", + tenant_id=tenant.id, + ) return ( provider1, @@ -586,6 +592,7 @@ def providers_fixture(tenants_fixture): provider11, provider12, provider13, + provider14, ) diff --git a/api/src/backend/tasks/jobs/export.py b/api/src/backend/tasks/jobs/export.py index 3be9b81544..1b9295cc67 100644 --- a/api/src/backend/tasks/jobs/export.py +++ b/api/src/backend/tasks/jobs/export.py @@ -47,6 +47,9 @@ from prowler.lib.outputs.compliance.csa.csa_oraclecloud import OracleCloudCSA from prowler.lib.outputs.compliance.ens.ens_aws import AWSENS from prowler.lib.outputs.compliance.ens.ens_azure import AzureENS from prowler.lib.outputs.compliance.ens.ens_gcp import GCPENS +from prowler.lib.outputs.compliance.asd_essential_eight.asd_essential_eight_aws import ( + ASDEssentialEightAWS, +) from prowler.lib.outputs.compliance.iso27001.iso27001_aws import AWSISO27001 from prowler.lib.outputs.compliance.iso27001.iso27001_azure import AzureISO27001 from prowler.lib.outputs.compliance.iso27001.iso27001_gcp import GCPISO27001 @@ -100,6 +103,7 @@ COMPLIANCE_CLASS_MAP = { (lambda name: name.startswith("ccc_"), CCC_AWS), (lambda name: name.startswith("c5_"), AWSC5), (lambda name: name.startswith("csa_"), AWSCSA), + (lambda name: name == "asd_essential_eight_aws", ASDEssentialEightAWS), ], "azure": [ (lambda name: name.startswith("cis_"), AzureCIS), diff --git a/api/src/backend/tasks/jobs/report.py b/api/src/backend/tasks/jobs/report.py index 12f48b7040..4cc3074bcd 100644 --- a/api/src/backend/tasks/jobs/report.py +++ b/api/src/backend/tasks/jobs/report.py @@ -20,11 +20,15 @@ from tasks.jobs.reports import ( ThreatScoreReportGenerator, ) from tasks.jobs.threatscore import compute_threatscore_metrics -from tasks.jobs.threatscore_utils import _aggregate_requirement_statistics_from_database +from tasks.jobs.threatscore_utils import ( + _aggregate_requirement_statistics_from_database, + _get_compliance_check_ids, +) from api.db_router import READ_REPLICA_ALIAS, MainRouter from api.db_utils import rls_transaction from api.models import Provider, Scan, ScanSummary, StateChoices, ThreatScoreSnapshot +from api.utils import initialize_prowler_provider from prowler.lib.check.compliance_models import Compliance from prowler.lib.outputs.finding import Finding as FindingOutput @@ -427,6 +431,7 @@ def generate_threatscore_report( provider_obj: Provider | None = None, requirement_statistics: dict[str, dict[str, int]] | None = None, findings_cache: dict[str, list[FindingOutput]] | None = None, + prowler_provider=None, ) -> None: """ Generate a PDF compliance report based on Prowler ThreatScore framework. @@ -455,6 +460,7 @@ def generate_threatscore_report( provider_obj=provider_obj, requirement_statistics=requirement_statistics, findings_cache=findings_cache, + prowler_provider=prowler_provider, only_failed=only_failed, ) @@ -469,6 +475,7 @@ def generate_ens_report( provider_obj: Provider | None = None, requirement_statistics: dict[str, dict[str, int]] | None = None, findings_cache: dict[str, list[FindingOutput]] | None = None, + prowler_provider=None, ) -> None: """ Generate a PDF compliance report for ENS RD2022 framework. @@ -495,6 +502,7 @@ def generate_ens_report( provider_obj=provider_obj, requirement_statistics=requirement_statistics, findings_cache=findings_cache, + prowler_provider=prowler_provider, include_manual=include_manual, ) @@ -510,6 +518,7 @@ def generate_nis2_report( provider_obj: Provider | None = None, requirement_statistics: dict[str, dict[str, int]] | None = None, findings_cache: dict[str, list[FindingOutput]] | None = None, + prowler_provider=None, ) -> None: """ Generate a PDF compliance report for NIS2 Directive (EU) 2022/2555. @@ -537,6 +546,7 @@ def generate_nis2_report( provider_obj=provider_obj, requirement_statistics=requirement_statistics, findings_cache=findings_cache, + prowler_provider=prowler_provider, only_failed=only_failed, include_manual=include_manual, ) @@ -553,6 +563,7 @@ def generate_csa_report( provider_obj: Provider | None = None, requirement_statistics: dict[str, dict[str, int]] | None = None, findings_cache: dict[str, list[FindingOutput]] | None = None, + prowler_provider=None, ) -> None: """ Generate a PDF compliance report for CSA Cloud Controls Matrix (CCM) v4.0. @@ -580,6 +591,7 @@ def generate_csa_report( provider_obj=provider_obj, requirement_statistics=requirement_statistics, findings_cache=findings_cache, + prowler_provider=prowler_provider, only_failed=only_failed, include_manual=include_manual, ) @@ -596,6 +608,7 @@ def generate_cis_report( provider_obj: Provider | None = None, requirement_statistics: dict[str, dict[str, int]] | None = None, findings_cache: dict[str, list[FindingOutput]] | None = None, + prowler_provider=None, ) -> None: """ Generate a PDF compliance report for a specific CIS Benchmark variant. @@ -627,6 +640,7 @@ def generate_cis_report( provider_obj=provider_obj, requirement_statistics=requirement_statistics, findings_cache=findings_cache, + prowler_provider=prowler_provider, only_failed=only_failed, include_manual=include_manual, ) @@ -771,6 +785,17 @@ def generate_compliance_reports( results["csa"] = {"upload": False, "path": ""} generate_csa = False + # Load the framework definitions for this provider once. We use this map + # both to pick the latest CIS variant and to precompute the set of + # check_ids each framework consumes (for findings_cache eviction). + frameworks_bulk: dict = {} + try: + frameworks_bulk = Compliance.get_bulk(provider_type) + except Exception as e: + logger.error("Error loading compliance frameworks for %s: %s", provider_type, e) + # Fall through; individual frameworks will still try and fail + # gracefully if their compliance_id is missing. + # For CIS we do NOT pre-check the provider against a hard-coded whitelist # (that list drifts the moment a new CIS JSON ships). Instead, we inspect # the dynamically loaded framework map and pick the latest available CIS @@ -778,7 +803,6 @@ def generate_compliance_reports( latest_cis: str | None = None if generate_cis: try: - frameworks_bulk = Compliance.get_bulk(provider_type) latest_cis = _pick_latest_cis_variant( name for name in frameworks_bulk.keys() if name.startswith("cis_") ) @@ -815,10 +839,84 @@ def generate_compliance_reports( tenant_id, scan_id ) - # Create shared findings cache - findings_cache = {} + # Initialize the Prowler provider once for the whole report batch. Each + # generator used to re-init this in _load_compliance_data, paying the + # boto3/Azure-SDK construction cost 5 times per scan. The instance is + # only used by FindingOutput.transform_api_finding to enrich findings, + # so a single shared instance is correct. + logger.info("Initializing prowler_provider once for all reports (scan %s)", scan_id) + try: + with rls_transaction(tenant_id, using=READ_REPLICA_ALIAS): + prowler_provider = initialize_prowler_provider(provider_obj) + except Exception as init_error: + # If init fails the generators will fall back to lazy init in + # _load_compliance_data; we just log and continue. + logger.warning( + "Could not pre-initialize prowler_provider for scan %s: %s", + scan_id, + init_error, + ) + prowler_provider = None + + # Create shared findings cache up front so the eviction closure below + # can reference it. Defined BEFORE the closure to avoid the UnboundLocalError + # trap if an early-return is later inserted between the closure and its + # first use. + findings_cache: dict[str, list[FindingOutput]] = {} logger.info("Created shared findings cache for all reports") + # Precompute the set of check_ids each framework consumes. After a + # framework finishes, every check_id that no remaining framework still + # needs is evicted from findings_cache so the dict does not keep + # growing through the batch (PROWLER-1733). + pending_checks_by_framework: dict[str, set[str]] = {} + if generate_threatscore: + pending_checks_by_framework["threatscore"] = _get_compliance_check_ids( + frameworks_bulk.get(f"prowler_threatscore_{provider_type}") + ) + if generate_ens: + pending_checks_by_framework["ens"] = _get_compliance_check_ids( + frameworks_bulk.get(f"ens_rd2022_{provider_type}") + ) + if generate_nis2: + pending_checks_by_framework["nis2"] = _get_compliance_check_ids( + frameworks_bulk.get(f"nis2_{provider_type}") + ) + if generate_csa: + pending_checks_by_framework["csa"] = _get_compliance_check_ids( + frameworks_bulk.get(f"csa_ccm_4.0_{provider_type}") + ) + if generate_cis and latest_cis: + pending_checks_by_framework["cis"] = _get_compliance_check_ids( + frameworks_bulk.get(latest_cis) + ) + + def _evict_after_framework(done_key: str) -> int: + """Drop from findings_cache every check_id no pending framework still needs.""" + done = pending_checks_by_framework.pop(done_key, set()) + still_needed: set[str] = ( + set().union(*pending_checks_by_framework.values()) + if pending_checks_by_framework + else set() + ) + exclusive = done - still_needed + evicted = 0 + for cid in exclusive: + if findings_cache.pop(cid, None) is not None: + evicted += 1 + if evicted: + logger.info( + "Evicted %d exclusive check entries from findings_cache after %s " + "(remaining cache size: %d)", + evicted, + done_key, + len(findings_cache), + ) + # Release the lists' memory now instead of waiting for the next + # gc cycle; FindingOutput instances retain quite a bit of state. + gc.collect() + return evicted + generated_report_keys: list[str] = [] output_paths: dict[str, str] = {} out_dir: str | None = None @@ -907,6 +1005,7 @@ def generate_compliance_reports( provider_obj=provider_obj, requirement_statistics=requirement_statistics, findings_cache=findings_cache, + prowler_provider=prowler_provider, ) # Compute and store ThreatScore metrics snapshot @@ -984,9 +1083,15 @@ def generate_compliance_reports( logger.warning("ThreatScore report saved locally at %s", out_dir) except Exception as e: - logger.error("Error generating ThreatScore report: %s", e) + logger.exception( + "compliance_report_failed framework=threatscore scan_id=%s tenant_id=%s", + scan_id, + tenant_id, + ) results["threatscore"] = {"upload": False, "path": "", "error": str(e)} + _evict_after_framework("threatscore") + # Generate ENS report if generate_ens: generated_report_keys.append("ens") @@ -1006,6 +1111,7 @@ def generate_compliance_reports( provider_obj=provider_obj, requirement_statistics=requirement_statistics, findings_cache=findings_cache, + prowler_provider=prowler_provider, ) upload_uri_ens = _upload_to_s3( @@ -1020,9 +1126,15 @@ def generate_compliance_reports( logger.warning("ENS report saved locally at %s", out_dir) except Exception as e: - logger.error("Error generating ENS report: %s", e) + logger.exception( + "compliance_report_failed framework=ens scan_id=%s tenant_id=%s", + scan_id, + tenant_id, + ) results["ens"] = {"upload": False, "path": "", "error": str(e)} + _evict_after_framework("ens") + # Generate NIS2 report if generate_nis2: generated_report_keys.append("nis2") @@ -1043,6 +1155,7 @@ def generate_compliance_reports( provider_obj=provider_obj, requirement_statistics=requirement_statistics, findings_cache=findings_cache, + prowler_provider=prowler_provider, ) upload_uri_nis2 = _upload_to_s3( @@ -1057,9 +1170,15 @@ def generate_compliance_reports( logger.warning("NIS2 report saved locally at %s", out_dir) except Exception as e: - logger.error("Error generating NIS2 report: %s", e) + logger.exception( + "compliance_report_failed framework=nis2 scan_id=%s tenant_id=%s", + scan_id, + tenant_id, + ) results["nis2"] = {"upload": False, "path": "", "error": str(e)} + _evict_after_framework("nis2") + # Generate CSA CCM report if generate_csa: generated_report_keys.append("csa") @@ -1080,6 +1199,7 @@ def generate_compliance_reports( provider_obj=provider_obj, requirement_statistics=requirement_statistics, findings_cache=findings_cache, + prowler_provider=prowler_provider, ) upload_uri_csa = _upload_to_s3( @@ -1094,9 +1214,15 @@ def generate_compliance_reports( logger.warning("CSA CCM report saved locally at %s", out_dir) except Exception as e: - logger.error("Error generating CSA CCM report: %s", e) + logger.exception( + "compliance_report_failed framework=csa scan_id=%s tenant_id=%s", + scan_id, + tenant_id, + ) results["csa"] = {"upload": False, "path": "", "error": str(e)} + _evict_after_framework("csa") + # Generate CIS Benchmark report for the latest available version only. # CIS ships multiple versions per provider (e.g. cis_1.4_aws, cis_5.0_aws, # cis_6.0_aws); we dynamically pick the highest semantic version at run @@ -1119,6 +1245,7 @@ def generate_compliance_reports( provider_obj=provider_obj, requirement_statistics=requirement_statistics, findings_cache=findings_cache, + prowler_provider=prowler_provider, ) upload_uri_cis = _upload_to_s3( @@ -1147,14 +1274,22 @@ def generate_compliance_reports( ) except Exception as e: - logger.error("Error generating CIS report %s: %s", latest_cis, e) + logger.exception( + "compliance_report_failed framework=cis variant=%s scan_id=%s tenant_id=%s", + latest_cis, + scan_id, + tenant_id, + ) results["cis"] = { "upload": False, "path": "", "error": str(e), } finally: - # Free ReportLab/matplotlib memory before moving on. + # Free ReportLab/matplotlib memory before moving on. CIS is + # always the last framework, so evicting its entries clears the + # cache entirely (subject to its check_ids set). + _evict_after_framework("cis") gc.collect() # Clean up temporary files only if all generated reports were diff --git a/api/src/backend/tasks/jobs/reports/base.py b/api/src/backend/tasks/jobs/reports/base.py index f348c6d0d2..4180c847d8 100644 --- a/api/src/backend/tasks/jobs/reports/base.py +++ b/api/src/backend/tasks/jobs/reports/base.py @@ -1,6 +1,9 @@ import gc import os +import resource as _resource_module +import time from abc import ABC, abstractmethod +from contextlib import contextmanager from dataclasses import dataclass, field from typing import Any @@ -41,6 +44,7 @@ from .config import ( COLOR_LIGHT_BLUE, COLOR_LIGHTER_BLUE, COLOR_PROWLER_DARK_GREEN, + FINDINGS_TABLE_CHUNK_SIZE, PADDING_LARGE, PADDING_SMALL, FrameworkConfig, @@ -48,6 +52,46 @@ from .config import ( logger = get_task_logger(__name__) + +@contextmanager +def _log_phase(phase: str, **tags: Any): + """Log start/end timing and RSS deltas around a long-running task section. + + Generic helper: callers pass arbitrary ``key=value`` tags + (e.g. ``scan_id``, ``framework``, ``provider_id``) and they are + emitted as part of the structured log line, so Grafana/Datadog/ + CloudWatch queries can pivot by whichever dimension is relevant to + the task. ``getrusage`` returns KB on Linux and bytes on macOS; + the values are still useful in relative terms even though units + differ across platforms. + """ + tag_str = " ".join(f"{key}={value}" for key, value in tags.items()) + suffix = f" {tag_str}" if tag_str else "" + + start = time.perf_counter() + rss_before = _resource_module.getrusage(_resource_module.RUSAGE_SELF).ru_maxrss + logger.info("phase_start phase=%s%s rss_kb=%d", phase, suffix, rss_before) + try: + yield + except Exception: + elapsed = time.perf_counter() - start + logger.exception( + "phase_failed phase=%s%s elapsed_s=%.2f", phase, suffix, elapsed + ) + raise + else: + elapsed = time.perf_counter() - start + rss_after = _resource_module.getrusage(_resource_module.RUSAGE_SELF).ru_maxrss + logger.info( + "phase_end phase=%s%s elapsed_s=%.2f rss_kb=%d delta_rss_kb=%d", + phase, + suffix, + elapsed, + rss_after, + rss_after - rss_before, + ) + + # Register fonts (done once at module load) _fonts_registered: bool = False @@ -335,6 +379,7 @@ class BaseComplianceReportGenerator(ABC): provider_obj: Provider | None = None, requirement_statistics: dict[str, dict[str, int]] | None = None, findings_cache: dict[str, list[FindingOutput]] | None = None, + prowler_provider: Any | None = None, **kwargs, ) -> None: """Generate the PDF compliance report. @@ -351,23 +396,35 @@ class BaseComplianceReportGenerator(ABC): provider_obj: Optional pre-fetched Provider object requirement_statistics: Optional pre-aggregated statistics findings_cache: Optional pre-loaded findings cache + prowler_provider: Optional pre-initialized Prowler provider. When + generating multiple reports for the same scan the master + function initializes this once and passes it in to avoid + re-running boto3/Azure-SDK setup per framework. **kwargs: Additional framework-specific arguments """ + framework = self.config.display_name logger.info( - "Generating %s report for scan %s", self.config.display_name, scan_id + "report_generation_start framework=%s scan_id=%s compliance_id=%s", + framework, + scan_id, + compliance_id, ) try: # 1. Load compliance data - data = self._load_compliance_data( - tenant_id=tenant_id, - scan_id=scan_id, - compliance_id=compliance_id, - provider_id=provider_id, - provider_obj=provider_obj, - requirement_statistics=requirement_statistics, - findings_cache=findings_cache, - ) + with _log_phase( + "load_compliance_data", scan_id=scan_id, framework=framework + ): + data = self._load_compliance_data( + tenant_id=tenant_id, + scan_id=scan_id, + compliance_id=compliance_id, + provider_id=provider_id, + provider_obj=provider_obj, + requirement_statistics=requirement_statistics, + findings_cache=findings_cache, + prowler_provider=prowler_provider, + ) # 2. Create PDF document doc = self._create_document(output_path, data) @@ -377,37 +434,54 @@ class BaseComplianceReportGenerator(ABC): elements = [] # Cover page (lightweight) - elements.extend(self.create_cover_page(data)) - elements.append(PageBreak()) + with _log_phase("cover_page", scan_id=scan_id, framework=framework): + elements.extend(self.create_cover_page(data)) + elements.append(PageBreak()) # Executive summary (framework-specific) - elements.extend(self.create_executive_summary(data)) + with _log_phase("executive_summary", scan_id=scan_id, framework=framework): + elements.extend(self.create_executive_summary(data)) # Body sections (charts + requirements index) # Override _build_body_sections() in subclasses to change section order - elements.extend(self._build_body_sections(data)) + with _log_phase("body_sections", scan_id=scan_id, framework=framework): + elements.extend(self._build_body_sections(data)) # Detailed findings - heaviest section, loads findings on-demand - logger.info("Building detailed findings section...") - elements.extend(self.create_detailed_findings(data, **kwargs)) - gc.collect() # Free findings data after processing + with _log_phase("detailed_findings", scan_id=scan_id, framework=framework): + elements.extend(self.create_detailed_findings(data, **kwargs)) + gc.collect() # Free findings data after processing # 4. Build the PDF - logger.info("Building PDF document with %d elements...", len(elements)) - self._build_pdf(doc, elements, data) + logger.info( + "doc_build_about_to_run framework=%s scan_id=%s elements=%d", + framework, + scan_id, + len(elements), + ) + with _log_phase("doc_build", scan_id=scan_id, framework=framework): + self._build_pdf(doc, elements, data) # Final cleanup del elements gc.collect() - logger.info("Successfully generated report at %s", output_path) + logger.info( + "report_generation_end framework=%s scan_id=%s output_path=%s", + framework, + scan_id, + output_path, + ) - except Exception as e: - import traceback - - tb_lineno = e.__traceback__.tb_lineno if e.__traceback__ else "unknown" - logger.error("Error generating report, line %s -- %s", tb_lineno, e) - logger.error("Full traceback:\n%s", traceback.format_exc()) + except Exception: + # logger.exception captures the full traceback; the contextual + # keys keep production search-by-scan-id viable. + logger.exception( + "report_generation_failed framework=%s scan_id=%s compliance_id=%s", + framework, + scan_id, + compliance_id, + ) raise def _build_body_sections(self, data: ComplianceData) -> list: @@ -638,15 +712,25 @@ class BaseComplianceReportGenerator(ABC): for req in requirements: check_ids_to_load.extend(req.checks) - # Load findings on-demand only for the checks that will be displayed - # Uses the shared findings cache to avoid duplicate queries across reports + # Load findings on-demand only for the checks that will be displayed. + # When ``only_failed`` is active at requirement level, also push the + # FAIL filter down to the finding level: a requirement marked FAIL + # because 1/1000 findings failed must not render a table dominated by + # 999 PASS rows. That hides the actual failure under noise and + # makes the per-check cap truncate the wrong rows. + # ``total_counts`` is populated with the pre-cap total per check_id + # (FAIL-only when only_failed is active) so the "Showing first N of + # M" banner uses the same denominator the reader cares about. logger.info("Loading findings on-demand for %d requirements", len(requirements)) + total_counts: dict[str, int] = {} findings_by_check_id = _load_findings_for_requirement_checks( data.tenant_id, data.scan_id, check_ids_to_load, data.prowler_provider, data.findings_by_check_id, # Pass the cache to update it + total_counts_out=total_counts, + only_failed_findings=only_failed, ) for req in requirements: @@ -678,9 +762,31 @@ class BaseComplianceReportGenerator(ABC): ) ) else: - # Create findings table - findings_table = self._create_findings_table(findings) - elements.append(findings_table) + # Surface truncation BEFORE the tables so readers see it + # at the same scroll position as the data itself, not + # after thousands of rendered rows. + loaded = len(findings) + total = total_counts.get(check_id, loaded) + if total > loaded: + kind = "failed findings" if only_failed else "findings" + elements.append( + Paragraph( + f"⚠ Showing first {loaded:,} of " + f"{total:,} {kind} for this check. " + f"Use the CSV or JSON-OCSF export for the full " + f"list. The PDF caps detail rows to keep " + f"the report readable and bounded in size.", + self.styles["normal"], + ) + ) + elements.append(Spacer(1, 0.05 * inch)) + + # Create chunked findings tables to prevent OOM when a + # single check has thousands of findings (ReportLab + # resolves layout per Flowable, so many small tables + # render contiguously with a bounded memory peak). + findings_tables = self._create_findings_tables(findings) + elements.extend(findings_tables) elements.append(Spacer(1, 0.1 * inch)) @@ -735,6 +841,7 @@ class BaseComplianceReportGenerator(ABC): provider_obj: Provider | None, requirement_statistics: dict | None, findings_cache: dict | None, + prowler_provider: Any | None = None, ) -> ComplianceData: """Load and aggregate compliance data from the database. @@ -746,6 +853,9 @@ class BaseComplianceReportGenerator(ABC): provider_obj: Optional pre-fetched Provider requirement_statistics: Optional pre-aggregated statistics findings_cache: Optional pre-loaded findings + prowler_provider: Optional pre-initialized Prowler provider. When + the master function initializes it once and passes it in, + we skip the per-report ``initialize_prowler_provider`` call. Returns: Aggregated ComplianceData object @@ -755,7 +865,8 @@ class BaseComplianceReportGenerator(ABC): if provider_obj is None: provider_obj = Provider.objects.get(id=provider_id) - prowler_provider = initialize_prowler_provider(provider_obj) + if prowler_provider is None: + prowler_provider = initialize_prowler_provider(provider_obj) provider_type = provider_obj.provider # Load compliance framework @@ -823,13 +934,32 @@ class BaseComplianceReportGenerator(ABC): ) -> SimpleDocTemplate: """Create the PDF document template. + Validates that ``output_path`` is a filesystem path string with an + existing parent directory. SimpleDocTemplate technically accepts a + BytesIO too, but we want every report to land on disk so the + Celery worker doesn't hold the full PDF in memory while uploading + to S3. + Args: output_path: Path for the output PDF data: Compliance data for metadata Returns: Configured SimpleDocTemplate + + Raises: + TypeError: ``output_path`` is not a string. + FileNotFoundError: The parent directory does not exist. """ + if not isinstance(output_path, str): + raise TypeError( + "output_path must be a filesystem path string; " + f"got {type(output_path).__name__}" + ) + parent_dir = os.path.dirname(output_path) + if parent_dir and not os.path.isdir(parent_dir): + raise FileNotFoundError(f"Output directory does not exist: {parent_dir}") + return SimpleDocTemplate( output_path, pagesize=letter, @@ -876,47 +1006,10 @@ class BaseComplianceReportGenerator(ABC): onLaterPages=add_footer, ) - def _create_findings_table(self, findings: list[FindingOutput]) -> Any: - """Create a findings table. - - Args: - findings: List of finding objects - - Returns: - ReportLab Table element - """ - - def get_finding_title(f): - metadata = getattr(f, "metadata", None) - if metadata: - return getattr(metadata, "CheckTitle", getattr(f, "check_id", "")) - return getattr(f, "check_id", "") - - def get_resource_name(f): - name = getattr(f, "resource_name", "") - if not name: - name = getattr(f, "resource_uid", "") - return name - - def get_severity(f): - metadata = getattr(f, "metadata", None) - if metadata: - return getattr(metadata, "Severity", "").capitalize() - return "" - - # Convert findings to dicts for the table - data = [] - for f in findings: - item = { - "title": get_finding_title(f), - "resource_name": get_resource_name(f), - "severity": get_severity(f), - "status": getattr(f, "status", "").upper(), - "region": getattr(f, "region", "global"), - } - data.append(item) - - columns = [ + # Column layout shared by all findings sub-tables. Defined as a method so + # subclasses can override it without re-implementing the chunking logic. + def _findings_table_columns(self) -> list[ColumnConfig]: + return [ ColumnConfig("Finding", 2.5 * inch, "title"), ColumnConfig("Resource", 3 * inch, "resource_name"), ColumnConfig("Severity", 0.9 * inch, "severity"), @@ -924,9 +1017,122 @@ class BaseComplianceReportGenerator(ABC): ColumnConfig("Region", 0.9 * inch, "region"), ] + @staticmethod + def _finding_to_row(f: FindingOutput) -> dict[str, str]: + """Project a FindingOutput onto the row dict the table expects. + + Kept defensive: missing metadata or attributes return empty strings + rather than raising, so a single malformed finding never breaks the + whole report. + """ + metadata = getattr(f, "metadata", None) + title = ( + getattr(metadata, "CheckTitle", getattr(f, "check_id", "")) + if metadata + else getattr(f, "check_id", "") + ) + resource_name = getattr(f, "resource_name", "") or getattr( + f, "resource_uid", "" + ) + severity = getattr(metadata, "Severity", "").capitalize() if metadata else "" + return { + "title": title, + "resource_name": resource_name, + "severity": severity, + "status": getattr(f, "status", "").upper(), + "region": getattr(f, "region", "global"), + } + + def _create_findings_tables( + self, + findings: list[FindingOutput], + chunk_size: int | None = None, + ) -> list[Any]: + """Build a list of small findings tables to keep ``doc.build()`` memory bounded. + + ReportLab resolves layout (column widths, row heights, page-breaks) + per Flowable. A single ``LongTable`` of 15k rows forces all of that + to be computed at once and reliably OOMs the worker on large scans. + Splitting into chunks of ``chunk_size`` rows produces an equivalent- + looking PDF (LongTable repeats headers; chunks render contiguously) + with a bounded memory peak per chunk. + + Args: + findings: List of finding objects for a single check. + chunk_size: Rows per sub-table. ``None`` uses + ``FINDINGS_TABLE_CHUNK_SIZE`` from config. + + Returns: + List of ReportLab flowables (interleaved ``Table``/``LongTable`` + and small ``Spacer`` between chunks). Empty list when there are + no findings. + """ + if not findings: + return [] + + chunk_size = chunk_size or FINDINGS_TABLE_CHUNK_SIZE + + # Build all rows first so we can chunk without re-walking the + # FindingOutput list. Malformed findings are skipped with a logged + # exception, never enough to abort the entire report. + rows: list[dict[str, str]] = [] + for f in findings: + try: + rows.append(self._finding_to_row(f)) + except Exception: + logger.exception( + "Skipping malformed finding while building table for check %s", + getattr(f, "check_id", "unknown"), + ) + + if not rows: + return [] + + columns = self._findings_table_columns() + + flowables: list = [] + total = len(rows) + for start in range(0, total, chunk_size): + chunk = rows[start : start + chunk_size] + flowables.append( + create_data_table( + data=chunk, + columns=columns, + header_color=self.config.primary_color, + normal_style=self.styles["normal_center"], + ) + ) + # A tiny spacer between chunks keeps them visually contiguous + # without forcing a page-break (KeepTogether would negate the + # memory benefit of chunking). + if start + chunk_size < total: + flowables.append(Spacer(1, 0.05 * inch)) + + if total > chunk_size: + logger.debug( + "Built %d findings sub-tables (chunk_size=%d, total_findings=%d)", + (total + chunk_size - 1) // chunk_size, + chunk_size, + total, + ) + + return flowables + + def _create_findings_table(self, findings: list[FindingOutput]) -> Any: + """Deprecated alias kept for backwards compatibility. + + Returns the first chunk produced by ``_create_findings_tables``. + New callers MUST use ``_create_findings_tables``, which returns a + list of flowables and is what ``create_detailed_findings`` invokes. + """ + flowables = self._create_findings_tables(findings) + if flowables: + return flowables[0] + # Empty input → return an empty (header-only) table so callers that + # used to receive a Table never get None. return create_data_table( - data=data, - columns=columns, + data=[], + columns=self._findings_table_columns(), header_color=self.config.primary_color, normal_style=self.styles["normal_center"], ) diff --git a/api/src/backend/tasks/jobs/reports/charts.py b/api/src/backend/tasks/jobs/reports/charts.py index 0f0338acab..da3f5aae10 100644 --- a/api/src/backend/tasks/jobs/reports/charts.py +++ b/api/src/backend/tasks/jobs/reports/charts.py @@ -1,9 +1,11 @@ import gc import io import math +import time from typing import Callable import matplotlib +from celery.utils.log import get_task_logger # Use non-interactive Agg backend for memory efficiency in server environments # This MUST be set before importing pyplot @@ -20,6 +22,26 @@ from .config import ( # noqa: E402 CHART_DPI_DEFAULT, ) +logger = get_task_logger(__name__) + + +def _log_chart_built(name: str, dpi: int, buffer: io.BytesIO, started: float) -> None: + """Emit a structured DEBUG line summarising a chart render. + + Centralised so the formatting stays consistent across all chart helpers + and so we never accidentally pay for buffer.getbuffer().nbytes when + debug logging is disabled. + """ + if logger.isEnabledFor(10): # logging.DEBUG + logger.debug( + "chart_built name=%s dpi=%d bytes=%d elapsed_s=%.2f", + name, + dpi, + buffer.getbuffer().nbytes, + time.perf_counter() - started, + ) + + # Use centralized DPI setting from config DEFAULT_CHART_DPI = CHART_DPI_DEFAULT @@ -77,6 +99,7 @@ def create_vertical_bar_chart( Returns: BytesIO buffer containing the PNG image """ + _started = time.perf_counter() if color_func is None: color_func = get_chart_color_for_percentage @@ -122,6 +145,7 @@ def create_vertical_bar_chart( plt.close(fig) gc.collect() # Force garbage collection after heavy matplotlib operation + _log_chart_built("vertical_bar", dpi, buffer, _started) return buffer @@ -156,6 +180,7 @@ def create_horizontal_bar_chart( Returns: BytesIO buffer containing the PNG image """ + _started = time.perf_counter() if color_func is None: color_func = get_chart_color_for_percentage @@ -207,6 +232,7 @@ def create_horizontal_bar_chart( plt.close(fig) gc.collect() # Force garbage collection after heavy matplotlib operation + _log_chart_built("horizontal_bar", dpi, buffer, _started) return buffer @@ -239,6 +265,7 @@ def create_radar_chart( Returns: BytesIO buffer containing the PNG image """ + _started = time.perf_counter() num_vars = len(labels) angles = [n / float(num_vars) * 2 * math.pi for n in range(num_vars)] @@ -275,6 +302,7 @@ def create_radar_chart( plt.close(fig) gc.collect() # Force garbage collection after heavy matplotlib operation + _log_chart_built("radar", dpi, buffer, _started) return buffer @@ -303,6 +331,7 @@ def create_pie_chart( Returns: BytesIO buffer containing the PNG image """ + _started = time.perf_counter() fig, ax = plt.subplots(figsize=figsize) _, _, autotexts = ax.pie( @@ -330,6 +359,7 @@ def create_pie_chart( plt.close(fig) gc.collect() # Force garbage collection after heavy matplotlib operation + _log_chart_built("pie", dpi, buffer, _started) return buffer @@ -362,6 +392,7 @@ def create_stacked_bar_chart( Returns: BytesIO buffer containing the PNG image """ + _started = time.perf_counter() fig, ax = plt.subplots(figsize=figsize) # Default colors if not provided @@ -401,4 +432,5 @@ def create_stacked_bar_chart( plt.close(fig) gc.collect() # Force garbage collection after heavy matplotlib operation + _log_chart_built("stacked_bar", dpi, buffer, _started) return buffer diff --git a/api/src/backend/tasks/jobs/reports/components.py b/api/src/backend/tasks/jobs/reports/components.py index 049cc043d3..31b808ec5c 100644 --- a/api/src/backend/tasks/jobs/reports/components.py +++ b/api/src/backend/tasks/jobs/reports/components.py @@ -475,8 +475,15 @@ def create_data_table( else: value = item.get(col.field, "") + # Wrap every string cell in Paragraph so the data rows keep the + # caller-supplied font/colour/alignment. Skipping Paragraph for + # short cells (a tempting micro-optimisation) breaks visual + # consistency: ReportLab Table falls back to Helvetica/black for + # raw strings, mixing fonts within the same table. + # ``escape_html`` keeps ``<``/``>``/``&`` in resource names from + # breaking Paragraph's mini-HTML parser. if normal_style and isinstance(value, str): - value = Paragraph(value, normal_style) + value = Paragraph(escape_html(value), normal_style) row.append(value) table_data.append(row) @@ -508,17 +515,26 @@ def create_data_table( for idx, col in enumerate(columns): styles.append(("ALIGN", (idx, 0), (idx, -1), col.align)) - # Alternate row backgrounds - skip for very large tables as it adds memory overhead + # Alternate row backgrounds: single O(1) ROWBACKGROUNDS style entry. + # The previous implementation appended N per-row BACKGROUND commands, + # which scaled the TableStyle list linearly with row count. ReportLab + # cycles through the colour list row-by-row so the visual is identical. + # The ALTERNATE_ROWS_MAX_SIZE cap is preserved to mirror legacy + # behaviour (very large tables stay plain), but the memory cost of the + # styles list is now constant regardless of row count. if ( alternate_rows and len(table_data) > 1 and len(table_data) <= ALTERNATE_ROWS_MAX_SIZE ): - for i in range(1, len(table_data)): - if i % 2 == 0: - styles.append( - ("BACKGROUND", (0, i), (-1, i), colors.Color(0.98, 0.98, 0.98)) - ) + styles.append( + ( + "ROWBACKGROUNDS", + (0, 1), + (-1, -1), + [colors.white, colors.Color(0.98, 0.98, 0.98)], + ) + ) table.setStyle(TableStyle(styles)) return table diff --git a/api/src/backend/tasks/jobs/reports/config.py b/api/src/backend/tasks/jobs/reports/config.py index 669f31c287..3b660e014a 100644 --- a/api/src/backend/tasks/jobs/reports/config.py +++ b/api/src/backend/tasks/jobs/reports/config.py @@ -1,3 +1,4 @@ +import os from dataclasses import dataclass, field from reportlab.lib import colors @@ -23,6 +24,47 @@ ALTERNATE_ROWS_MAX_SIZE = 200 # Larger = fewer queries but more memory per batch FINDINGS_BATCH_SIZE = 2000 +# Maximum rows per findings sub-table. ReportLab resolves layout per Flowable; +# splitting a huge findings list into multiple smaller tables keeps the peak +# memory of doc.build() bounded. A single 15k-row LongTable would force +# ReportLab to compute all column widths/row heights/page-breaks at once and +# OOM the worker; 300-row chunks are rendered contiguously with negligible +# visual impact. +FINDINGS_TABLE_CHUNK_SIZE = 300 + +# Maximum findings rendered per check in the detailed-findings section. +# +# Product behaviour: compliance PDFs render at most ``MAX_FINDINGS_PER_CHECK`` +# **failed** findings per check (PASS rows are excluded at SQL level by the +# ``only_failed`` flag that all four list-rendering frameworks default to: +# ThreatScore, NIS2, CSA, CIS; ENS does not render finding tables). Above +# this cap each affected check renders an in-PDF banner +# ("Showing first 100 of N failed findings for this check. Use the CSV +# or JSON export for the full list") so the reader knows the table is +# truncated and where to find the full data. +# +# Why a cap exists at all: +# * ``FindingOutput.transform_api_finding`` is O(N) per finding (Pydantic +# v1 validation + nested model construction). +# * ReportLab resolves layout per Flowable; thousands of sub-tables make +# ``doc.build()`` very slow and grow the PDF unboundedly. +# * A human-readable executive/auditor PDF does not need 12,000 rows for +# one check; that is forensic data and lives in the CSV/JSON exports. +# +# Why 100 specifically: +# * Covers ~99% of real scans without truncation (most checks emit far +# fewer than 100 findings even in enterprise estates). +# * Worst-case rendered rows = 100 × ~500 checks = 50k rows across all +# frameworks, which keeps RSS bounded and a 5-framework run completes +# in minutes instead of hours. +# +# Override at runtime via ``DJANGO_PDF_MAX_FINDINGS_PER_CHECK``: +# * Set to ``0`` to disable the cap entirely (load every finding; only +# advisable for small scans). +# * Set to a larger value (e.g. ``500``) for forensic detail in big runs; +# watch RSS in the Celery worker. +MAX_FINDINGS_PER_CHECK = int(os.environ.get("DJANGO_PDF_MAX_FINDINGS_PER_CHECK", "100")) + # ============================================================================= # Base colors diff --git a/api/src/backend/tasks/jobs/scan.py b/api/src/backend/tasks/jobs/scan.py index 515aea8620..a772173a7f 100644 --- a/api/src/backend/tasks/jobs/scan.py +++ b/api/src/backend/tasks/jobs/scan.py @@ -42,7 +42,6 @@ from api.db_utils import ( SET_CONFIG_QUERY, psycopg_connection, rls_transaction, - update_objects_in_batches, ) from api.exceptions import ProviderConnectionError from api.models import ( @@ -59,6 +58,7 @@ from api.models import ( ResourceFindingMapping, ResourceScanSummary, ResourceTag, + ResourceTagMapping, Scan, ScanCategorySummary, ScanGroupSummary, @@ -97,8 +97,16 @@ COMPLIANCE_REQUIREMENT_COPY_COLUMNS = ( ) # Controls how many findings we process per micro-batch before flushing to DB writes FINDINGS_MICRO_BATCH_SIZE = env.int("DJANGO_FINDINGS_MICRO_BATCH_SIZE", default=3000) -# Controls how many rows each ORM bulk_create/bulk_update call sends to Postgres -SCAN_DB_BATCH_SIZE = env.int("DJANGO_SCAN_DB_BATCH_SIZE", default=500) +# Controls how many rows each ORM bulk_create/bulk_update call sends to Postgres. +SCAN_DB_BATCH_SIZE = env.int("DJANGO_SCAN_DB_BATCH_SIZE", default=1000) +# Throttle scan progress persistence: minimum progress delta (fraction 0-1) +# between two persisted progress updates. +PROGRESS_THROTTLE_DELTA = env.float("DJANGO_SCAN_PROGRESS_THROTTLE_DELTA", default=0.01) +# Throttle scan progress persistence: maximum seconds without persisting progress +# regardless of delta (so slow checks still show progress in the UI). +PROGRESS_THROTTLE_SECONDS = env.float( + "DJANGO_SCAN_PROGRESS_THROTTLE_SECONDS", default=10.0 +) ATTACK_SURFACE_PROVIDER_COMPATIBILITY = { "internet-exposed": None, # Compatible with all providers @@ -202,8 +210,9 @@ def _get_attack_surface_mapping_from_provider(provider_type: str) -> dict: "iam_inline_policy_allows_privilege_escalation", }, "ec2-imdsv1": { - "ec2_instance_imdsv2_enabled" - }, # AWS only - IMDSv1 enabled findings + "ec2_instance_imdsv2_enabled", + "ec2_instance_account_imdsv2_enabled", + }, # AWS only - instance-level IMDSv1 exposure and account IMDS defaults } for category_name, check_ids in attack_surface_check_mappings.items(): if check_ids is None: @@ -527,16 +536,26 @@ def _process_finding_micro_batch( """ # Accumulate objects for bulk operations findings_to_create = [] - mappings_to_create = [] dirty_resources = {} + resources_with_new_tag_mappings: set[str] = set() resource_denormalized_data = [] # (finding_instance, resource_instance) pairs + tag_mappings_to_create: list[ResourceTagMapping] = [] skipped_findings_count = 0 # Track findings skipped due to UID length - # Prefetch last statuses for all findings in this batch - # TEMPORARY WORKAROUND: Filter out UIDs > 300 chars to avoid query errors - finding_uids = [ - f.uid for f in findings_batch if f is not None and len(f.uid) <= 300 - ] + # Separate findings into those persistable (uid <= 300) and over-limit. + # Resources/tags ARE still resolved for over-limit findings to preserve the + # original behavior (resources are persisted even when their finding is dropped). + non_null_findings = [f for f in findings_batch if f is not None] + persistable_findings = [f for f in non_null_findings if len(f.uid) <= 300] + skipped_findings_count = len(non_null_findings) - len(persistable_findings) + none_count = len(findings_batch) - len(non_null_findings) + if none_count: + logger.error( + f"{none_count} None finding(s) detected on scan {scan_instance.id}." + ) + + # Prefetch last statuses for all persistable findings in this batch (read replica) + finding_uids = [f.uid for f in persistable_findings] with rls_transaction(tenant_id, using=READ_REPLICA_ALIAS): last_statuses = { item["uid"]: (item["status"], item["first_seen_at"]) @@ -547,281 +566,411 @@ def _process_finding_micro_batch( .order_by("uid", "-inserted_at") .distinct("uid") } - # Update cache for uid, data in last_statuses.items(): if uid not in last_status_cache: last_status_cache[uid] = data - # Process each finding in the batch - for finding in findings_batch: - if finding is None: - logger.error(f"None finding detected on scan {scan_instance.id}.") - continue + # All DB writes for this micro-batch run inside ONE rls_transaction, + # with deadlock-retry at micro-batch granularity instead of per-finding. + for attempt in range(CELERY_DEADLOCK_ATTEMPTS): + try: + with rls_transaction(tenant_id): + # 1) Pre-resolve Resources in bulk + # Collect all uids referenced by this batch that are not in cache yet. + # NOTE: we intentionally include empty-string uids here. The SDK + # explicitly emits findings with `resource_uid=""` for some flows + # (IaC scans, some Azure/GCP/K8s checks). The original + # `get_or_create` behavior was to create/share a Resource with + # uid="" for these findings rather than dropping them. Preserve + # that behavior; do NOT filter by truthiness. + batch_resource_uids: set[str] = set() + for f in non_null_findings: + if f.resource_uid not in resource_cache: + batch_resource_uids.add(f.resource_uid) - # Process resource with deadlock retry - for attempt in range(CELERY_DEADLOCK_ATTEMPTS): - try: - with rls_transaction(tenant_id): - resource_uid = finding.resource_uid - if resource_uid not in resource_cache: - check_metadata = finding.get_metadata() - group = check_metadata.get("resourcegroup") or None - resource_instance, _ = Resource.objects.get_or_create( + if batch_resource_uids: + existing_resources = { + r.uid: r + for r in Resource.objects.filter( tenant_id=tenant_id, - provider=provider_instance, - uid=resource_uid, - defaults={ - "region": finding.region, - "service": finding.service_name, - "type": finding.resource_type, - "name": finding.resource_name, - "groups": [group] if group else None, - }, + provider_id=provider_instance.id, + uid__in=batch_resource_uids, ) - resource_cache[resource_uid] = resource_instance - resource_failed_findings_cache[resource_uid] = 0 - else: - resource_instance = resource_cache[resource_uid] - break - except (OperationalError, IntegrityError) as db_err: - if attempt < CELERY_DEADLOCK_ATTEMPTS - 1: - logger.warning( - f"{'Deadlock error' if isinstance(db_err, OperationalError) else 'Integrity error'} " - f"detected when processing resource {resource_uid} on scan {scan_instance.id}. Retrying..." + } + missing_uids = batch_resource_uids - existing_resources.keys() + if missing_uids: + # Build defaults from the first finding referencing each uid. + first_finding_per_uid: dict[str, ProwlerFinding] = {} + for f in non_null_findings: + if f.resource_uid in missing_uids: + first_finding_per_uid.setdefault(f.resource_uid, f) + resources_to_create = [] + for uid in missing_uids: + f = first_finding_per_uid[uid] + check_metadata = f.get_metadata() + group = check_metadata.get("resourcegroup") or None + resources_to_create.append( + Resource( + tenant_id=tenant_id, + provider=provider_instance, + uid=uid, + region=f.region, + service=f.service_name, + type=f.resource_type, + name=f.resource_name, + groups=[group] if group else None, + ) + ) + Resource.objects.bulk_create( + resources_to_create, + batch_size=SCAN_DB_BATCH_SIZE, + ignore_conflicts=True, + unique_fields=["tenant_id", "provider_id", "uid"], + ) + # Re-fetch to obtain instances we just created AND any + # created concurrently by another scan against the same provider. + existing_resources.update( + { + r.uid: r + for r in Resource.objects.filter( + tenant_id=tenant_id, + provider_id=provider_instance.id, + uid__in=missing_uids, + ) + } + ) + for uid, r in existing_resources.items(): + resource_cache[uid] = r + resource_failed_findings_cache.setdefault(uid, 0) + + # 2) Pre-resolve ResourceTags in bulk + batch_tag_kv: set[tuple[str, str]] = set() + for f in non_null_findings: + for k, v in f.resource_tags.items(): + if (k, v) not in tag_cache: + batch_tag_kv.add((k, v)) + + if batch_tag_kv: + keys_to_query = {k for k, _ in batch_tag_kv} + existing_tags = { + (t.key, t.value): t + for t in ResourceTag.objects.filter( + tenant_id=tenant_id, key__in=keys_to_query + ) + if (t.key, t.value) in batch_tag_kv + } + missing_kv = batch_tag_kv - existing_tags.keys() + if missing_kv: + ResourceTag.objects.bulk_create( + [ + ResourceTag(tenant_id=tenant_id, key=k, value=v) + for k, v in missing_kv + ], + batch_size=SCAN_DB_BATCH_SIZE, + ignore_conflicts=True, + unique_fields=["tenant_id", "key", "value"], + ) + existing_tags.update( + { + (t.key, t.value): t + for t in ResourceTag.objects.filter( + tenant_id=tenant_id, + key__in={k for k, _ in missing_kv}, + ) + if (t.key, t.value) in missing_kv + } + ) + tag_cache.update(existing_tags) + + # 3) Per-finding in-memory processing + for finding in non_null_findings: + resource_uid = finding.resource_uid + resource_instance = resource_cache.get(resource_uid) + if resource_instance is None: + # Should be unreachable after the pre-resolve step. Defensive log. + logger.error( + f"Resource {resource_uid} missing from cache after pre-resolve " + f"on scan {scan_instance.id}; skipping finding." + ) + continue + + # Detect resource field changes (defer save until end-of-batch bulk_update). + check_metadata = finding.get_metadata() + group = check_metadata.get("resourcegroup") or None + updated = False + if finding.region and resource_instance.region != finding.region: + resource_instance.region = finding.region + updated = True + if resource_instance.service != finding.service_name: + resource_instance.service = finding.service_name + updated = True + if resource_instance.type != finding.resource_type: + resource_instance.type = finding.resource_type + updated = True + if resource_instance.metadata != finding.resource_metadata: + resource_instance.metadata = json.dumps( + finding.resource_metadata, cls=CustomEncoder + ) + updated = True + if resource_instance.details != finding.resource_details: + resource_instance.details = finding.resource_details + updated = True + if resource_instance.partition != finding.partition: + resource_instance.partition = finding.partition + updated = True + if group and ( + not resource_instance.groups + or group not in resource_instance.groups + ): + resource_instance.groups = (resource_instance.groups or []) + [ + group + ] + updated = True + + if updated: + dirty_resources[resource_uid] = resource_instance + + # Accumulate ResourceTagMapping rows; bulk_create at end of block. + for k, v in finding.resource_tags.items(): + tag_instance = tag_cache.get((k, v)) + if tag_instance is None: + # Should not happen after pre-resolve; skip defensively. + continue + tag_mappings_to_create.append( + ResourceTagMapping( + tenant_id=tenant_id, + resource=resource_instance, + tag=tag_instance, + ) + ) + + unique_resources.add( + (resource_instance.uid, resource_instance.region) ) - time.sleep(0.1 * (2**attempt)) - continue - else: - raise db_err - # Track resource field changes (defer save) - updated = False - check_metadata = finding.get_metadata() - group = check_metadata.get("resourcegroup") or None - if finding.region and resource_instance.region != finding.region: - resource_instance.region = finding.region - updated = True - if resource_instance.service != finding.service_name: - resource_instance.service = finding.service_name - updated = True - if resource_instance.type != finding.resource_type: - resource_instance.type = finding.resource_type - updated = True - if resource_instance.metadata != finding.resource_metadata: - resource_instance.metadata = json.dumps( - finding.resource_metadata, cls=CustomEncoder - ) - updated = True - if resource_instance.details != finding.resource_details: - resource_instance.details = finding.resource_details - updated = True - if resource_instance.partition != finding.partition: - resource_instance.partition = finding.partition - updated = True - if group and ( - not resource_instance.groups or group not in resource_instance.groups - ): - resource_instance.groups = (resource_instance.groups or []) + [group] - updated = True + # TEMPORARY WORKAROUND: Skip findings with UID > 300 chars + # TODO: Remove this after implementing text field migration for finding.uid + if len(finding.uid) > 300: + logger.warning( + f"Skipping finding with UID exceeding 300 characters. " + f"Length: {len(finding.uid)}, " + f"Check: {finding.check_id}, " + f"Resource: {finding.resource_name}, " + f"UID: {finding.uid}" + ) + continue - if updated: - dirty_resources[resource_uid] = resource_instance - - # Process tags - tags = [] - with rls_transaction(tenant_id): - for key, value in finding.resource_tags.items(): - tag_key = (key, value) - if tag_key not in tag_cache: - tag_instance, _ = ResourceTag.objects.get_or_create( - tenant_id=tenant_id, key=key, value=value + finding_uid = finding.uid + last_status, last_first_seen_at = last_status_cache.get( + finding_uid, (None, None) ) - tag_cache[tag_key] = tag_instance - else: - tag_instance = tag_cache[tag_key] - tags.append(tag_instance) - resource_instance.upsert_or_delete_tags(tags=tags) - unique_resources.add((resource_instance.uid, resource_instance.region)) + status = FindingStatus[finding.status] + delta = _create_finding_delta(last_status, status) - # Prepare finding data - finding_uid = finding.uid + if not last_first_seen_at: + last_first_seen_at = datetime.now(tz=timezone.utc) - # TEMPORARY WORKAROUND: Skip findings with UID > 300 chars - # TODO: Remove this after implementing text field migration for finding.uid - if len(finding_uid) > 300: - skipped_findings_count += 1 - logger.warning( - f"Skipping finding with UID exceeding 300 characters. " - f"Length: {len(finding_uid)}, " - f"Check: {finding.check_id}, " - f"Resource: {finding.resource_name}, " - f"UID: {finding_uid}" - ) - continue + # Determine if finding should be muted and why + # Priority: mutelist processor (highest) > manual mute rules + is_muted = False + muted_reason = None + if finding.muted: + is_muted = True + muted_reason = "Muted by mutelist" + elif finding_uid in mute_rules_cache: + is_muted = True + muted_reason = mute_rules_cache[finding_uid] - last_status, last_first_seen_at = last_status_cache.get( - finding_uid, (None, None) - ) + if status == FindingStatus.FAIL and not is_muted: + resource_failed_findings_cache[resource_uid] += 1 - status = FindingStatus[finding.status] - delta = _create_finding_delta(last_status, status) + check_metadata["compliance"] = finding.compliance + finding_instance = Finding( + tenant_id=tenant_id, + uid=finding_uid, + delta=delta, + check_metadata=check_metadata, + status=status, + status_extended=finding.status_extended, + severity=finding.severity, + impact=finding.severity, + raw_result=finding.raw, + check_id=finding.check_id, + scan=scan_instance, + first_seen_at=last_first_seen_at, + muted=is_muted, + muted_at=datetime.now(tz=timezone.utc) if is_muted else None, + muted_reason=muted_reason, + compliance=finding.compliance, + categories=check_metadata.get("categories", []) or [], + resource_groups=check_metadata.get("resourcegroup") or None, + # Denormalized resource arrays populated directly on insert + # (was previously a separate bulk_update; saves a CASE WHEN + # over thousands of rows per micro-batch). + resource_regions=[resource_instance.region] + if resource_instance.region + else [], + resource_services=[resource_instance.service] + if resource_instance.service + else [], + resource_types=[resource_instance.type] + if resource_instance.type + else [], + ) + findings_to_create.append(finding_instance) + resource_denormalized_data.append( + (finding_instance, resource_instance) + ) - if not last_first_seen_at: - last_first_seen_at = datetime.now(tz=timezone.utc) + scan_resource_cache.add( + ( + str(resource_instance.id), + resource_instance.service, + resource_instance.region, + resource_instance.type, + ) + ) - # Determine if finding should be muted and why - # Priority: mutelist processor (highest) > manual mute rules - is_muted = False - muted_reason = None + aggregate_category_counts( + categories=check_metadata.get("categories", []) or [], + severity=finding.severity.value, + status=status.value, + delta=delta.value if delta else None, + muted=is_muted, + cache=scan_categories_cache, + ) - # Check mutelist processor first (highest priority) - if finding.muted: - is_muted = True - muted_reason = "Muted by mutelist" - # If not muted by mutelist, check manual mute rules - elif finding_uid in mute_rules_cache: - is_muted = True - muted_reason = mute_rules_cache[finding_uid] + aggregate_resource_group_counts( + resource_group=check_metadata.get("resourcegroup") or None, + severity=finding.severity.value, + status=status.value, + delta=delta.value if delta else None, + muted=is_muted, + resource_uid=resource_instance.uid if resource_instance else "", + cache=scan_resource_groups_cache, + group_resources_cache=group_resources_cache, + ) - # Increment failed_findings_count cache if needed - if status == FindingStatus.FAIL and not is_muted: - resource_failed_findings_cache[resource_uid] += 1 + # 4) Bulk create ResourceTagMappings + # Replaces the original per-resource `upsert_or_delete_tags` + # (which did one `update_or_create` + SELECT FOR UPDATE per mapping). + if tag_mappings_to_create: + # Pre-SELECT existing pairs: `bulk_create(ignore_conflicts=True)` + # does not populate `pk`, so we cannot tell new vs existing from + # the result; we need that to bump `updated_at` only on resources + # that actually gain a mapping. + candidate_resource_ids = { + m.resource_id for m in tag_mappings_to_create + } + candidate_tag_ids = {m.tag_id for m in tag_mappings_to_create} + existing_pairs = set( + ResourceTagMapping.objects.filter( + tenant_id=tenant_id, + resource_id__in=candidate_resource_ids, + tag_id__in=candidate_tag_ids, + ).values_list("resource_id", "tag_id") + ) + resource_uid_by_id = { + str(r.id): uid for uid, r in resource_cache.items() + } + for m in tag_mappings_to_create: + if (m.resource_id, m.tag_id) not in existing_pairs: + uid = resource_uid_by_id.get(str(m.resource_id)) + if uid is not None: + resources_with_new_tag_mappings.add(uid) - # Create finding object (don't save yet) - check_metadata = finding.get_metadata() - check_metadata["compliance"] = finding.compliance - finding_instance = Finding( - tenant_id=tenant_id, - uid=finding_uid, - delta=delta, - check_metadata=check_metadata, - status=status, - status_extended=finding.status_extended, - severity=finding.severity, - impact=finding.severity, - raw_result=finding.raw, - check_id=finding.check_id, - scan=scan_instance, - first_seen_at=last_first_seen_at, - muted=is_muted, - muted_at=datetime.now(tz=timezone.utc) if is_muted else None, - muted_reason=muted_reason, - compliance=finding.compliance, - categories=check_metadata.get("categories", []) or [], - resource_groups=check_metadata.get("resourcegroup") or None, - ) - findings_to_create.append(finding_instance) - resource_denormalized_data.append((finding_instance, resource_instance)) + ResourceTagMapping.objects.bulk_create( + tag_mappings_to_create, + batch_size=SCAN_DB_BATCH_SIZE, + ignore_conflicts=True, + unique_fields=["tenant_id", "resource_id", "tag_id"], + ) - # Track for scan summary - scan_resource_cache.add( - ( - str(resource_instance.id), - resource_instance.service, - resource_instance.region, - resource_instance.type, - ) - ) + # 5) Bulk create Findings + if findings_to_create: + Finding.objects.bulk_create( + findings_to_create, batch_size=SCAN_DB_BATCH_SIZE + ) - # Track categories with counts for ScanCategorySummary by (category, severity) - aggregate_category_counts( - categories=check_metadata.get("categories", []) or [], - severity=finding.severity.value, - status=status.value, - delta=delta.value if delta else None, - muted=is_muted, - cache=scan_categories_cache, - ) + # 6) Bulk create ResourceFindingMapping rows + mappings_to_create = [ + ResourceFindingMapping( + tenant_id=tenant_id, + resource=resource_instance, + finding=finding_instance, + ) + for finding_instance, resource_instance in resource_denormalized_data + ] + if mappings_to_create: + created_mappings = ResourceFindingMapping.objects.bulk_create( + mappings_to_create, + batch_size=SCAN_DB_BATCH_SIZE, + ignore_conflicts=True, + unique_fields=["tenant_id", "resource_id", "finding_id"], + ) + inserted = sum(1 for m in created_mappings if m.pk) + if inserted != len(mappings_to_create): + logger.error( + f"scan {scan_instance.id}: expected " + f"{len(mappings_to_create)} ResourceFindingMapping rows, " + f"inserted {inserted}. Rolling back micro-batch." + ) - # Track resource groups with counts for ScanGroupSummary - aggregate_resource_group_counts( - resource_group=check_metadata.get("resourcegroup") or None, - severity=finding.severity.value, - status=status.value, - delta=delta.value if delta else None, - muted=is_muted, - resource_uid=resource_instance.uid if resource_instance else "", - cache=scan_resource_groups_cache, - group_resources_cache=group_resources_cache, - ) - - # Bulk operations within single transaction - with rls_transaction(tenant_id): - # Bulk create findings - if findings_to_create: - Finding.objects.bulk_create( - findings_to_create, batch_size=SCAN_DB_BATCH_SIZE - ) - - # Bulk create resource-finding mappings - for finding_instance, resource_instance in resource_denormalized_data: - mappings_to_create.append( - ResourceFindingMapping( - tenant_id=tenant_id, - resource=resource_instance, - finding=finding_instance, + # 7) Bulk update Resources + # Union of: + # - resources whose fields changed (dirty_resources) + # - resources that got new tag mappings (need updated_at bump, + # preserving the original `self.save(update_fields=["updated_at"])` + # behavior of `upsert_or_delete_tags`) + all_resource_uids_to_touch = ( + set(dirty_resources.keys()) | resources_with_new_tag_mappings ) - ) - - if mappings_to_create: - created_mappings = ResourceFindingMapping.objects.bulk_create( - mappings_to_create, - batch_size=SCAN_DB_BATCH_SIZE, - ignore_conflicts=True, - unique_fields=["tenant_id", "resource_id", "finding_id"], - ) - inserted = sum(1 for m in created_mappings if m.pk) - if inserted != len(mappings_to_create): - logger.error( - f"scan {scan_instance.id}: expected " - f"{len(mappings_to_create)} ResourceFindingMapping rows, " - f"inserted {inserted}. Rolling back micro-batch." + if all_resource_uids_to_touch: + now_utc = datetime.now(tz=timezone.utc) + resources_to_bulk_update = [] + for uid in all_resource_uids_to_touch: + # Use the instance from dirty_resources if present (has mutated + # fields), otherwise the cached one (for updated_at bump only). + r = dirty_resources.get(uid) or resource_cache.get(uid) + if r is None: + continue + # Manually bump updated_at since bulk_update bypasses auto_now. + r.updated_at = now_utc + resources_to_bulk_update.append(r) + if resources_to_bulk_update: + Resource.objects.bulk_update( + resources_to_bulk_update, + [ + "metadata", + "details", + "partition", + "region", + "service", + "type", + "groups", + "updated_at", + ], + batch_size=1000, + ) + # Successful execution: leave deadlock retry loop. + break + except (OperationalError, IntegrityError) as db_err: + if attempt < CELERY_DEADLOCK_ATTEMPTS - 1: + logger.warning( + f"{'Deadlock error' if isinstance(db_err, OperationalError) else 'Integrity error'} " + f"on micro-batch for scan {scan_instance.id}. Retrying (attempt {attempt + 1})..." ) - - # Update finding denormalized arrays - findings_to_update = [] - for finding_instance, resource_instance in resource_denormalized_data: - if not finding_instance.resource_regions: - finding_instance.resource_regions = [] - if not finding_instance.resource_services: - finding_instance.resource_services = [] - if not finding_instance.resource_types: - finding_instance.resource_types = [] - - if resource_instance.region not in finding_instance.resource_regions: - finding_instance.resource_regions.append(resource_instance.region) - if resource_instance.service not in finding_instance.resource_services: - finding_instance.resource_services.append(resource_instance.service) - if resource_instance.type not in finding_instance.resource_types: - finding_instance.resource_types.append(resource_instance.type) - - findings_to_update.append(finding_instance) - - if findings_to_update: - Finding.objects.bulk_update( - findings_to_update, - ["resource_regions", "resource_services", "resource_types"], - batch_size=SCAN_DB_BATCH_SIZE, - ) - - # Bulk update dirty resources - if dirty_resources: - update_objects_in_batches( - tenant_id=tenant_id, - model=Resource, - objects=list(dirty_resources.values()), - fields=[ - "metadata", - "details", - "partition", - "region", - "service", - "type", - "groups", - ], - batch_size=1000, - ) + time.sleep(0.1 * (2**attempt)) + # Clear accumulators that we appended to inside the failed transaction + # so the retry produces consistent results. + findings_to_create.clear() + resource_denormalized_data.clear() + tag_mappings_to_create.clear() + dirty_resources.clear() + resources_with_new_tag_mappings.clear() + continue + raise # Log skipped findings summary if skipped_findings_count > 0: @@ -872,7 +1021,7 @@ def perform_prowler_scan( scan_instance = Scan.objects.get(pk=scan_id) scan_instance.state = StateChoices.EXECUTING scan_instance.started_at = datetime.now(tz=timezone.utc) - scan_instance.save() + scan_instance.save(update_fields=["state", "started_at", "updated_at"]) # Find the mutelist processor if it exists with rls_transaction(tenant_id, using=READ_REPLICA_ALIAS): @@ -917,7 +1066,13 @@ def perform_prowler_scan( provider_instance.connection_last_checked_at = datetime.now( tz=timezone.utc ) - provider_instance.save() + provider_instance.save( + update_fields=[ + "connected", + "connection_last_checked_at", + "updated_at", + ] + ) # If the provider is not connected, raise an exception outside the transaction. # If raised within the transaction, the transaction will be rolled back and the provider will not be marked @@ -932,6 +1087,13 @@ def perform_prowler_scan( last_status_cache = {} resource_failed_findings_cache = defaultdict(int) + # Throttle scan_instance progress writes to avoid hammering the writer: + # only persist when progress moves by at least `PROGRESS_THROTTLE_DELTA` + # OR `PROGRESS_THROTTLE_SECONDS` have elapsed. The final progress (1.0) + # always persists in the `finally` block below. + last_persisted_progress = -1.0 + last_persisted_progress_at = 0.0 + for progress, findings in prowler_scan.scan(): # Process findings in micro-batches findings_list = list(findings) @@ -958,10 +1120,20 @@ def perform_prowler_scan( group_resources_cache=group_resources_cache, ) - # Update scan progress - with rls_transaction(tenant_id): - scan_instance.progress = progress - scan_instance.save() + # Throttled progress save (the final save in the `finally` block + # below always runs regardless of throttle). + now = time.time() + progress_delta = progress - last_persisted_progress + elapsed = now - last_persisted_progress_at + if ( + progress_delta >= PROGRESS_THROTTLE_DELTA + or elapsed >= PROGRESS_THROTTLE_SECONDS + ): + with rls_transaction(tenant_id): + scan_instance.progress = progress + scan_instance.save(update_fields=["progress", "updated_at"]) + last_persisted_progress = progress + last_persisted_progress_at = now scan_instance.state = StateChoices.COMPLETED @@ -975,13 +1147,16 @@ def perform_prowler_scan( resources_to_update.append(resource_instance) if resources_to_update: - update_objects_in_batches( - tenant_id=tenant_id, - model=Resource, - objects=resources_to_update, - fields=["failed_findings_count"], - batch_size=1000, - ) + # Single rls_transaction wrapping the bulk_update (previously + # `update_objects_in_batches` opened one rls_transaction per + # chunk; for tenants with many resources this collapsed N + # BEGINs/COMMITs into 1). + with rls_transaction(tenant_id): + Resource.objects.bulk_update( + resources_to_update, + ["failed_findings_count"], + batch_size=SCAN_DB_BATCH_SIZE, + ) except Exception as e: logger.error(f"Error performing scan {scan_id}: {e}") @@ -993,7 +1168,16 @@ def perform_prowler_scan( scan_instance.duration = time.time() - start_time scan_instance.completed_at = datetime.now(tz=timezone.utc) scan_instance.unique_resource_count = len(unique_resources) - scan_instance.save() + scan_instance.save( + update_fields=[ + "state", + "duration", + "completed_at", + "unique_resource_count", + "progress", + "updated_at", + ] + ) if exception is not None: raise exception diff --git a/api/src/backend/tasks/jobs/threatscore_utils.py b/api/src/backend/tasks/jobs/threatscore_utils.py index 2ef29484ee..7be32c6ade 100644 --- a/api/src/backend/tasks/jobs/threatscore_utils.py +++ b/api/src/backend/tasks/jobs/threatscore_utils.py @@ -1,6 +1,8 @@ from celery.utils.log import get_task_logger from config.django.base import DJANGO_FINDINGS_BATCH_SIZE -from django.db.models import Count, Q +from django.db.models import Count, F, Q, Window +from django.db.models.functions import RowNumber +from tasks.jobs.reports.config import MAX_FINDINGS_PER_CHECK from api.db_router import READ_REPLICA_ALIAS from api.db_utils import rls_transaction @@ -154,6 +156,8 @@ def _load_findings_for_requirement_checks( check_ids: list[str], prowler_provider, findings_cache: dict[str, list[FindingOutput]] | None = None, + total_counts_out: dict[str, int] | None = None, + only_failed_findings: bool = False, ) -> dict[str, list[FindingOutput]]: """ Load findings for specific check IDs on-demand with optional caching. @@ -178,6 +182,23 @@ def _load_findings_for_requirement_checks( prowler_provider: The initialized Prowler provider instance. findings_cache (dict, optional): Cache of already loaded findings. If provided, checks are first looked up in cache before querying database. + total_counts_out (dict, optional): If provided, populated with + ``{check_id: total_findings_in_db}`` BEFORE any per-check cap is + applied. Lets callers render a "Showing first N of M" banner for + truncated checks. Only populated for ``check_ids`` actually + queried (cache hits keep whatever value the caller already had). + When ``only_failed_findings=True`` the total is FAIL-only. + only_failed_findings (bool): When True, push the ``status=FAIL`` + filter down into the SQL query so PASS rows are never loaded + from the DB nor pydantic-transformed. This matches the + ``only_failed`` requirement-level filter applied at PDF render + time: a requirement marked FAIL because 1/1000 findings failed + shouldn't render a table of 999 PASS rows. That hides the + actual failure under noise and wastes the per-check cap on + irrelevant data. NOTE: the findings cache stores whatever the + first caller asked for, so all callers in a single + ``generate_compliance_reports`` run MUST pass the same flag + (which they do: it threads from ``only_failed`` defaults). Returns: dict[str, list[FindingOutput]]: Dictionary mapping check_id to list of FindingOutput objects. @@ -222,17 +243,88 @@ def _load_findings_for_requirement_checks( ) with rls_transaction(tenant_id, using=READ_REPLICA_ALIAS): - # Use iterator with chunk_size for memory-efficient streaming - # chunk_size controls how many rows Django fetches from DB at once - findings_queryset = ( - Finding.all_objects.filter( - tenant_id=tenant_id, - scan_id=scan_id, - check_id__in=check_ids_to_load, - ) - .order_by("check_id", "uid") - .iterator(chunk_size=DJANGO_FINDINGS_BATCH_SIZE) + base_qs = Finding.all_objects.filter( + tenant_id=tenant_id, + scan_id=scan_id, + check_id__in=check_ids_to_load, ) + if only_failed_findings: + # Push the FAIL filter down into SQL: DB returns ~N×FAIL + # rows instead of N×ALL, and we never spend pydantic CPU on + # PASS findings the PDF would never render. + base_qs = base_qs.filter(status=StatusChoices.FAIL) + + # Aggregate totals once so we (a) know which checks need capping + # and (b) can surface "Showing first N of M" in the PDF banner. + # Cheap: a single COUNT grouped by check_id. + totals: dict[str, int] = { + row["check_id"]: row["total"] + for row in base_qs.values("check_id").annotate(total=Count("id")) + } + if total_counts_out is not None: + total_counts_out.update(totals) + + cap = MAX_FINDINGS_PER_CHECK + checks_over_cap = ( + {cid for cid, n in totals.items() if n > cap} if cap > 0 else set() + ) + + # Use iterator with chunk_size for memory-efficient streaming. + # FindingOutput.transform_api_finding (prowler/lib/outputs/finding.py) + # reads finding.resources.first() and resource.tags.all() per + # finding, which without prefetch generates 2N queries per chunk. + # prefetch_related runs once per iterator chunk (Django >=4.1) and + # collapses that into a constant 2 extra queries per chunk. + if checks_over_cap: + # Two-step query so we can both cap rows per check AND attach + # prefetch_related on the streamed results: + # + # 1) ``ranked`` annotates every matching finding with a + # per-check row number via a window function. The + # partition keeps numbering independent per check, and + # ordering by ``uid`` makes the "first N" selection + # deterministic across runs (same scan → same rows). + # + # 2) The outer ``Finding.all_objects.filter(id__in=...)`` + # keeps only IDs whose row number is within the cap and + # re-opens a plain queryset on it. Django cannot combine + # ``Window`` annotations with ``prefetch_related`` on the + # same queryset (the window is evaluated post-aggregation + # and the prefetch loader fights with it), so the inner + # SELECT becomes a subquery and the outer queryset is + # free to prefetch resources/tags as usual. + # + # PostgreSQL only materialises + # ``cap * |checks_over_cap| + sum(uncapped)`` rows for the + # window step, vs the full table scan the previous path did. + ranked = base_qs.annotate( + rn=Window( + expression=RowNumber(), + partition_by=[F("check_id")], + order_by=F("uid").asc(), + ) + ) + findings_queryset = ( + Finding.all_objects.filter( + id__in=ranked.filter(rn__lte=cap).values("id") + ) + .prefetch_related("resources", "resources__tags") + .order_by("check_id", "uid") + .iterator(chunk_size=DJANGO_FINDINGS_BATCH_SIZE) + ) + logger.info( + "Per-check cap=%d active for %d checks (max %d each); " + "skipping transform for surplus rows", + cap, + len(checks_over_cap), + cap, + ) + else: + findings_queryset = ( + base_qs.prefetch_related("resources", "resources__tags") + .order_by("check_id", "uid") + .iterator(chunk_size=DJANGO_FINDINGS_BATCH_SIZE) + ) # Pre-initialize empty lists for all check_ids to load # This avoids repeated dict lookups and 'if not in' checks @@ -248,7 +340,11 @@ def _load_findings_for_requirement_checks( findings_count += 1 logger.info( - f"Loaded {findings_count} findings for {len(check_ids_to_load)} checks" + "Loaded %d findings for %d checks (truncated %d checks total=%d)", + findings_count, + len(check_ids_to_load), + len(checks_over_cap), + sum(totals.values()), ) # Build result dict using cache references (no data duplication) @@ -258,3 +354,40 @@ def _load_findings_for_requirement_checks( } return result + + +def _get_compliance_check_ids(compliance_obj) -> set[str]: + """Return the union of all check_ids referenced by a compliance framework. + + Used by the master report orchestrator to know which checks each + framework consumes from the shared ``findings_cache``, so that once a + framework finishes the entries no other pending framework needs can be + evicted from the cache (PROWLER-1733). + + Args: + compliance_obj: A loaded Compliance framework object exposing a + ``Requirements`` iterable, each requirement carrying ``Checks``. + ``None`` is treated as "no checks" rather than raising, so the + caller can pass ``frameworks_bulk.get(...)`` directly without + an extra existence check. + + Returns: + Set of check_id strings (empty if ``compliance_obj`` is ``None``). + """ + if compliance_obj is None: + return set() + checks: set[str] = set() + requirements = getattr(compliance_obj, "Requirements", None) or [] + try: + # Defensive: Mock objects (used in unit tests) return another Mock + # for any attribute access, which is truthy but not iterable. Treat + # any non-iterable Requirements value as "no checks". + for req in requirements: + req_checks = getattr(req, "Checks", None) or [] + try: + checks.update(req_checks) + except TypeError: + continue + except TypeError: + return set() + return checks diff --git a/api/src/backend/tasks/tasks.py b/api/src/backend/tasks/tasks.py index 3d36d711d5..0fda2999a9 100644 --- a/api/src/backend/tasks/tasks.py +++ b/api/src/backend/tasks/tasks.py @@ -69,7 +69,7 @@ from tasks.utils import ( from api.compliance import get_compliance_frameworks from api.db_router import READ_REPLICA_ALIAS -from api.db_utils import rls_transaction +from api.db_utils import delete_related_daily_task, rls_transaction from api.decorators import handle_provider_deletion, set_tenant from api.models import Finding, Integration, Provider, Scan, ScanSummary, StateChoices from api.utils import initialize_prowler_provider @@ -274,6 +274,17 @@ def perform_scan_task( Returns: dict: The result of the scan execution, typically including the status and results of the performed checks. """ + with rls_transaction(tenant_id): + if not Provider.objects.filter(pk=provider_id).exists(): + logger.warning( + "scan-perform skipped: provider %s no longer exists " + "(tenant=%s, scan=%s)", + provider_id, + tenant_id, + scan_id, + ) + return None + result = perform_prowler_scan( tenant_id=tenant_id, scan_id=scan_id, @@ -310,6 +321,16 @@ def perform_scheduled_scan_task(self, tenant_id: str, provider_id: str): task_id = self.request.id with rls_transaction(tenant_id): + if not Provider.objects.filter(pk=provider_id).exists(): + logger.warning( + "scheduled scan-perform skipped: provider %s no longer exists " + "(tenant=%s)", + provider_id, + tenant_id, + ) + delete_related_daily_task(provider_id) + return None + periodic_task_instance = PeriodicTask.objects.get( name=f"scan-perform-scheduled-{provider_id}" ) diff --git a/api/src/backend/tasks/tests/test_reports.py b/api/src/backend/tasks/tests/test_reports.py index b5cb0cc38e..ad8a2ff29e 100644 --- a/api/src/backend/tasks/tests/test_reports.py +++ b/api/src/backend/tasks/tests/test_reports.py @@ -44,6 +44,8 @@ from api.models import ( Finding, Resource, ResourceFindingMapping, + ResourceTag, + ResourceTagMapping, StateChoices, StatusChoices, ) @@ -367,6 +369,317 @@ class TestLoadFindingsForChecks: assert result == {} + def test_prefetch_avoids_n_plus_one(self, tenants_fixture, scans_fixture): + """Loading N findings must NOT execute O(N) extra queries for resources/tags. + + Regression test for PROWLER-1733. ``FindingOutput.transform_api_finding`` + reads ``finding.resources.first()`` and ``resource.tags.all()`` per + finding. Without ``prefetch_related`` that's 2N additional queries; + with prefetch it collapses to a small constant per iterator chunk. + """ + from django.test.utils import CaptureQueriesContext + from django.db import connections + + tenant = tenants_fixture[0] + scan = scans_fixture[0] + + # Build N findings, each linked to one resource that owns 2 tags. + N = 20 + for i in range(N): + finding = Finding.objects.create( + tenant_id=tenant.id, + scan=scan, + uid=f"f-prefetch-{i}", + check_id="aws_check_prefetch", + status=StatusChoices.FAIL, + severity=Severity.high, + impact=Severity.high, + check_metadata={ + "provider": "aws", + "checkid": "aws_check_prefetch", + "checktitle": "t", + "checktype": [], + "servicename": "s", + "subservicename": "", + "severity": "high", + "resourcetype": "r", + "description": "", + "risk": "", + "relatedurl": "", + "remediation": { + "recommendation": {"text": "", "url": ""}, + "code": { + "nativeiac": "", + "terraform": "", + "cli": "", + "other": "", + }, + }, + "resourceidtemplate": "", + "categories": [], + "dependson": [], + "relatedto": [], + "notes": "", + }, + raw_result={}, + ) + resource = Resource.objects.create( + tenant_id=tenant.id, + provider=scan.provider, + uid=f"r-prefetch-{i}", + name=f"r-prefetch-{i}", + metadata="{}", + details="", + region="us-east-1", + service="s", + type="t::r", + ) + ResourceFindingMapping.objects.create( + tenant_id=tenant.id, finding=finding, resource=resource + ) + for k in ("env", "owner"): + tag, _ = ResourceTag.objects.get_or_create( + tenant_id=tenant.id, key=k, value=f"v-{i}-{k}" + ) + ResourceTagMapping.objects.create( + tenant_id=tenant.id, resource=resource, tag=tag + ) + + mock_provider = Mock() + mock_provider.type = "aws" + mock_provider.identity.account = "test" + + # Patch transform_api_finding to a no-op so the test isolates queries + # to the queryset/prefetch path (transform itself is exercised by + # the integration tests above and not by this regression check). + with patch( + "tasks.jobs.threatscore_utils.FindingOutput.transform_api_finding", + side_effect=lambda model, provider: Mock(check_id=model.check_id), + ): + with CaptureQueriesContext( + connections["default_read_replica"] + if "default_read_replica" in connections.databases + else connections["default"] + ) as ctx: + _load_findings_for_requirement_checks( + str(tenant.id), + str(scan.id), + ["aws_check_prefetch"], + mock_provider, + ) + + # Expected: a small constant number of queries irrespective of N. + # Pre-fix this would be ~1 + 2*N. We give some slack for RLS SET + # LOCAL statements that the rls_transaction emits. + assert len(ctx.captured_queries) < N, ( + f"Expected O(1) queries with prefetch_related; got " + f"{len(ctx.captured_queries)} for N={N} (N+1 regression?)" + ) + + def test_max_findings_per_check_cap(self, tenants_fixture, scans_fixture): + """When a check exceeds ``MAX_FINDINGS_PER_CHECK``, only ``cap`` rows + are loaded AND ``total_counts_out`` reports the pre-cap total. + + Guards the PROWLER-1733 truncation knob: prevents both runaway memory + and silent data loss in the PDF (the banner relies on knowing the + real total). + """ + from unittest.mock import patch as _patch + + tenant = tenants_fixture[0] + scan = scans_fixture[0] + + # Create 12 findings for a single check; cap to 5. + check_id = "aws_check_cap_test" + for i in range(12): + finding = Finding.objects.create( + tenant_id=tenant.id, + scan=scan, + uid=f"f-cap-{i:02d}", + check_id=check_id, + status=StatusChoices.FAIL, + severity=Severity.high, + impact=Severity.high, + check_metadata={}, + raw_result={}, + ) + resource = Resource.objects.create( + tenant_id=tenant.id, + provider=scan.provider, + uid=f"r-cap-{i:02d}", + name=f"r-cap-{i:02d}", + metadata="{}", + details="", + region="us-east-1", + service="s", + type="t::r", + ) + ResourceFindingMapping.objects.create( + tenant_id=tenant.id, finding=finding, resource=resource + ) + + mock_provider = Mock(type="aws") + mock_provider.identity.account = "test" + + totals: dict = {} + # Patch the cap to a small value AND skip the heavy transform so we + # only assert on row counts and totals. + with ( + _patch("tasks.jobs.threatscore_utils.MAX_FINDINGS_PER_CHECK", 5), + _patch( + "tasks.jobs.threatscore_utils.FindingOutput.transform_api_finding", + side_effect=lambda model, provider: Mock(check_id=model.check_id), + ), + ): + result = _load_findings_for_requirement_checks( + str(tenant.id), + str(scan.id), + [check_id], + mock_provider, + total_counts_out=totals, + ) + + assert ( + len(result[check_id]) == 5 + ), f"cap=5 should yield exactly 5 loaded findings, got {len(result[check_id])}" + assert ( + totals[check_id] == 12 + ), f"total_counts_out should report the pre-cap total (12), got {totals[check_id]}" + + def test_only_failed_findings_pushes_down_to_sql( + self, tenants_fixture, scans_fixture + ): + """When ``only_failed_findings=True``, PASS rows are excluded by the + DB filter, not just visually hidden afterwards. + + Regression for the consistency fix: previously the requirement-level + ``only_failed`` flag filtered which requirements appeared, but inside + each rendered requirement the table still showed PASS rows mixed + with FAIL, which combined with ``MAX_FINDINGS_PER_CHECK`` could + truncate to 1000 PASS findings and hide the actual failure. + """ + from unittest.mock import patch as _patch + + tenant = tenants_fixture[0] + scan = scans_fixture[0] + check_id = "aws_check_only_failed_test" + + # Mix PASS and FAIL so the filter has something to drop. + for i in range(6): + status = StatusChoices.FAIL if i % 2 == 0 else StatusChoices.PASS + finding = Finding.objects.create( + tenant_id=tenant.id, + scan=scan, + uid=f"f-of-{i:02d}", + check_id=check_id, + status=status, + severity=Severity.high, + impact=Severity.high, + check_metadata={}, + raw_result={}, + ) + resource = Resource.objects.create( + tenant_id=tenant.id, + provider=scan.provider, + uid=f"r-of-{i:02d}", + name=f"r-of-{i:02d}", + metadata="{}", + details="", + region="us-east-1", + service="s", + type="t::r", + ) + ResourceFindingMapping.objects.create( + tenant_id=tenant.id, finding=finding, resource=resource + ) + + mock_provider = Mock(type="aws") + mock_provider.identity.account = "test" + + totals: dict = {} + with _patch( + "tasks.jobs.threatscore_utils.FindingOutput.transform_api_finding", + side_effect=lambda model, provider: Mock( + check_id=model.check_id, status=model.status + ), + ): + result = _load_findings_for_requirement_checks( + str(tenant.id), + str(scan.id), + [check_id], + mock_provider, + total_counts_out=totals, + only_failed_findings=True, + ) + + # 3 FAIL + 3 PASS in DB; FAIL-only filter should load just 3. + loaded = result[check_id] + assert len(loaded) == 3, f"expected 3 FAIL findings, got {len(loaded)}" + statuses = {getattr(f, "status", None) for f in loaded} + assert statuses == { + StatusChoices.FAIL + }, f"expected all loaded findings to be FAIL; got statuses {statuses}" + # total_counts must reflect the FAIL-only total, not the global total. + assert ( + totals[check_id] == 3 + ), f"total_counts should be FAIL-only (3), got {totals[check_id]}" + + def test_max_findings_per_check_disabled(self, tenants_fixture, scans_fixture): + """``MAX_FINDINGS_PER_CHECK=0`` disables the cap; load all rows.""" + from unittest.mock import patch as _patch + + tenant = tenants_fixture[0] + scan = scans_fixture[0] + + check_id = "aws_check_uncapped" + for i in range(8): + f = Finding.objects.create( + tenant_id=tenant.id, + scan=scan, + uid=f"f-unc-{i:02d}", + check_id=check_id, + status=StatusChoices.FAIL, + severity=Severity.high, + impact=Severity.high, + check_metadata={}, + raw_result={}, + ) + r = Resource.objects.create( + tenant_id=tenant.id, + provider=scan.provider, + uid=f"r-unc-{i:02d}", + name=f"r-unc-{i:02d}", + metadata="{}", + details="", + region="us-east-1", + service="s", + type="t::r", + ) + ResourceFindingMapping.objects.create( + tenant_id=tenant.id, finding=f, resource=r + ) + + mock_provider = Mock(type="aws") + mock_provider.identity.account = "test" + totals: dict = {} + with ( + _patch("tasks.jobs.threatscore_utils.MAX_FINDINGS_PER_CHECK", 0), + _patch( + "tasks.jobs.threatscore_utils.FindingOutput.transform_api_finding", + side_effect=lambda model, provider: Mock(check_id=model.check_id), + ), + ): + result = _load_findings_for_requirement_checks( + str(tenant.id), + str(scan.id), + [check_id], + mock_provider, + total_counts_out=totals, + ) + + assert len(result[check_id]) == 8 + assert totals[check_id] == 8 + class TestCleanupStaleTmpOutputDirectories: """Unit tests for opportunistic stale cleanup under tmp output root.""" @@ -855,6 +1168,181 @@ class TestGenerateComplianceReportsOptimized: assert result["cis"] == {"upload": False, "path": ""} mock_cis.assert_not_called() + @patch("api.utils.initialize_prowler_provider") + @patch("tasks.jobs.report.rmtree") + @patch("tasks.jobs.report._upload_to_s3") + @patch("tasks.jobs.report.generate_cis_report") + @patch("tasks.jobs.report.generate_csa_report") + @patch("tasks.jobs.report.generate_nis2_report") + @patch("tasks.jobs.report.generate_ens_report") + @patch("tasks.jobs.report.generate_threatscore_report") + @patch("tasks.jobs.report._generate_compliance_output_directory") + @patch("tasks.jobs.report._aggregate_requirement_statistics_from_database") + @patch("tasks.jobs.report.Compliance.get_bulk") + @patch("tasks.jobs.report.Provider.objects.get") + @patch("tasks.jobs.report.ScanSummary.objects.filter") + def test_findings_cache_eviction_after_framework( + self, + mock_scan_summary_filter, + mock_provider_get, + mock_get_bulk, + mock_aggregate_stats, + mock_generate_output_dir, + mock_threatscore, + mock_ens, + mock_nis2, + mock_csa, + mock_cis, + mock_upload_to_s3, + mock_rmtree, + mock_init_provider, + ): + """After each framework finishes, exclusive entries are evicted. + + Threat scenario for PROWLER-1733: the shared ``findings_cache`` used + to grow monotonically through all 5 frameworks. With the new + eviction logic, check_ids only used by ThreatScore are dropped when + ThreatScore finishes, before ENS runs. + """ + from types import SimpleNamespace + from tasks.jobs import report as report_mod + + mock_scan_summary_filter.return_value.exists.return_value = True + mock_provider_get.return_value = Mock(uid="provider-uid", provider="aws") + # ThreatScore consumes {tsc_only, shared}; ENS consumes {ens_only, + # shared}. After ThreatScore evicts, tsc_only must be gone but + # shared and ens_only must remain. + mock_get_bulk.return_value = { + "prowler_threatscore_aws": SimpleNamespace( + Requirements=[SimpleNamespace(Checks=["tsc_only", "shared"])] + ), + "ens_rd2022_aws": SimpleNamespace( + Requirements=[SimpleNamespace(Checks=["ens_only", "shared"])] + ), + } + mock_aggregate_stats.return_value = {} + mock_generate_output_dir.return_value = "/tmp/tenant/scan/x/prowler-out" + mock_upload_to_s3.return_value = "s3://bucket/tenant/scan/x/report.pdf" + mock_init_provider.return_value = Mock(name="prowler_provider") + + # Seed the cache as if both frameworks had already loaded their + # findings. We mutate it indirectly: each generator wrapper is a + # Mock: make ThreatScore populate the cache, and have ENS observe + # the state at call time so we can introspect post-eviction. + observed_state: dict = {} + + def _threatscore_side_effect(**kwargs): + cache = kwargs["findings_cache"] + cache["tsc_only"] = ["tsc-finding"] + cache["shared"] = ["shared-finding"] + + def _ens_side_effect(**kwargs): + # ENS runs AFTER threatscore's _evict_after_framework("threatscore"). + observed_state["cache_keys_when_ens_runs"] = set( + kwargs["findings_cache"].keys() + ) + kwargs["findings_cache"]["ens_only"] = ["ens-finding"] + + mock_threatscore.side_effect = _threatscore_side_effect + mock_ens.side_effect = _ens_side_effect + + report_mod.generate_compliance_reports( + tenant_id=str(uuid.uuid4()), + scan_id=str(uuid.uuid4()), + provider_id=str(uuid.uuid4()), + generate_threatscore=True, + generate_ens=True, + generate_nis2=False, + generate_csa=False, + generate_cis=False, + ) + + # ``tsc_only`` was exclusive to ThreatScore → evicted before ENS ran. + # ``shared`` is still pending for ENS → must remain. + assert ( + "tsc_only" not in observed_state["cache_keys_when_ens_runs"] + ), "tsc_only should have been evicted before ENS ran" + assert ( + "shared" in observed_state["cache_keys_when_ens_runs"] + ), "shared must remain in cache because ENS still needs it" + + @patch("tasks.jobs.report.initialize_prowler_provider") + @patch("tasks.jobs.report.rmtree") + @patch("tasks.jobs.report._upload_to_s3") + @patch("tasks.jobs.report.generate_cis_report") + @patch("tasks.jobs.report.generate_csa_report") + @patch("tasks.jobs.report.generate_nis2_report") + @patch("tasks.jobs.report.generate_ens_report") + @patch("tasks.jobs.report.generate_threatscore_report") + @patch("tasks.jobs.report._generate_compliance_output_directory") + @patch("tasks.jobs.report._aggregate_requirement_statistics_from_database") + @patch("tasks.jobs.report.Compliance.get_bulk") + @patch("tasks.jobs.report.Provider.objects.get") + @patch("tasks.jobs.report.ScanSummary.objects.filter") + def test_prowler_provider_initialized_once( + self, + mock_scan_summary_filter, + mock_provider_get, + mock_get_bulk, + mock_aggregate_stats, + mock_generate_output_dir, + mock_threatscore, + mock_ens, + mock_nis2, + mock_csa, + mock_cis, + mock_upload_to_s3, + mock_rmtree, + mock_init_provider, + ): + """``initialize_prowler_provider`` must be called exactly once for + the whole batch (PROWLER-1733). Previously each generator re-init'd + the SDK provider in ``_load_compliance_data`` → 5 inits per scan. + """ + mock_scan_summary_filter.return_value.exists.return_value = True + mock_provider_get.return_value = Mock(uid="provider-uid", provider="aws") + # CIS variant discovery needs at least one cis_* key. + mock_get_bulk.return_value = {"cis_6.0_aws": Mock()} + mock_aggregate_stats.return_value = {} + mock_generate_output_dir.return_value = "/tmp/tenant/scan/x/prowler-out" + mock_upload_to_s3.return_value = "s3://bucket/tenant/scan/x/report.pdf" + mock_init_provider.return_value = Mock(name="prowler_provider") + + generate_compliance_reports( + tenant_id=str(uuid.uuid4()), + scan_id=str(uuid.uuid4()), + provider_id=str(uuid.uuid4()), + generate_threatscore=True, + generate_ens=True, + generate_nis2=True, + generate_csa=True, + generate_cis=True, + ) + + # All 5 wrappers were invoked once each… + mock_threatscore.assert_called_once() + mock_ens.assert_called_once() + mock_nis2.assert_called_once() + mock_csa.assert_called_once() + mock_cis.assert_called_once() + # …but the SDK provider was initialized only once. + assert mock_init_provider.call_count == 1, ( + f"expected 1 init, got {mock_init_provider.call_count} " + f"(prowler_provider must be shared across reports)" + ) + + # The shared instance must reach every wrapper as kwargs. + shared = mock_init_provider.return_value + for mock_wrapper in ( + mock_threatscore, + mock_ens, + mock_nis2, + mock_csa, + mock_cis, + ): + _, call_kwargs = mock_wrapper.call_args + assert call_kwargs.get("prowler_provider") is shared + @patch("tasks.jobs.report.rmtree") @patch("tasks.jobs.report._upload_to_s3") @patch("tasks.jobs.report.generate_threatscore_report") diff --git a/api/src/backend/tasks/tests/test_reports_base.py b/api/src/backend/tasks/tests/test_reports_base.py index d2fda4f830..6246654436 100644 --- a/api/src/backend/tasks/tests/test_reports_base.py +++ b/api/src/backend/tasks/tests/test_reports_base.py @@ -1269,6 +1269,48 @@ class TestComponentEdgeCases: # Should be a LongTable for large datasets assert isinstance(table, LongTable) + def test_zebra_uses_rowbackgrounds_not_per_row_background(self, monkeypatch): + """The styles list must contain exactly one ROWBACKGROUNDS entry + regardless of row count, never N per-row BACKGROUND entries. + """ + captured: dict = {} + + # Capture the list passed to TableStyle. create_data_table builds a + # list of style tuples and wraps it in a TableStyle exactly once; + # by patching TableStyle we intercept that list. + import tasks.jobs.reports.components as comp_mod + + original_table_style = comp_mod.TableStyle + + def _capture_table_style(style_list): + captured["styles"] = list(style_list) + return original_table_style(style_list) + + monkeypatch.setattr(comp_mod, "TableStyle", _capture_table_style) + + data = [{"name": f"Item {i}"} for i in range(60)] + columns = [ColumnConfig("Name", 2 * inch, "name")] + comp_mod.create_data_table(data, columns, alternate_rows=True) + + styles = captured["styles"] + # Count by command name. + names = [s[0] for s in styles if isinstance(s, tuple) and s] + # Exactly one ROWBACKGROUNDS entry. + assert names.count("ROWBACKGROUNDS") == 1 + # Zero per-row BACKGROUND entries on data rows. (The header row + # BACKGROUND command is intentional and lives at coords (0,0)/(-1,0).) + data_row_bg = [ + s + for s in styles + if isinstance(s, tuple) + and s[0] == "BACKGROUND" + and not (s[1] == (0, 0) and s[2] == (-1, 0)) + ] + assert data_row_bg == [], ( + f"expected no per-row BACKGROUND entries on data rows; " + f"got {len(data_row_bg)}" + ) + def test_create_risk_component_zero_values(self): """Test risk component with zero values.""" component = create_risk_component(risk_level=0, weight=0, score=0) @@ -1344,3 +1386,194 @@ class TestFrameworkConfigEdgeCases: assert get_framework_config("my_custom_threatscore_compliance") is not None assert get_framework_config("ens_something_else") is not None assert get_framework_config("nis2_gcp") is not None + + +# ============================================================================= +# Findings Table Chunking Tests (PROWLER-1733) +# ============================================================================= +# +# These tests guard the OOM-prevention behaviour added in PROWLER-1733: +# ``_create_findings_tables`` must split a list of findings into multiple +# small sub-tables instead of producing one giant Table, which would force +# ReportLab to resolve layout for all rows at once and OOM the worker on +# scans with thousands of findings per check. + + +class _DummyMetadata: + """Lightweight stand-in for FindingOutput.metadata used in chunking tests.""" + + def __init__(self, check_title: str = "Title", severity: str = "high"): + self.CheckTitle = check_title + self.Severity = severity + + +class _DummyFinding: + """Lightweight stand-in for FindingOutput used in chunking tests. + + The chunking code only reads a small set of attributes via ``getattr``, + so a duck-typed object is enough and lets the tests run without touching + the DB or pydantic deserialisation. + """ + + def __init__( + self, + check_id: str = "aws_check", + resource_name: str = "res-1", + resource_uid: str = "", + status: str = "FAIL", + region: str = "us-east-1", + with_metadata: bool = True, + ): + self.check_id = check_id + self.resource_name = resource_name + self.resource_uid = resource_uid + self.status = status + self.region = region + if with_metadata: + self.metadata = _DummyMetadata() + else: + self.metadata = None + + +def _make_concrete_generator(): + """Return a minimal concrete subclass of BaseComplianceReportGenerator.""" + + class _Concrete(BaseComplianceReportGenerator): + def create_executive_summary(self, data): + return [] + + def create_charts_section(self, data): + return [] + + def create_requirements_index(self, data): + return [] + + return _Concrete(FrameworkConfig(name="test", display_name="Test")) + + +class TestFindingsTableChunking: + """Tests for ``_create_findings_tables`` (PROWLER-1733).""" + + def test_chunking_produces_expected_number_of_subtables(self): + """5000 findings @ chunk_size=300 → 17 sub-tables + 16 spacers.""" + generator = _make_concrete_generator() + findings = [_DummyFinding(check_id="c1") for _ in range(5000)] + + flowables = generator._create_findings_tables(findings, chunk_size=300) + + tables = [f for f in flowables if isinstance(f, (Table, LongTable))] + spacers = [f for f in flowables if isinstance(f, Spacer)] + # ceil(5000 / 300) == 17 + assert len(tables) == 17 + # Spacer between every pair of contiguous tables, not after the last + assert len(spacers) == 16 + + def test_chunk_size_param_overrides_default(self): + """250 findings @ chunk_size=100 → 3 sub-tables.""" + generator = _make_concrete_generator() + findings = [_DummyFinding(check_id="c2") for _ in range(250)] + + flowables = generator._create_findings_tables(findings, chunk_size=100) + tables = [f for f in flowables if isinstance(f, (Table, LongTable))] + assert len(tables) == 3 + + def test_empty_findings_returns_empty_list(self): + """No findings → no flowables. Callers can extend(...) safely.""" + generator = _make_concrete_generator() + assert generator._create_findings_tables([]) == [] + + def test_single_chunk_has_no_spacer(self): + """A single sub-table must not emit a trailing spacer.""" + generator = _make_concrete_generator() + findings = [_DummyFinding(check_id="c3") for _ in range(10)] + + flowables = generator._create_findings_tables(findings, chunk_size=300) + assert len(flowables) == 1 + assert isinstance(flowables[0], (Table, LongTable)) + + def test_malformed_finding_is_skipped(self): + """A broken finding must not abort the report; it is logged and skipped.""" + generator = _make_concrete_generator() + + class _Broken: + # No attributes at all; getattr() defaults will mostly cope, but + # we force an explicit error by making the metadata attribute + # itself raise on access. + @property + def metadata(self): + raise RuntimeError("boom") + + check_id = "broken" + + findings = [ + _DummyFinding(check_id="c4"), + _Broken(), + _DummyFinding(check_id="c4"), + ] + flowables = generator._create_findings_tables(findings, chunk_size=300) + # Two good rows → one sub-table containing them; the broken one is + # logged and dropped, not propagated. + tables = [f for f in flowables if isinstance(f, (Table, LongTable))] + assert len(tables) == 1 + + def test_create_findings_table_alias_returns_first_chunk(self): + """The deprecated alias must keep returning a single Table flowable.""" + generator = _make_concrete_generator() + findings = [_DummyFinding(check_id="c5") for _ in range(700)] + + first = generator._create_findings_table(findings) + assert isinstance(first, (Table, LongTable)) + + def test_create_findings_table_alias_empty(self): + """Alias on empty input returns an empty (header-only) Table, not None.""" + generator = _make_concrete_generator() + result = generator._create_findings_table([]) + # The legacy alias never returned None; an empty header-only table + # is a strict superset of that contract. + assert isinstance(result, (Table, LongTable)) + + +# ============================================================================= +# Logging Context Manager Tests (PROWLER-1733) +# ============================================================================= + + +class TestLogPhaseContextManager: + """Tests for ``_log_phase`` (PROWLER-1733). + + The context manager emits structured ``phase_start`` / ``phase_end`` + logs with ``scan_id``, ``framework`` and ``elapsed_s``, so Datadog/ + CloudWatch queries can pivot by scan and find the slow section. + """ + + def test_emits_start_and_end_with_elapsed_and_rss(self, caplog): + from tasks.jobs.reports.base import _log_phase + + caplog.set_level("INFO", logger="tasks.jobs.reports.base") + with _log_phase("unit_test_phase", scan_id="s-1", framework="Test FW"): + pass + + messages = [r.getMessage() for r in caplog.records] + starts = [m for m in messages if "phase_start" in m] + ends = [m for m in messages if "phase_end" in m] + + assert len(starts) == 1 and len(ends) == 1 + assert "phase=unit_test_phase" in starts[0] + assert "scan_id=s-1" in starts[0] + assert "framework=Test FW" in starts[0] + assert "elapsed_s=" in ends[0] + assert "rss_kb=" in ends[0] + assert "delta_rss_kb=" in ends[0] + + def test_failure_logs_phase_failed_and_reraises(self, caplog): + from tasks.jobs.reports.base import _log_phase + + caplog.set_level("INFO", logger="tasks.jobs.reports.base") + with pytest.raises(RuntimeError, match="boom"): + with _log_phase("failing_phase", scan_id="s-2", framework="FW"): + raise RuntimeError("boom") + + messages = [r.getMessage() for r in caplog.records] + assert any("phase_failed" in m and "failing_phase" in m for m in messages) + # No phase_end on the failure path. + assert not any("phase_end" in m for m in messages) diff --git a/api/src/backend/tasks/tests/test_scan.py b/api/src/backend/tasks/tests/test_scan.py index dc13c31e08..0a7193cd4d 100644 --- a/api/src/backend/tasks/tests/test_scan.py +++ b/api/src/backend/tasks/tests/test_scan.py @@ -3853,6 +3853,7 @@ class TestAggregateAttackSurface: in result["privilege-escalation"] ) assert "ec2_instance_imdsv2_enabled" in result["ec2-imdsv1"] + assert "ec2_instance_account_imdsv2_enabled" in result["ec2-imdsv1"] @patch("tasks.jobs.scan.AttackSurfaceOverview.objects.bulk_create") @patch("tasks.jobs.scan.Finding.all_objects.filter") diff --git a/api/src/backend/tasks/tests/test_tasks.py b/api/src/backend/tasks/tests/test_tasks.py index bcebc2c9c9..f15bdf7192 100644 --- a/api/src/backend/tasks/tests/test_tasks.py +++ b/api/src/backend/tasks/tests/test_tasks.py @@ -21,6 +21,7 @@ from tasks.tasks import ( check_lighthouse_provider_connection_task, generate_outputs_task, perform_attack_paths_scan_task, + perform_scan_task, perform_scheduled_scan_task, reaggregate_all_finding_group_summaries_task, refresh_lighthouse_provider_models_task, @@ -2454,6 +2455,57 @@ class TestPerformScheduledScanTask: == 1 ) + def test_no_op_when_provider_does_not_exist(self, tenants_fixture): + """Return None without raising when the provider was already deleted.""" + tenant = tenants_fixture[0] + missing_provider_id = str(uuid.uuid4()) + task_id = str(uuid.uuid4()) + self._create_task_result(tenant.id, task_id) + # Orphan PeriodicTask left behind from a previous lifecycle. + self._create_periodic_task(missing_provider_id, tenant.id) + orphan_name = f"scan-perform-scheduled-{missing_provider_id}" + assert PeriodicTask.objects.filter(name=orphan_name).exists() + + with ( + patch("tasks.tasks.perform_prowler_scan") as mock_scan, + patch("tasks.tasks._perform_scan_complete_tasks") as mock_complete_tasks, + self._override_task_request(perform_scheduled_scan_task, id=task_id), + ): + result = perform_scheduled_scan_task.run( + tenant_id=str(tenant.id), provider_id=missing_provider_id + ) + + assert result is None + mock_scan.assert_not_called() + mock_complete_tasks.assert_not_called() + # Orphan PeriodicTask is cleaned up so beat stops re-firing it. + assert not PeriodicTask.objects.filter(name=orphan_name).exists() + + +@pytest.mark.django_db +class TestPerformScanTask: + """Unit tests for perform_scan_task.""" + + def test_no_op_when_provider_does_not_exist(self, tenants_fixture): + """Return None without raising when the provider was already deleted.""" + tenant = tenants_fixture[0] + missing_provider_id = str(uuid.uuid4()) + scan_id = str(uuid.uuid4()) + + with ( + patch("tasks.tasks.perform_prowler_scan") as mock_scan, + patch("tasks.tasks._perform_scan_complete_tasks") as mock_complete_tasks, + ): + result = perform_scan_task.run( + tenant_id=str(tenant.id), + scan_id=scan_id, + provider_id=missing_provider_id, + ) + + assert result is None + mock_scan.assert_not_called() + mock_complete_tasks.assert_not_called() + @pytest.mark.django_db class TestReaggregateAllFindingGroupSummaries: diff --git a/api/uv.lock b/api/uv.lock new file mode 100644 index 0000000000..57dd1a388d --- /dev/null +++ b/api/uv.lock @@ -0,0 +1,5992 @@ +version = 1 +revision = 3 +requires-python = ">=3.11, <3.13" +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform != 'win32'", + "python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version < '3.12' and sys_platform != 'win32'", +] + +[manifest] +constraints = [ + { name = "about-time", specifier = "==4.2.1" }, + { name = "adal", specifier = "==1.2.7" }, + { name = "aioboto3", specifier = "==15.5.0" }, + { name = "aiobotocore", specifier = "==2.25.1" }, + { name = "aiofiles", specifier = "==24.1.0" }, + { name = "aiohappyeyeballs", specifier = "==2.6.1" }, + { name = "aiohttp", specifier = "==3.13.5" }, + { name = "aioitertools", specifier = "==0.13.0" }, + { name = "aiosignal", specifier = "==1.4.0" }, + { name = "alibabacloud-actiontrail20200706", specifier = "==2.4.1" }, + { name = "alibabacloud-credentials", specifier = "==1.0.3" }, + { name = "alibabacloud-credentials-api", specifier = "==1.0.0" }, + { name = "alibabacloud-cs20151215", specifier = "==6.1.0" }, + { name = "alibabacloud-darabonba-array", specifier = "==0.1.0" }, + { name = "alibabacloud-darabonba-encode-util", specifier = "==0.0.2" }, + { name = "alibabacloud-darabonba-map", specifier = "==0.0.1" }, + { name = "alibabacloud-darabonba-signature-util", specifier = "==0.0.4" }, + { name = "alibabacloud-darabonba-string", specifier = "==0.0.4" }, + { name = "alibabacloud-darabonba-time", specifier = "==0.0.1" }, + { name = "alibabacloud-ecs20140526", specifier = "==7.2.5" }, + { name = "alibabacloud-endpoint-util", specifier = "==0.0.4" }, + { name = "alibabacloud-gateway-oss", specifier = "==0.0.17" }, + { name = "alibabacloud-gateway-oss-util", specifier = "==0.0.3" }, + { name = "alibabacloud-gateway-sls", specifier = "==0.4.0" }, + { name = "alibabacloud-gateway-sls-util", specifier = "==0.4.0" }, + { name = "alibabacloud-gateway-spi", specifier = "==0.0.3" }, + { name = "alibabacloud-openapi-util", specifier = "==0.2.4" }, + { name = "alibabacloud-oss-util", specifier = "==0.0.6" }, + { name = "alibabacloud-oss20190517", specifier = "==1.0.6" }, + { name = "alibabacloud-ram20150501", specifier = "==1.2.0" }, + { name = "alibabacloud-rds20140815", specifier = "==12.0.0" }, + { name = "alibabacloud-sas20181203", specifier = "==6.1.0" }, + { name = "alibabacloud-sls20201230", specifier = "==5.9.0" }, + { name = "alibabacloud-sts20150401", specifier = "==1.1.6" }, + { name = "alibabacloud-tea", specifier = "==0.4.3" }, + { name = "alibabacloud-tea-openapi", specifier = "==0.4.4" }, + { name = "alibabacloud-tea-util", specifier = "==0.3.14" }, + { name = "alibabacloud-tea-xml", specifier = "==0.0.3" }, + { name = "alibabacloud-vpc20160428", specifier = "==6.13.0" }, + { name = "alive-progress", specifier = "==3.3.0" }, + { name = "aliyun-log-fastpb", specifier = "==0.2.0" }, + { name = "amqp", specifier = "==5.3.1" }, + { name = "annotated-types", specifier = "==0.7.0" }, + { name = "anyio", specifier = "==4.12.1" }, + { name = "applicationinsights", specifier = "==0.11.10" }, + { name = "apscheduler", specifier = "==3.11.2" }, + { name = "argcomplete", specifier = "==3.5.3" }, + { name = "asgiref", specifier = "==3.11.0" }, + { name = "astroid", specifier = "==3.2.4" }, + { name = "async-timeout", specifier = "==5.0.1" }, + { name = "attrs", specifier = "==25.4.0" }, + { name = "authlib", specifier = "==1.6.9" }, + { name = "autopep8", specifier = "==2.3.2" }, + { name = "awsipranges", specifier = "==0.3.3" }, + { name = "azure-cli-core", specifier = "==2.83.0" }, + { name = "azure-cli-telemetry", specifier = "==1.1.0" }, + { name = "azure-common", specifier = "==1.1.28" }, + { name = "azure-core", specifier = "==1.38.1" }, + { name = "azure-identity", specifier = "==1.21.0" }, + { name = "azure-keyvault-certificates", specifier = "==4.10.0" }, + { name = "azure-keyvault-keys", specifier = "==4.10.0" }, + { name = "azure-keyvault-secrets", specifier = "==4.10.0" }, + { name = "azure-mgmt-apimanagement", specifier = "==5.0.0" }, + { name = "azure-mgmt-applicationinsights", specifier = "==4.1.0" }, + { name = "azure-mgmt-authorization", specifier = "==4.0.0" }, + { name = "azure-mgmt-compute", specifier = "==34.0.0" }, + { name = "azure-mgmt-containerinstance", specifier = "==10.1.0" }, + { name = "azure-mgmt-containerregistry", specifier = "==12.0.0" }, + { name = "azure-mgmt-containerservice", specifier = "==34.1.0" }, + { name = "azure-mgmt-core", specifier = "==1.6.0" }, + { name = "azure-mgmt-cosmosdb", specifier = "==9.7.0" }, + { name = "azure-mgmt-databricks", specifier = "==2.0.0" }, + { name = "azure-mgmt-datafactory", specifier = "==9.2.0" }, + { name = "azure-mgmt-eventgrid", specifier = "==10.4.0" }, + { name = "azure-mgmt-eventhub", specifier = "==11.2.0" }, + { name = "azure-mgmt-keyvault", specifier = "==10.3.1" }, + { name = "azure-mgmt-loganalytics", specifier = "==12.0.0" }, + { name = "azure-mgmt-logic", specifier = "==10.0.0" }, + { name = "azure-mgmt-monitor", specifier = "==6.0.2" }, + { name = "azure-mgmt-network", specifier = "==28.1.0" }, + { name = "azure-mgmt-postgresqlflexibleservers", specifier = "==1.1.0" }, + { name = "azure-mgmt-rdbms", specifier = "==10.1.0" }, + { name = "azure-mgmt-recoveryservices", specifier = "==3.1.0" }, + { name = "azure-mgmt-recoveryservicesbackup", specifier = "==9.2.0" }, + { name = "azure-mgmt-resource", specifier = "==24.0.0" }, + { name = "azure-mgmt-search", specifier = "==9.1.0" }, + { name = "azure-mgmt-security", specifier = "==7.0.0" }, + { name = "azure-mgmt-sql", specifier = "==3.0.1" }, + { name = "azure-mgmt-storage", specifier = "==22.1.1" }, + { name = "azure-mgmt-subscription", specifier = "==3.1.1" }, + { name = "azure-mgmt-synapse", specifier = "==2.0.0" }, + { name = "azure-mgmt-web", specifier = "==8.0.0" }, + { name = "azure-monitor-query", specifier = "==2.0.0" }, + { name = "azure-storage-blob", specifier = "==12.24.1" }, + { name = "azure-synapse-artifacts", specifier = "==0.21.0" }, + { name = "backoff", specifier = "==2.2.1" }, + { name = "bandit", specifier = "==1.7.9" }, + { name = "billiard", specifier = "==4.2.4" }, + { name = "blinker", specifier = "==1.9.0" }, + { name = "boto3", specifier = "==1.40.61" }, + { name = "botocore", specifier = "==1.40.61" }, + { name = "cartography", specifier = "==0.135.0" }, + { name = "celery", specifier = "==5.6.2" }, + { name = "certifi", specifier = "==2026.1.4" }, + { name = "cffi", specifier = "==2.0.0" }, + { name = "charset-normalizer", specifier = "==3.4.4" }, + { name = "circuitbreaker", specifier = "==2.1.3" }, + { name = "click", specifier = "==8.3.1" }, + { name = "click-didyoumean", specifier = "==0.3.1" }, + { name = "click-plugins", specifier = "==1.1.1.2" }, + { name = "click-repl", specifier = "==0.3.0" }, + { name = "cloudflare", specifier = "==4.3.1" }, + { name = "colorama", specifier = "==0.4.6" }, + { name = "contextlib2", specifier = "==21.6.0" }, + { name = "contourpy", specifier = "==1.3.3" }, + { name = "coverage", specifier = "==7.5.4" }, + { name = "cron-descriptor", specifier = "==1.4.5" }, + { name = "crowdstrike-falconpy", specifier = "==1.6.0" }, + { name = "cryptography", specifier = "==46.0.7" }, + { name = "cycler", specifier = "==0.12.1" }, + { name = "darabonba-core", specifier = "==1.0.5" }, + { name = "dash", specifier = "==3.1.1" }, + { name = "dash-bootstrap-components", specifier = "==2.0.3" }, + { name = "debugpy", specifier = "==1.8.20" }, + { name = "decorator", specifier = "==5.2.1" }, + { name = "defusedxml", specifier = "==0.7.1" }, + { name = "detect-secrets", specifier = "==1.5.0" }, + { name = "dill", specifier = "==0.4.1" }, + { name = "distro", specifier = "==1.9.0" }, + { name = "dj-rest-auth", specifier = "==7.0.1" }, + { name = "django", specifier = "==5.1.15" }, + { name = "django-allauth", specifier = "==65.15.0" }, + { name = "django-celery-beat", specifier = "==2.9.0" }, + { name = "django-celery-results", specifier = "==2.6.0" }, + { name = "django-cors-headers", specifier = "==4.4.0" }, + { name = "django-environ", specifier = "==0.11.2" }, + { name = "django-filter", specifier = "==24.3" }, + { name = "django-guid", specifier = "==3.5.0" }, + { name = "django-postgres-extra", specifier = "==2.0.9" }, + { name = "django-silk", specifier = "==5.3.2" }, + { name = "django-timezone-field", specifier = "==7.2.1" }, + { name = "djangorestframework", specifier = "==3.15.2" }, + { name = "djangorestframework-jsonapi", specifier = "==7.0.2" }, + { name = "djangorestframework-simplejwt", specifier = "==5.5.1" }, + { name = "dnspython", specifier = "==2.8.0" }, + { name = "docker", specifier = "==7.1.0" }, + { name = "dogpile-cache", specifier = "==1.5.0" }, + { name = "dparse", specifier = "==0.6.4" }, + { name = "drf-extensions", specifier = "==0.8.0" }, + { name = "drf-nested-routers", specifier = "==0.95.0" }, + { name = "drf-simple-apikey", specifier = "==2.2.1" }, + { name = "drf-spectacular", specifier = "==0.27.2" }, + { name = "drf-spectacular-jsonapi", specifier = "==0.5.1" }, + { name = "dulwich", specifier = "==0.23.0" }, + { name = "duo-client", specifier = "==5.5.0" }, + { name = "durationpy", specifier = "==0.10" }, + { name = "email-validator", specifier = "==2.2.0" }, + { name = "execnet", specifier = "==2.1.2" }, + { name = "filelock", specifier = "==3.20.3" }, + { name = "flask", specifier = "==3.1.3" }, + { name = "fonttools", specifier = "==4.62.1" }, + { name = "freezegun", specifier = "==1.5.1" }, + { name = "frozenlist", specifier = "==1.8.0" }, + { name = "gevent", specifier = "==25.9.1" }, + { name = "google-api-core", specifier = "==2.29.0" }, + { name = "google-api-python-client", specifier = "==2.163.0" }, + { name = "google-auth", specifier = "==2.48.0" }, + { name = "google-auth-httplib2", specifier = "==0.2.0" }, + { name = "google-cloud-access-context-manager", specifier = "==0.3.0" }, + { name = "google-cloud-asset", specifier = "==4.2.0" }, + { name = "google-cloud-org-policy", specifier = "==1.16.0" }, + { name = "google-cloud-os-config", specifier = "==1.23.0" }, + { name = "google-cloud-resource-manager", specifier = "==1.16.0" }, + { name = "googleapis-common-protos", specifier = "==1.72.0" }, + { name = "gprof2dot", specifier = "==2025.4.14" }, + { name = "graphemeu", specifier = "==0.7.2" }, + { name = "greenlet", specifier = "==3.3.1" }, + { name = "grpc-google-iam-v1", specifier = "==0.14.3" }, + { name = "grpcio", specifier = "==1.76.0" }, + { name = "grpcio-status", specifier = "==1.76.0" }, + { name = "gunicorn", specifier = "==23.0.0" }, + { name = "h11", specifier = "==0.16.0" }, + { name = "h2", specifier = "==4.3.0" }, + { name = "hpack", specifier = "==4.1.0" }, + { name = "httpcore", specifier = "==1.0.9" }, + { name = "httplib2", specifier = "==0.31.2" }, + { name = "httpx", specifier = "==0.28.1" }, + { name = "humanfriendly", specifier = "==10.0" }, + { name = "hyperframe", specifier = "==6.1.0" }, + { name = "iamdata", specifier = "==0.1.202602021" }, + { name = "idna", specifier = "==3.11" }, + { name = "importlib-metadata", specifier = "==8.7.1" }, + { name = "inflection", specifier = "==0.5.1" }, + { name = "iniconfig", specifier = "==2.3.0" }, + { name = "iso8601", specifier = "==2.1.0" }, + { name = "isodate", specifier = "==0.7.2" }, + { name = "isort", specifier = "==5.13.2" }, + { name = "itsdangerous", specifier = "==2.2.0" }, + { name = "jinja2", specifier = "==3.1.6" }, + { name = "jiter", specifier = "==0.13.0" }, + { name = "jmespath", specifier = "==1.1.0" }, + { name = "joblib", specifier = "==1.5.3" }, + { name = "jsonpatch", specifier = "==1.33" }, + { name = "jsonpickle", specifier = "==4.1.1" }, + { name = "jsonpointer", specifier = "==3.0.0" }, + { name = "jsonschema", specifier = "==4.23.0" }, + { name = "jsonschema-specifications", specifier = "==2025.9.1" }, + { name = "keystoneauth1", specifier = "==5.13.0" }, + { name = "kiwisolver", specifier = "==1.4.9" }, + { name = "knack", specifier = "==0.11.0" }, + { name = "kombu", specifier = "==5.6.2" }, + { name = "kubernetes", specifier = "==32.0.1" }, + { name = "lxml", specifier = "==6.1.0" }, + { name = "lz4", specifier = "==4.4.5" }, + { name = "markdown", specifier = "==3.10.2" }, + { name = "markdown-it-py", specifier = "==4.0.0" }, + { name = "markupsafe", specifier = "==3.0.3" }, + { name = "marshmallow", specifier = "==4.3.0" }, + { name = "matplotlib", specifier = "==3.10.8" }, + { name = "mccabe", specifier = "==0.7.0" }, + { name = "mdurl", specifier = "==0.1.2" }, + { name = "microsoft-kiota-abstractions", specifier = "==1.9.9" }, + { name = "microsoft-kiota-authentication-azure", specifier = "==1.9.9" }, + { name = "microsoft-kiota-http", specifier = "==1.9.9" }, + { name = "microsoft-kiota-serialization-form", specifier = "==1.9.9" }, + { name = "microsoft-kiota-serialization-json", specifier = "==1.9.9" }, + { name = "microsoft-kiota-serialization-multipart", specifier = "==1.9.9" }, + { name = "microsoft-kiota-serialization-text", specifier = "==1.9.9" }, + { name = "microsoft-security-utilities-secret-masker", specifier = "==1.0.0b4" }, + { name = "msal", specifier = "==1.35.0b1" }, + { name = "msal-extensions", specifier = "==1.2.0" }, + { name = "msgraph-core", specifier = "==1.3.8" }, + { name = "msgraph-sdk", specifier = "==1.55.0" }, + { name = "msrest", specifier = "==0.7.1" }, + { name = "msrestazure", specifier = "==0.6.4.post1" }, + { name = "multidict", specifier = "==6.7.1" }, + { name = "mypy", specifier = "==1.10.1" }, + { name = "mypy-extensions", specifier = "==1.1.0" }, + { name = "narwhals", specifier = "==2.16.0" }, + { name = "neo4j", specifier = "==6.1.0" }, + { name = "nest-asyncio", specifier = "==1.6.0" }, + { name = "nltk", specifier = "==3.9.4" }, + { name = "numpy", specifier = "==2.0.2" }, + { name = "oauthlib", specifier = "==3.3.1" }, + { name = "oci", specifier = "==2.169.0" }, + { name = "openai", specifier = "==1.109.1" }, + { name = "openstacksdk", specifier = "==4.2.0" }, + { name = "opentelemetry-api", specifier = "==1.39.1" }, + { name = "opentelemetry-sdk", specifier = "==1.39.1" }, + { name = "opentelemetry-semantic-conventions", specifier = "==0.60b1" }, + { name = "os-service-types", specifier = "==1.8.2" }, + { name = "packageurl-python", specifier = "==0.17.6" }, + { name = "packaging", specifier = "==26.0" }, + { name = "pagerduty", specifier = "==6.1.0" }, + { name = "pandas", specifier = "==2.2.3" }, + { name = "pbr", specifier = "==7.0.3" }, + { name = "pillow", specifier = "==12.2.0" }, + { name = "pkginfo", specifier = "==1.12.1.2" }, + { name = "platformdirs", specifier = "==4.5.1" }, + { name = "plotly", specifier = "==6.5.2" }, + { name = "pluggy", specifier = "==1.6.0" }, + { name = "policyuniverse", specifier = "==1.5.1.20231109" }, + { name = "portalocker", specifier = "==2.10.1" }, + { name = "prek", specifier = "==0.3.9" }, + { name = "prompt-toolkit", specifier = "==3.0.52" }, + { name = "propcache", specifier = "==0.4.1" }, + { name = "proto-plus", specifier = "==1.27.0" }, + { name = "protobuf", specifier = "==6.33.5" }, + { name = "psutil", specifier = "==7.2.2" }, + { name = "psycopg2-binary", specifier = "==2.9.9" }, + { name = "py-deviceid", specifier = "==0.1.1" }, + { name = "py-iam-expand", specifier = "==0.1.0" }, + { name = "py-ocsf-models", specifier = "==0.8.1" }, + { name = "pyasn1", specifier = "==0.6.3" }, + { name = "pyasn1-modules", specifier = "==0.4.2" }, + { name = "pycodestyle", specifier = "==2.14.0" }, + { name = "pycparser", specifier = "==3.0" }, + { name = "pydantic", specifier = "==2.12.5" }, + { name = "pydantic-core", specifier = "==2.41.5" }, + { name = "pygithub", specifier = "==2.8.0" }, + { name = "pygments", specifier = "==2.20.0" }, + { name = "pyjwt", specifier = "==2.12.1" }, + { name = "pylint", specifier = "==3.2.5" }, + { name = "pymsalruntime", specifier = "==0.18.1" }, + { name = "pynacl", specifier = "==1.6.2" }, + { name = "pyopenssl", specifier = "==26.0.0" }, + { name = "pyparsing", specifier = "==3.3.2" }, + { name = "pyreadline3", specifier = "==3.5.4" }, + { name = "pysocks", specifier = "==1.7.1" }, + { name = "pytest", specifier = "==9.0.3" }, + { name = "pytest-celery", specifier = "==1.3.0" }, + { name = "pytest-cov", specifier = "==5.0.0" }, + { name = "pytest-django", specifier = "==4.8.0" }, + { name = "pytest-docker-tools", specifier = "==3.1.9" }, + { name = "pytest-env", specifier = "==1.1.3" }, + { name = "pytest-randomly", specifier = "==3.15.0" }, + { name = "pytest-xdist", specifier = "==3.6.1" }, + { name = "python-crontab", specifier = "==3.3.0" }, + { name = "python-dateutil", specifier = "==2.9.0.post0" }, + { name = "python-digitalocean", specifier = "==1.17.0" }, + { name = "python3-saml", specifier = "==1.16.0" }, + { name = "pytz", specifier = "==2025.1" }, + { name = "pywin32", specifier = "==311" }, + { name = "pyyaml", specifier = "==6.0.3" }, + { name = "redis", specifier = "==7.1.0" }, + { name = "referencing", specifier = "==0.37.0" }, + { name = "regex", specifier = "==2026.1.15" }, + { name = "reportlab", specifier = "==4.4.10" }, + { name = "requests", specifier = "==2.33.1" }, + { name = "requests-file", specifier = "==3.0.1" }, + { name = "requests-oauthlib", specifier = "==2.0.0" }, + { name = "requestsexceptions", specifier = "==1.4.0" }, + { name = "retrying", specifier = "==1.4.2" }, + { name = "rich", specifier = "==14.3.2" }, + { name = "rpds-py", specifier = "==0.30.0" }, + { name = "rsa", specifier = "==4.9.1" }, + { name = "ruamel-yaml", specifier = "==0.19.1" }, + { name = "ruff", specifier = "==0.5.0" }, + { name = "s3transfer", specifier = "==0.14.0" }, + { name = "scaleway", specifier = "==2.10.3" }, + { name = "scaleway-core", specifier = "==2.10.3" }, + { name = "schema", specifier = "==0.7.5" }, + { name = "sentry-sdk", specifier = "==2.56.0" }, + { name = "setuptools", specifier = "==80.10.2" }, + { name = "shellingham", specifier = "==1.5.4" }, + { name = "shodan", specifier = "==1.31.0" }, + { name = "six", specifier = "==1.17.0" }, + { name = "slack-sdk", specifier = "==3.39.0" }, + { name = "sniffio", specifier = "==1.3.1" }, + { name = "sqlparse", specifier = "==0.5.5" }, + { name = "statsd", specifier = "==4.0.1" }, + { name = "std-uritemplate", specifier = "==2.0.8" }, + { name = "stevedore", specifier = "==5.6.0" }, + { name = "tabulate", specifier = "==0.9.0" }, + { name = "tenacity", specifier = "==9.1.2" }, + { name = "tldextract", specifier = "==5.3.1" }, + { name = "tomlkit", specifier = "==0.14.0" }, + { name = "tqdm", specifier = "==4.67.1" }, + { name = "typer", specifier = "==0.21.1" }, + { name = "types-aiobotocore-ecr", specifier = "==3.1.1" }, + { name = "typing-extensions", specifier = "==4.15.0" }, + { name = "typing-inspection", specifier = "==0.4.2" }, + { name = "tzdata", specifier = "==2025.3" }, + { name = "tzlocal", specifier = "==5.3.1" }, + { name = "uritemplate", specifier = "==4.2.0" }, + { name = "urllib3", specifier = "==2.7.0" }, + { name = "uuid6", specifier = "==2024.7.10" }, + { name = "vine", specifier = "==5.1.0" }, + { name = "vulture", specifier = "==2.14" }, + { name = "wcwidth", specifier = "==0.5.3" }, + { name = "websocket-client", specifier = "==1.9.0" }, + { name = "werkzeug", specifier = "==3.1.7" }, + { name = "workos", specifier = "==6.0.4" }, + { name = "wrapt", specifier = "==1.17.3" }, + { name = "xlsxwriter", specifier = "==3.2.9" }, + { name = "xmlsec", specifier = "==1.3.17" }, + { name = "xmltodict", specifier = "==1.0.2" }, + { name = "yarl", specifier = "==1.22.0" }, + { name = "zipp", specifier = "==3.23.0" }, + { name = "zope-event", specifier = "==6.1" }, + { name = "zope-interface", specifier = "==8.2" }, + { name = "zstd", specifier = "==1.5.7.3" }, +] +overrides = [ + { name = "microsoft-kiota-abstractions", specifier = "==1.9.9" }, + { name = "okta", specifier = "==3.4.2" }, +] + +[[package]] +name = "about-time" +version = "4.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/3f/ccb16bdc53ebb81c1bf837c1ee4b5b0b69584fd2e4a802a2a79936691c0a/about-time-4.2.1.tar.gz", hash = "sha256:6a538862d33ce67d997429d14998310e1dbfda6cb7d9bbfbf799c4709847fece", size = 15380, upload-time = "2022-12-21T04:15:54.991Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/cd/7ee00d6aa023b1d0551da0da5fee3bc23c3eeea632fbfc5126d1fec52b7e/about_time-4.2.1-py3-none-any.whl", hash = "sha256:8bbf4c75fe13cbd3d72f49a03b02c5c7dca32169b6d49117c257e7eb3eaee341", size = 13295, upload-time = "2022-12-21T04:15:53.613Z" }, +] + +[[package]] +name = "adal" +version = "1.2.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyjwt" }, + { name = "python-dateutil" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/d7/a829bc5e8ff28f82f9e2dc9b363f3b7b9c1194766d5a75105e3885bfa9a8/adal-1.2.7.tar.gz", hash = "sha256:d74f45b81317454d96e982fd1c50e6fb5c99ac2223728aea8764433a39f566f1", size = 35196, upload-time = "2021-04-05T16:33:40.88Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/8d/58008a9a86075827f99aa8bb75d8db515bb9c34654f95e647cda31987db7/adal-1.2.7-py2.py3-none-any.whl", hash = "sha256:2a7451ed7441ddbc57703042204a3e30ef747478eea022c70f789fc7f084bc3d", size = 55539, upload-time = "2021-04-05T16:33:39.544Z" }, +] + +[[package]] +name = "aenum" +version = "3.1.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/07/e9/8b283567c1fef7c24d1f390b37daede8b61593d8cdaffb8e95d571699e83/aenum-3.1.17.tar.gz", hash = "sha256:a969a4516b194895de72c875ece355f17c0d272146f7fda346ef74f93cf4d5ba", size = 137648, upload-time = "2026-03-20T20:43:29.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/8d/1fe30c6fd8999b9d462547c4a1bb6690bda24af38f2913c4bec7decb81f2/aenum-3.1.17-py3-none-any.whl", hash = "sha256:8b883a37a04e74cc838ac442bdd28c266eae5bbf13e1342c7ef123ed25230139", size = 165560, upload-time = "2026-03-20T20:43:27.681Z" }, +] + +[[package]] +name = "aioboto3" +version = "15.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiobotocore", extra = ["boto3"] }, + { name = "aiofiles" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/01/92e9ab00f36e2899315f49eefcd5b4685fbb19016c7f19a9edf06da80bb0/aioboto3-15.5.0.tar.gz", hash = "sha256:ea8d8787d315594842fbfcf2c4dce3bac2ad61be275bc8584b2ce9a3402a6979", size = 255069, upload-time = "2025-10-30T13:37:16.122Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/3e/e8f5b665bca646d43b916763c901e00a07e40f7746c9128bdc912a089424/aioboto3-15.5.0-py3-none-any.whl", hash = "sha256:cc880c4d6a8481dd7e05da89f41c384dbd841454fc1998ae25ca9c39201437a6", size = 35913, upload-time = "2025-10-30T13:37:14.549Z" }, +] + +[[package]] +name = "aiobotocore" +version = "2.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "aioitertools" }, + { name = "botocore" }, + { name = "jmespath" }, + { name = "multidict" }, + { name = "python-dateutil" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/94/2e4ec48cf1abb89971cb2612d86f979a6240520f0a659b53a43116d344dc/aiobotocore-2.25.1.tar.gz", hash = "sha256:ea9be739bfd7ece8864f072ec99bb9ed5c7e78ebb2b0b15f29781fbe02daedbc", size = 120560, upload-time = "2025-10-28T22:33:21.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/2a/d275ec4ce5cd0096665043995a7d76f5d0524853c76a3d04656de49f8808/aiobotocore-2.25.1-py3-none-any.whl", hash = "sha256:eb6daebe3cbef5b39a0bb2a97cffbe9c7cb46b2fcc399ad141f369f3c2134b1f", size = 86039, upload-time = "2025-10-28T22:33:19.949Z" }, +] + +[package.optional-dependencies] +boto3 = [ + { name = "boto3" }, +] + +[[package]] +name = "aiofiles" +version = "24.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247, upload-time = "2024-06-24T11:02:03.584Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896, upload-time = "2024-06-24T11:02:01.529Z" }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/f5/a20c4ac64aeaef1679e25c9983573618ff765d7aa829fa2b84ae7573169e/aiohttp-3.13.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ab7229b6f9b5c1ba4910d6c41a9eb11f543eadb3f384df1b4c293f4e73d44d6", size = 757513, upload-time = "2026-03-31T21:57:02.146Z" }, + { url = "https://files.pythonhosted.org/packages/75/0a/39fa6c6b179b53fcb3e4b3d2b6d6cad0180854eda17060c7218540102bef/aiohttp-3.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f14c50708bb156b3a3ca7230b3d820199d56a48e3af76fa21c2d6087190fe3d", size = 506748, upload-time = "2026-03-31T21:57:04.275Z" }, + { url = "https://files.pythonhosted.org/packages/87/ec/e38ce072e724fd7add6243613f8d1810da084f54175353d25ccf9f9c7e5a/aiohttp-3.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7d2f8616f0ff60bd332022279011776c3ac0faa0f1b463f7bb12326fbc97a1c", size = 501673, upload-time = "2026-03-31T21:57:06.208Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ba/3bc7525d7e2beaa11b309a70d48b0d3cfc3c2089ec6a7d0820d59c657053/aiohttp-3.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2567b72e1ffc3ab25510db43f355b29eeada56c0a622e58dcdb19530eb0a3cb", size = 1763757, upload-time = "2026-03-31T21:57:07.882Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ab/e87744cf18f1bd78263aba24924d4953b41086bd3a31d22452378e9028a0/aiohttp-3.13.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fb0540c854ac9c0c5ad495908fdfd3e332d553ec731698c0e29b1877ba0d2ec6", size = 1720152, upload-time = "2026-03-31T21:57:09.946Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f3/ed17a6f2d742af17b50bae2d152315ed1b164b07a5fd5cc1754d99e4dfa5/aiohttp-3.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9883051c6972f58bfc4ebb2116345ee2aa151178e99c3f2b2bbe2af712abd13", size = 1818010, upload-time = "2026-03-31T21:57:12.157Z" }, + { url = "https://files.pythonhosted.org/packages/53/06/ecbc63dc937192e2a5cb46df4d3edb21deb8225535818802f210a6ea5816/aiohttp-3.13.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2294172ce08a82fb7c7273485895de1fa1186cc8294cfeb6aef4af42ad261174", size = 1907251, upload-time = "2026-03-31T21:57:14.023Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a5/0521aa32c1ddf3aa1e71dcc466be0b7db2771907a13f18cddaa45967d97b/aiohttp-3.13.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a807cabd5115fb55af198b98178997a5e0e57dead43eb74a93d9c07d6d4a7dc", size = 1759969, upload-time = "2026-03-31T21:57:16.146Z" }, + { url = "https://files.pythonhosted.org/packages/f6/78/a38f8c9105199dd3b9706745865a8a59d0041b6be0ca0cc4b2ccf1bab374/aiohttp-3.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6d0d932e0f39c02b80744273cd5c388a2d9bc07760a03164f229c8e02662f6", size = 1616871, upload-time = "2026-03-31T21:57:17.856Z" }, + { url = "https://files.pythonhosted.org/packages/6f/41/27392a61ead8ab38072105c71aa44ff891e71653fe53d576a7067da2b4e8/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60869c7ac4aaabe7110f26499f3e6e5696eae98144735b12a9c3d9eae2b51a49", size = 1739844, upload-time = "2026-03-31T21:57:19.679Z" }, + { url = "https://files.pythonhosted.org/packages/6e/55/5564e7ae26d94f3214250009a0b1c65a0c6af4bf88924ccb6fdab901de28/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:26d2f8546f1dfa75efa50c3488215a903c0168d253b75fba4210f57ab77a0fb8", size = 1731969, upload-time = "2026-03-31T21:57:22.006Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c5/705a3929149865fc941bcbdd1047b238e4a72bcb215a9b16b9d7a2e8d992/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1162a1492032c82f14271e831c8f4b49f2b6078f4f5fc74de2c912fa225d51d", size = 1795193, upload-time = "2026-03-31T21:57:24.256Z" }, + { url = "https://files.pythonhosted.org/packages/a6/19/edabed62f718d02cff7231ca0db4ef1c72504235bc467f7b67adb1679f48/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8b14eb3262fad0dc2f89c1a43b13727e709504972186ff6a99a3ecaa77102b6c", size = 1606477, upload-time = "2026-03-31T21:57:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/de/fc/76f80ef008675637d88d0b21584596dc27410a990b0918cb1e5776545b5b/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ca9ac61ac6db4eb6c2a0cd1d0f7e1357647b638ccc92f7e9d8d133e71ed3c6ac", size = 1813198, upload-time = "2026-03-31T21:57:28.316Z" }, + { url = "https://files.pythonhosted.org/packages/e5/67/5b3ac26b80adb20ea541c487f73730dc8fa107d632c998f25bbbab98fcda/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7996023b2ed59489ae4762256c8516df9820f751cf2c5da8ed2fb20ee50abab3", size = 1752321, upload-time = "2026-03-31T21:57:30.549Z" }, + { url = "https://files.pythonhosted.org/packages/88/06/e4a2e49255ea23fa4feeb5ab092d90240d927c15e47b5b5c48dff5a9ce29/aiohttp-3.13.5-cp311-cp311-win32.whl", hash = "sha256:77dfa48c9f8013271011e51c00f8ada19851f013cde2c48fca1ba5e0caf5bb06", size = 439069, upload-time = "2026-03-31T21:57:32.388Z" }, + { url = "https://files.pythonhosted.org/packages/c0/43/8c7163a596dab4f8be12c190cf467a1e07e4734cf90eebb39f7f5d53fc6a/aiohttp-3.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:d3a4834f221061624b8887090637db9ad4f61752001eae37d56c52fddade2dc8", size = 462859, upload-time = "2026-03-31T21:57:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" }, + { url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" }, + { url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" }, + { url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" }, + { url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" }, + { url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" }, + { url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" }, + { url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" }, + { url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" }, + { url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" }, + { url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" }, + { url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" }, + { url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" }, + { url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" }, + { url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" }, +] + +[[package]] +name = "aioitertools" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/3c/53c4a17a05fb9ea2313ee1777ff53f5e001aefd5cc85aa2f4c2d982e1e38/aioitertools-0.13.0.tar.gz", hash = "sha256:620bd241acc0bbb9ec819f1ab215866871b4bbd1f73836a55f799200ee86950c", size = 19322, upload-time = "2025-11-06T22:17:07.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/a1/510b0a7fadc6f43a6ce50152e69dbd86415240835868bb0bd9b5b88b1e06/aioitertools-0.13.0-py3-none-any.whl", hash = "sha256:0be0292b856f08dfac90e31f4739432f4cb6d7520ab9eb73e143f4f2fa5259be", size = 24182, upload-time = "2025-11-06T22:17:06.502Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "alibabacloud-actiontrail20200706" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alibabacloud-endpoint-util" }, + { name = "alibabacloud-openapi-util" }, + { name = "alibabacloud-tea-openapi" }, + { name = "alibabacloud-tea-util" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/97/ce0fb7ee2507fa9e6d41c8bc5bbc224d15af6c3c47047af6c1254b17b969/alibabacloud_actiontrail20200706-2.4.1.tar.gz", hash = "sha256:b65c6b37a96443fbe625dd5a4dd1be52a7476006a411db75206908b11588ffa8", size = 30016, upload-time = "2025-08-18T02:11:13.404Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/f8/c4b886644f5f3d69c6cc9cb8fabe286d9c3d1ec8ab7796205e2f6e038437/alibabacloud_actiontrail20200706-2.4.1-py3-none-any.whl", hash = "sha256:5dee0009db9b7cba182fbac742820f6a949287a8faafb843b5107f7dc89136da", size = 28861, upload-time = "2025-08-18T02:11:12.49Z" }, +] + +[[package]] +name = "alibabacloud-credentials" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiofiles" }, + { name = "alibabacloud-credentials-api" }, + { name = "alibabacloud-tea" }, + { name = "apscheduler" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/82/45ec98bd19387507cf058ce47f62d6fea288bf0511c5a101b832e13d3edd/alibabacloud-credentials-1.0.3.tar.gz", hash = "sha256:9d8707e96afc6f348e23f5677ed15a21c2dfce7cfe6669776548ee4c80e1dfaf", size = 35831, upload-time = "2025-10-14T06:39:58.97Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/df/dbd9ae9d531a40d5613573c5a22ef774ecfdcaa0dc43aad42189f89c04ce/alibabacloud_credentials-1.0.3-py3-none-any.whl", hash = "sha256:30c8302f204b663c655d97e1c283ee9f9f84a6257d7901b931477d6cf34445a8", size = 41875, upload-time = "2025-10-14T06:39:58.029Z" }, +] + +[[package]] +name = "alibabacloud-credentials-api" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a0/87/1d7019d23891897cb076b2f7e3c81ab3c2ba91de3bb067196f675d60d34c/alibabacloud-credentials-api-1.0.0.tar.gz", hash = "sha256:8c340038d904f0218d7214a8f4088c31912bfcf279af2cbc7d9be4897a97dd2f", size = 2330, upload-time = "2025-01-13T05:53:04.931Z" } + +[[package]] +name = "alibabacloud-cs20151215" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alibabacloud-endpoint-util" }, + { name = "alibabacloud-openapi-util" }, + { name = "alibabacloud-tea-openapi" }, + { name = "alibabacloud-tea-util" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/51/6a6aa2c0c1e392e137fbde77c1d5f6bf16e665b3d5e6e3ab747664e67cdf/alibabacloud_cs20151215-6.1.0.tar.gz", hash = "sha256:5b3d99306701bf499ddd57cd9f2905b7721cb1bb4bb38ffe4d051f7b4e80e355", size = 191423, upload-time = "2025-11-05T11:49:31.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/51/00c07f61c64deb1c1a780c8fe5e34cf57950ea47a545647dc7c00314be93/alibabacloud_cs20151215-6.1.0-py3-none-any.whl", hash = "sha256:75e90b1bb9acca2236244bb0e44234ca4805d456ea4303ba4225ac15152a458e", size = 189706, upload-time = "2025-11-05T11:49:29.848Z" }, +] + +[[package]] +name = "alibabacloud-darabonba-array" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/50/be/1813d7553e11e20a1422ffaaead392cfa7239a855c7e67c6a6b5776cfa64/alibabacloud_darabonba_array-0.1.0.tar.gz", hash = "sha256:7f9a7c632518ff4f0cebb0d4e825a48c12e7cf0b9016ea25054dd73732e155aa", size = 2306, upload-time = "2022-11-01T06:32:47.928Z" } + +[[package]] +name = "alibabacloud-darabonba-encode-util" +version = "0.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/d8/22543b2ade9aa68fef028a9f0c4154bfdb970926f918f63d7b85bae527a9/alibabacloud_darabonba_encode_util-0.0.2.tar.gz", hash = "sha256:f1c484f276d60450fa49b4b2987194e741fcb2f7faae7f287c0ae65abc85fd4d", size = 3990, upload-time = "2022-12-10T04:43:48.086Z" } + +[[package]] +name = "alibabacloud-darabonba-map" +version = "0.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/bc/f11d56adffffade9a0d33ccca155ce82ca950b97cdce27a75228715c4639/alibabacloud_darabonba_map-0.0.1.tar.gz", hash = "sha256:adb17384658a1a8f72418f1838d4b6a5fd2566bfd392a3ef06d9dbb0a595a23f", size = 2056, upload-time = "2021-12-04T03:41:20.369Z" } + +[[package]] +name = "alibabacloud-darabonba-signature-util" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/09/2118a2df631eaa06a291013ea61f31e449ba7a3cc3d6061477a43420e93a/alibabacloud_darabonba_signature_util-0.0.4.tar.gz", hash = "sha256:71d79b2ae65957bcfbf699ced894fda782b32f9635f1616635533e5a90d5feb0", size = 4170, upload-time = "2022-12-10T04:44:42.979Z" } + +[[package]] +name = "alibabacloud-darabonba-string" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/d4/3d22bd2ff88985f970a10f8cedf2ea326d11d4d95e244f7665dc83d26465/alibabacloud-darabonba-string-0.0.4.tar.gz", hash = "sha256:ec6614c0448dadcbc5e466485838a1f8cfdd911135bea739e20b14511270c6f7", size = 2852, upload-time = "2021-12-13T13:30:06.114Z" } + +[[package]] +name = "alibabacloud-darabonba-time" +version = "0.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/df/33a239384353130eef51af14cec1fa99058b2c5f4c7f3d0b283e5f17b874/alibabacloud_darabonba_time-0.0.1.tar.gz", hash = "sha256:0ad9c7b0696570d1a3f40106cc7777f755fd92baa0d1dcab5b7df78dde5b922d", size = 2885, upload-time = "2021-01-05T02:58:06.121Z" } + +[[package]] +name = "alibabacloud-ecs20140526" +version = "7.2.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alibabacloud-endpoint-util" }, + { name = "alibabacloud-openapi-util" }, + { name = "alibabacloud-tea-openapi" }, + { name = "alibabacloud-tea-util" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c8/f1/721627d499eb8ed5f8ad3ba0dbf57a0da10ff01deaf64f3536f70edbab8c/alibabacloud_ecs20140526-7.2.5.tar.gz", hash = "sha256:2abbe630ce42d69061821f38950b938c5982cc31902ccd7132d05be328765a55", size = 651634, upload-time = "2025-10-29T17:38:48.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/8e/e3709447c737d7deb3cd34dc97f6ea2b3b57e0c8c3973ab9018c113ca68c/alibabacloud_ecs20140526-7.2.5-py3-none-any.whl", hash = "sha256:10bda5e185f6ba899e7d51477373595c629d66db7530a8a37433fb4e9034a96f", size = 659546, upload-time = "2025-10-29T17:38:35.768Z" }, +] + +[[package]] +name = "alibabacloud-endpoint-util" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/7d/8cc92a95c920e344835b005af6ea45a0db98763ad6ad19299d26892e6c8d/alibabacloud_endpoint_util-0.0.4.tar.gz", hash = "sha256:a593eb8ddd8168d5dc2216cd33111b144f9189fcd6e9ca20e48f358a739bbf90", size = 2813, upload-time = "2025-06-12T07:20:52.572Z" } + +[[package]] +name = "alibabacloud-gateway-oss" +version = "0.0.17" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alibabacloud-credentials" }, + { name = "alibabacloud-darabonba-array" }, + { name = "alibabacloud-darabonba-encode-util" }, + { name = "alibabacloud-darabonba-map" }, + { name = "alibabacloud-darabonba-signature-util" }, + { name = "alibabacloud-darabonba-string" }, + { name = "alibabacloud-darabonba-time" }, + { name = "alibabacloud-gateway-oss-util" }, + { name = "alibabacloud-gateway-spi" }, + { name = "alibabacloud-openapi-util" }, + { name = "alibabacloud-oss-util" }, + { name = "alibabacloud-tea-util" }, + { name = "alibabacloud-tea-xml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/c3/4172567b96d885117d93b5ac7b849dda7f0421af7410e074f3ccf3469357/alibabacloud_gateway_oss-0.0.17.tar.gz", hash = "sha256:8c4b66c8c7dd285fc210ee232ab3f062b5573258752804d19382000746531e29", size = 8030, upload-time = "2025-04-07T03:16:28.346Z" } + +[[package]] +name = "alibabacloud-gateway-oss-util" +version = "0.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/1e/caf81e07d285dbef4dc62e63763448cc84a9b483a69b1a1a576efbbe8795/alibabacloud_gateway_oss_util-0.0.3.tar.gz", hash = "sha256:5eb7fa450dc7350d5c71577974b9d7f489479e5c5ec7efc1c5376385e8c1c0a5", size = 105567, upload-time = "2025-01-03T07:59:41.501Z" } + +[[package]] +name = "alibabacloud-gateway-sls" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alibabacloud-credentials" }, + { name = "alibabacloud-darabonba-array" }, + { name = "alibabacloud-darabonba-encode-util" }, + { name = "alibabacloud-darabonba-map" }, + { name = "alibabacloud-darabonba-signature-util" }, + { name = "alibabacloud-darabonba-string" }, + { name = "alibabacloud-gateway-sls-util" }, + { name = "alibabacloud-gateway-spi" }, + { name = "alibabacloud-openapi-util" }, + { name = "alibabacloud-tea-util" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/ca/38effb3f7e81f1abc78f0c283ba2d1a19cb9fbdc3a0ad7b8f6300a0a413c/alibabacloud_gateway_sls-0.4.0.tar.gz", hash = "sha256:9d2aceb377c9b3ed0558149fda16fe39fa114cc0a22e22a88dc76efdda34633b", size = 7660, upload-time = "2025-11-18T11:51:08.26Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/ec/f2c170b70ff5e375bdf869bf1b976b0e027640742887817dc62a58bb1d97/alibabacloud_gateway_sls-0.4.0-py3-none-any.whl", hash = "sha256:a0299a83a5528025983b42b7533a28028461bced5e180a66f97999e0134760a6", size = 6905, upload-time = "2025-11-18T11:51:07.357Z" }, +] + +[[package]] +name = "alibabacloud-gateway-sls-util" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aliyun-log-fastpb" }, + { name = "lz4" }, + { name = "zstd" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/ef/590fee606b0ce16e28a0ece8c81338718b87c47d6bb1553f383ec493fafa/alibabacloud_gateway_sls_util-0.4.0.tar.gz", hash = "sha256:f8b683a36a2ae3fe9a8225d3d97773ea769bdf9cdf4f4d033eab2eb6062ddd1f", size = 3400, upload-time = "2025-11-18T11:07:12.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/4f/fd5ce522c7cec21e500a8e1c161b16f2762cdc343c370c05249af545239f/alibabacloud_gateway_sls_util-0.4.0-py3-none-any.whl", hash = "sha256:c91ab7fe55af526a01d25b0d431088c4d241b160db055da3d8cb7330bd74595a", size = 3499, upload-time = "2025-11-18T11:07:11.813Z" }, +] + +[[package]] +name = "alibabacloud-gateway-spi" +version = "0.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alibabacloud-credentials" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/98/d7111245f17935bf72ee9bea60bbbeff2bc42cdfe24d2544db52bc517e1a/alibabacloud_gateway_spi-0.0.3.tar.gz", hash = "sha256:10d1c53a3fc5f87915fbd6b4985b98338a776e9b44a0263f56643c5048223b8b", size = 4249, upload-time = "2025-02-23T16:29:54.222Z" } + +[[package]] +name = "alibabacloud-openapi-util" +version = "0.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alibabacloud-tea-util" }, + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/51/be5802851a4ed20ac2c6db50ac8354a6e431e93db6e714ca39b50983626f/alibabacloud_openapi_util-0.2.4.tar.gz", hash = "sha256:87022b9dcb7593a601f7a40ca698227ac3ccb776b58cb7b06b8dc7f510995c34", size = 7981, upload-time = "2026-01-15T08:05:03.947Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/46/9b217343648b366eb93447f5d93116e09a61956005794aed5ef95a2e9e2e/alibabacloud_openapi_util-0.2.4-py3-none-any.whl", hash = "sha256:a2474f230b5965ae9a8c286e0dc86132a887928d02d20b8182656cf6b1b6c5bd", size = 7661, upload-time = "2026-01-15T08:05:01.374Z" }, +] + +[[package]] +name = "alibabacloud-oss-util" +version = "0.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alibabacloud-tea" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/7c/d7e812b9968247a302573daebcfef95d0f9a718f7b4bfcca8d3d83e266be/alibabacloud_oss_util-0.0.6.tar.gz", hash = "sha256:d3ecec36632434bd509a113e8cf327dc23e830ac8d9dd6949926f4e334c8b5d6", size = 10008, upload-time = "2021-04-28T09:25:04.056Z" } + +[[package]] +name = "alibabacloud-oss20190517" +version = "1.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alibabacloud-gateway-oss" }, + { name = "alibabacloud-gateway-spi" }, + { name = "alibabacloud-openapi-util" }, + { name = "alibabacloud-tea-openapi" }, + { name = "alibabacloud-tea-util" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/d7/2f9ad0b13bd196b0f45bdb4a0d1ecc14d1723ad1685e37d975cfbb566919/alibabacloud_oss20190517-1.0.6.tar.gz", hash = "sha256:7cd0fb16af613ceb38d2e0e529aa1f58038c7cf59eb67c8c8775ae44ea717852", size = 51772, upload-time = "2023-07-27T06:41:50.418Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/ca/bf1cb50f736684711fefeb10dfb95152fe9c1c0d30e80356fd45bfd77e9e/alibabacloud_oss20190517-1.0.6-py3-none-any.whl", hash = "sha256:365fda353de6658a1a289f4d70dcd0394e2a8e2921b6b5834ba6d9772121d2f6", size = 50948, upload-time = "2023-07-27T06:41:48.645Z" }, +] + +[[package]] +name = "alibabacloud-ram20150501" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alibabacloud-endpoint-util" }, + { name = "alibabacloud-openapi-util" }, + { name = "alibabacloud-tea-openapi" }, + { name = "alibabacloud-tea-util" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fd/5b18adb76bbe55fc6da2913f22f4862f296eea8ed84c605f106aaff50daa/alibabacloud_ram20150501-1.2.0.tar.gz", hash = "sha256:6253513c8880769f4fd5b36fedddb362a9ca628ad9ae9c05c0eeacf5fbc95b42", size = 44340, upload-time = "2025-06-12T07:59:39.81Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/ca/50b3d365a2d73841cead4f1ca777f883d237d7191c51f4010247ec2b69e0/alibabacloud_ram20150501-1.2.0-py3-none-any.whl", hash = "sha256:03a0f2a0259848787c1f74e802b486184a88e04183486bd9398766971e5eb00a", size = 43138, upload-time = "2025-06-12T07:59:38.72Z" }, +] + +[[package]] +name = "alibabacloud-rds20140815" +version = "12.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alibabacloud-endpoint-util" }, + { name = "alibabacloud-openapi-util" }, + { name = "alibabacloud-tea-openapi" }, + { name = "alibabacloud-tea-util" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/6d/167b4e285f74f2426ea137388220f484e41263bbc21f42c8280cfa95cb0d/alibabacloud_rds20140815-12.0.0.tar.gz", hash = "sha256:e7421d94f18a914c0a06b0e7fad0daff557713f1c97d415d463a78c1270e9b98", size = 466530, upload-time = "2025-07-04T10:12:23.26Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/f9/5dd141ebf043d10b83a0913fd9e689733b5e81b0074bf12333f3797a1016/alibabacloud_rds20140815-12.0.0-py3-none-any.whl", hash = "sha256:0bd7e2018a428d86b1b0681087336e74665b48fc3eb0a13c4f4377ed5eab2b08", size = 472145, upload-time = "2025-07-04T10:12:21.875Z" }, +] + +[[package]] +name = "alibabacloud-sas20181203" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alibabacloud-endpoint-util" }, + { name = "alibabacloud-openapi-util" }, + { name = "alibabacloud-tea-openapi" }, + { name = "alibabacloud-tea-util" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/c4/7cdd75a2f7afceecc6652ca3c35c6ec3a4a095cd84d02b07749c52317814/alibabacloud_sas20181203-6.1.0.tar.gz", hash = "sha256:e49ffd53e630274a8bf5a8299ca753023ad118510c80f6d9c6fb018b7479bf37", size = 837657, upload-time = "2025-11-13T17:30:54.077Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/fb/0f67a5c982f1818001e97caeebece2ad8b658b1f2cb51e948ab89201754f/alibabacloud_sas20181203-6.1.0-py3-none-any.whl", hash = "sha256:1ad735332c50c7961be036b17420d56b5ec3b5557e3aea1daa19491e8b75da20", size = 848025, upload-time = "2025-11-13T17:30:52.387Z" }, +] + +[[package]] +name = "alibabacloud-sls20201230" +version = "5.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alibabacloud-gateway-sls" }, + { name = "alibabacloud-openapi-util" }, + { name = "alibabacloud-tea-openapi" }, + { name = "alibabacloud-tea-util" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/09/49043ef5074e1127b81d133ab606f581f206135541e734b6dccb99b0d66f/alibabacloud_sls20201230-5.9.0.tar.gz", hash = "sha256:bea830b64fbc7ed1719ba386ceeefb120f08d705f03eb0e02409dc6f12a291da", size = 120270, upload-time = "2025-09-11T12:50:32.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/6b/8167bfdd5f35fcee5ac0738627d3d157687a584c660cedea8d45b0b90dcd/alibabacloud_sls20201230-5.9.0-py3-none-any.whl", hash = "sha256:c4ae14096817a9686af5a0ae2389f1f6a8781e60b9edb8643445250cf15c26f1", size = 118782, upload-time = "2025-09-11T12:50:31.17Z" }, +] + +[[package]] +name = "alibabacloud-sts20150401" +version = "1.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alibabacloud-endpoint-util" }, + { name = "alibabacloud-openapi-util" }, + { name = "alibabacloud-tea-openapi" }, + { name = "alibabacloud-tea-util" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/94/3b360c5051d4e4c3f2f410b7631e4a8bf7e508a498acc15be6ec7bf58594/alibabacloud_sts20150401-1.1.6.tar.gz", hash = "sha256:c2529b41e0e4531e21cb393e4df346e19fd6d54cc6337d1138dbcd2191438d4c", size = 12343, upload-time = "2025-06-30T09:19:52.537Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/fa/cc66a8527235e61718e7cdbc43f82d11124ca44c89ce336226d5dc875b80/alibabacloud_sts20150401-1.1.6-py3-none-any.whl", hash = "sha256:627f5ca1f86e19b0bf8ce0e99071a36fb65579fad9256fbee38fdc8d500598e9", size = 11072, upload-time = "2025-06-30T09:19:51.176Z" }, +] + +[[package]] +name = "alibabacloud-tea" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/7d/b22cb9a0d4f396ee0f3f9d7f26b76b9ed93d4101add7867a2c87ed2534f5/alibabacloud-tea-0.4.3.tar.gz", hash = "sha256:ec8053d0aa8d43ebe1deb632d5c5404339b39ec9a18a0707d57765838418504a", size = 8785, upload-time = "2025-03-24T07:34:42.958Z" } + +[[package]] +name = "alibabacloud-tea-openapi" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alibabacloud-credentials" }, + { name = "alibabacloud-gateway-spi" }, + { name = "alibabacloud-tea-util" }, + { name = "cryptography" }, + { name = "darabonba-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/93/138bcdc8fc596add73e37cf2073798f285284d1240bda9ee02f9384fc6be/alibabacloud_tea_openapi-0.4.4.tar.gz", hash = "sha256:1b0917bc03cd49417da64945e92731716d53e2eb8707b235f54e45b7473221ce", size = 21960, upload-time = "2026-03-26T10:16:16.792Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/5a/6bfc4506438c1809c486f66217ad11eab78157192b3d5707b4e2f4212f6c/alibabacloud_tea_openapi-0.4.4-py3-none-any.whl", hash = "sha256:cea6bc1fe35b0319a8752cb99eb0ecb0dab7ca1a71b99c12970ba0867410995f", size = 26236, upload-time = "2026-03-26T10:16:15.861Z" }, +] + +[[package]] +name = "alibabacloud-tea-util" +version = "0.3.14" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alibabacloud-tea" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/ee/ea90be94ad781a5055db29556744681fc71190ef444ae53adba45e1be5f3/alibabacloud_tea_util-0.3.14.tar.gz", hash = "sha256:708e7c9f64641a3c9e0e566365d2f23675f8d7c2a3e2971d9402ceede0408cdb", size = 7515, upload-time = "2025-11-19T06:01:08.504Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/9e/c394b4e2104766fb28a1e44e3ed36e4c7773b4d05c868e482be99d5635c9/alibabacloud_tea_util-0.3.14-py3-none-any.whl", hash = "sha256:10d3e5c340d8f7ec69dd27345eb2fc5a1dab07875742525edf07bbe86db93bfe", size = 6697, upload-time = "2025-11-19T06:01:07.355Z" }, +] + +[[package]] +name = "alibabacloud-tea-xml" +version = "0.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alibabacloud-tea" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/eb/5e82e419c3061823f3feae9b5681588762929dc4da0176667297c2784c1a/alibabacloud_tea_xml-0.0.3.tar.gz", hash = "sha256:979cb51fadf43de77f41c69fc69c12529728919f849723eb0cd24eb7b048a90c", size = 3466, upload-time = "2025-07-01T08:04:55.144Z" } + +[[package]] +name = "alibabacloud-vpc20160428" +version = "6.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alibabacloud-endpoint-util" }, + { name = "alibabacloud-openapi-util" }, + { name = "alibabacloud-tea-openapi" }, + { name = "alibabacloud-tea-util" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/27/1d8dcfd832d5a21390347002cc69999adfb5c7e0a832345209c56e9f504c/alibabacloud_vpc20160428-6.13.0.tar.gz", hash = "sha256:daf00679a83d422799f9fcf263739fe1f360641675843cbfbe623833fc8b1681", size = 465701, upload-time = "2025-11-03T17:59:28.327Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b9/ad80faf7ac76c925e1b3fa766d1148effab3418cbf9a004eefa03e6e068e/alibabacloud_vpc20160428-6.13.0-py3-none-any.whl", hash = "sha256:933cf1e74322a20a2df27ca6323760d857744a4246eeadc9fb3eae01322fb1c6", size = 472137, upload-time = "2025-11-03T17:59:26.686Z" }, +] + +[[package]] +name = "alive-progress" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "about-time" }, + { name = "graphemeu" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/26/d43128764a6f8fe1668c4f87aba6b1fe52bea81d05a35c84a70d3c70b6f7/alive-progress-3.3.0.tar.gz", hash = "sha256:457dd2428b48dacd49854022a46448d236a48f1b7277874071c39395307e830c", size = 116281, upload-time = "2025-07-20T02:10:39.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/85/ec72f6c885703d18f3b09769645e950e14c7d0cc0a0e35d94127983f666f/alive_progress-3.3.0-py3-none-any.whl", hash = "sha256:63dd33bb94cde15ad9e5b666dbba8fedf71b72a4935d6fb9a92931e69402c9ff", size = 78403, upload-time = "2025-07-20T02:10:37.318Z" }, +] + +[[package]] +name = "aliyun-log-fastpb" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/ce/be2bc51121ab88ba9fd9d1f743a5bc2402036a0c4fe88d658d70bf03618e/aliyun_log_fastpb-0.2.0.tar.gz", hash = "sha256:91c714e76fb941c9a0db6b1aa1f4c56cb1626254ff5444c1179860f5e5b63d93", size = 16668, upload-time = "2025-11-18T02:22:13.115Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/68/8456bfeb892e08f9abf676218995a3efed24ed41612bca8538c84e2de556/aliyun_log_fastpb-0.2.0-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:51633d92d2b349aed4843c0b503454fb4f7d73eeaaa54f82aa5a36c10c064ef5", size = 388912, upload-time = "2025-11-18T02:21:54.508Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e5/0a2aefe7e8ed364ae136896fd8f1a77b93b8b30a4eab7a642675a619f296/aliyun_log_fastpb-0.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d2984aafc61ccbbf1db2589ce90b6d5a26e72dba137fb1fdf7f61ce3faa967c0", size = 199594, upload-time = "2025-11-18T02:21:55.896Z" }, + { url = "https://files.pythonhosted.org/packages/01/62/d5e1beb5d5b964387e665fad710ce917b849e23e00cac643d931d26f8274/aliyun_log_fastpb-0.2.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:181fc61ac9934f58b0880fa5617a4a4dc709dba09f8be95b5a71e828f2e48053", size = 208736, upload-time = "2025-11-18T02:21:57.19Z" }, + { url = "https://files.pythonhosted.org/packages/58/30/9b7585e4cb5bb7fb5e7ea257e98771b5846f55e1d79d401c92f7eee6232d/aliyun_log_fastpb-0.2.0-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12b8bfddf0bc5450f16f1954c6387a73da124fae10d1205a17a0117e66bb56db", size = 215661, upload-time = "2025-11-18T02:21:58.528Z" }, + { url = "https://files.pythonhosted.org/packages/dd/24/9196b789addf90760b9eda18f080bad6d0b5c0855b680d35931cab1c5bea/aliyun_log_fastpb-0.2.0-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8fbc83cbaa51d332e5e68871c1200014f1f3de54a8cba4fb55a634ee145cd4e4", size = 344569, upload-time = "2025-11-18T02:21:59.792Z" }, + { url = "https://files.pythonhosted.org/packages/17/b8/7266923774c406b78aff6bd5e5f3d1f86b0d1ff469e591bc3b5c113e8699/aliyun_log_fastpb-0.2.0-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42a86a6e11dd227d595fa23f69d30588446af19d045d1003bd1b66b5c9a55485", size = 339922, upload-time = "2025-11-18T02:22:01.104Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f6/8e8ff4a696ee0232c8bb5de90136cf70be5897f3f6dbdeae449134393d02/aliyun_log_fastpb-0.2.0-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd92c0b84ba300c1d1c227204c5f2fff243cea80bc3f9399293385e87c82ee3e", size = 241647, upload-time = "2025-11-18T02:22:02.291Z" }, + { url = "https://files.pythonhosted.org/packages/56/2c/02696108e224741a708d1812bfd11ab36c7c5743175e227b574e2cb7aef2/aliyun_log_fastpb-0.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7c07a6d81a3eab6666949240da305236ed2350c305154d7e39fcc121fc52291", size = 221910, upload-time = "2025-11-18T02:22:03.606Z" }, + { url = "https://files.pythonhosted.org/packages/f2/de/19b70b6fca8892e2d82ca8d7231378cf16f3158b6d8486484a8ca3bd5dc9/aliyun_log_fastpb-0.2.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2cff4fbdd0edff94adcee1dcabf16daacb5d336a12fc897887aa6e4f0ad25152", size = 238372, upload-time = "2025-11-18T02:22:04.973Z" }, + { url = "https://files.pythonhosted.org/packages/62/f3/50200a879a4259efe6c562252ac6413036009efdfd83c6369a67d9f67fdb/aliyun_log_fastpb-0.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5a451809e2a062accbb8dae8750e507e58806e4a8da48d69215cdeef428e9d63", size = 391304, upload-time = "2025-11-18T02:22:06.042Z" }, + { url = "https://files.pythonhosted.org/packages/5f/24/17e26a16a9e147331dc161e5e3f2f327502e693eb15f229232c479ea990a/aliyun_log_fastpb-0.2.0-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:61f09df30232f1f5628d13310cf0e175171399ea1c75a8470e9f9d97b045bfb5", size = 484348, upload-time = "2025-11-18T02:22:07.034Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a5/0daafb81d37e69981651722406eacf2e302d57fe787002b3e3d816cf411d/aliyun_log_fastpb-0.2.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:a5fbf0d41d8c0c964a3dc8dd0ee2e732f876b803e0ed3432550ef3b84dde84f1", size = 422952, upload-time = "2025-11-18T02:22:08.47Z" }, + { url = "https://files.pythonhosted.org/packages/32/11/5c715a1af59249485ea703ee4d34653e5403c8d9f50ae6fc80286fd7e9d8/aliyun_log_fastpb-0.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ae2f84ed0777e00045791044a56413f370afbd5b061505f5ded540c04b19c58e", size = 392587, upload-time = "2025-11-18T02:22:09.634Z" }, + { url = "https://files.pythonhosted.org/packages/2b/83/fdb216167c825878456e69937433db14519c0513a9c450c6e0c200abb170/aliyun_log_fastpb-0.2.0-cp37-abi3-win32.whl", hash = "sha256:967f9656c805602fd9be07d8c2756ad89204c852c99689c3c71aa035416ef42a", size = 112174, upload-time = "2025-11-18T02:22:10.643Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/013c773af65875ed74876197d016f6fb58dd4c14348462546de96ff8ff8c/aliyun_log_fastpb-0.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:bbdcf7b85f0f3437c2a8e8a1db0ef5584d21468b7c7a358269a4c651c84f4a54", size = 117948, upload-time = "2025-11-18T02:22:11.99Z" }, +] + +[[package]] +name = "amqp" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "vine" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/fc/ec94a357dfc6683d8c86f8b4cfa5416a4c36b28052ec8260c77aca96a443/amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432", size = 129013, upload-time = "2024-11-12T19:55:44.051Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/99/fc813cd978842c26c82534010ea849eee9ab3a13ea2b74e95cb9c99e747b/amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2", size = 50944, upload-time = "2024-11-12T19:55:41.782Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "applicationinsights" +version = "0.11.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/f2/46a75ac6096d60da0e71a068015b610206e697de01fa2fb5bba8564b0798/applicationinsights-0.11.10.tar.gz", hash = "sha256:0b761f3ef0680acf4731906dfc1807faa6f2a57168ae74592db0084a6099f7b3", size = 44722, upload-time = "2021-04-22T23:22:45.71Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/0d/cb6b23164eb55eebaa5f9f302dfe557cfa751bd7b2779863f1abd0343b6b/applicationinsights-0.11.10-py2.py3-none-any.whl", hash = "sha256:e89a890db1c6906b6a7d0bcfd617dac83974773c64573147c8d6654f9cf2a6ea", size = 55068, upload-time = "2021-04-22T23:22:44.451Z" }, +] + +[[package]] +name = "apscheduler" +version = "3.11.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzlocal" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/12/3e4389e5920b4c1763390c6d371162f3784f86f85cd6d6c1bfe68eef14e2/apscheduler-3.11.2.tar.gz", hash = "sha256:2a9966b052ec805f020c8c4c3ae6e6a06e24b1bf19f2e11d91d8cca0473eef41", size = 108683, upload-time = "2025-12-22T00:39:34.884Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/64/2e54428beba8d9992aa478bb8f6de9e4ecaa5f8f513bcfd567ed7fb0262d/apscheduler-3.11.2-py3-none-any.whl", hash = "sha256:ce005177f741409db4e4dd40a7431b76feb856b9dd69d57e0da49d6715bfd26d", size = 64439, upload-time = "2025-12-22T00:39:33.303Z" }, +] + +[[package]] +name = "argcomplete" +version = "3.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/be/6c23d80cb966fb8f83fb1ebfb988351ae6b0554d0c3a613ee4531c026597/argcomplete-3.5.3.tar.gz", hash = "sha256:c12bf50eded8aebb298c7b7da7a5ff3ee24dffd9f5281867dfe1424b58c55392", size = 72999, upload-time = "2024-12-31T19:22:57.301Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/08/2a4db06ec3d203124c967fc89295e85a202e5cbbcdc08fd6a64b65217d1e/argcomplete-3.5.3-py3-none-any.whl", hash = "sha256:2ab2c4a215c59fd6caaff41a869480a23e8f6a5f910b266c1808037f4e375b61", size = 43569, upload-time = "2024-12-31T19:22:54.305Z" }, +] + +[[package]] +name = "asgiref" +version = "3.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/b9/4db2509eabd14b4a8c71d1b24c8d5734c52b8560a7b1e1a8b56c8d25568b/asgiref-3.11.0.tar.gz", hash = "sha256:13acff32519542a1736223fb79a715acdebe24286d98e8b164a73085f40da2c4", size = 37969, upload-time = "2025-11-19T15:32:20.106Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/be/317c2c55b8bbec407257d45f5c8d1b6867abc76d12043f2d3d58c538a4ea/asgiref-3.11.0-py3-none-any.whl", hash = "sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d", size = 24096, upload-time = "2025-11-19T15:32:19.004Z" }, +] + +[[package]] +name = "astroid" +version = "3.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/53/1067e1113ecaf58312357f2cd93063674924119d80d173adc3f6f2387aa2/astroid-3.2.4.tar.gz", hash = "sha256:0e14202810b30da1b735827f78f5157be2bbd4a7a59b7707ca0bfc2fb4c0063a", size = 397576, upload-time = "2024-07-20T12:57:43.26Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/96/b32bbbb46170a1c8b8b1f28c794202e25cfe743565e9d3469b8eb1e0cc05/astroid-3.2.4-py3-none-any.whl", hash = "sha256:413658a61eeca6202a59231abb473f932038fbcbf1666587f66d482083413a25", size = 276348, upload-time = "2024-07-20T12:57:40.886Z" }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "autopep8" +version = "2.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycodestyle" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/d8/30873d2b7b57dee9263e53d142da044c4600a46f2d28374b3e38b023df16/autopep8-2.3.2.tar.gz", hash = "sha256:89440a4f969197b69a995e4ce0661b031f455a9f776d2c5ba3dbd83466931758", size = 92210, upload-time = "2025-01-14T14:46:18.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl", hash = "sha256:ce8ad498672c845a0c3de2629c15b635ec2b05ef8177a6e7c91c74f3e9b51128", size = 45807, upload-time = "2025-01-14T14:46:15.466Z" }, +] + +[[package]] +name = "awsipranges" +version = "0.3.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/2e/6efa95f995369da828715f41705686cd214b9259ed758266942553d40441/awsipranges-0.3.3.tar.gz", hash = "sha256:4f0b3f22a9dc1163c85b513bed812b6c92bdacd674e6a7b68252a3c25b99e2c0", size = 16739, upload-time = "2022-02-10T21:08:32.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/ce/5c9a8bf91bdc9592a409c99e58fd99f2727ab8d634719c0ad796021b76d7/awsipranges-0.3.3-py3-none-any.whl", hash = "sha256:f3d7a54aeaf7fe310beb5d377a4034a63a51b72677ae6af3e0967bc4de7eedaf", size = 18106, upload-time = "2022-02-10T21:08:31.497Z" }, +] + +[[package]] +name = "azure-cli-core" +version = "2.83.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argcomplete" }, + { name = "azure-cli-telemetry" }, + { name = "azure-core" }, + { name = "azure-mgmt-core" }, + { name = "cryptography" }, + { name = "distro", marker = "sys_platform == 'linux'" }, + { name = "humanfriendly" }, + { name = "jmespath" }, + { name = "knack" }, + { name = "microsoft-security-utilities-secret-masker" }, + { name = "msal" }, + { name = "msal", extra = ["broker"], marker = "sys_platform == 'win32'" }, + { name = "msal-extensions" }, + { name = "packaging" }, + { name = "pkginfo" }, + { name = "psutil", marker = "sys_platform != 'cygwin'" }, + { name = "py-deviceid" }, + { name = "pyjwt" }, + { name = "pyopenssl" }, + { name = "requests", extra = ["socks"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/c2/2bde346a14db05ef5e5a6b1c0660ab507683b40682c79017f7bbcaef1efa/azure_cli_core-2.83.0.tar.gz", hash = "sha256:ac59ae4307a961891587d746984a3349b7afe9759ed8267e1cdd614aeeeabbf9", size = 231971, upload-time = "2026-02-03T03:43:28.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/e9/d74bfd65e7bb491c985ff9a769c9d6593e94599b32c82b716f6fddbfb40a/azure_cli_core-2.83.0-py3-none-any.whl", hash = "sha256:3136f1434cb6fbd2f5b1d7f82b15cff3d4ba4a638808a86584376a829fd26b8a", size = 262490, upload-time = "2026-02-03T03:43:33.385Z" }, +] + +[[package]] +name = "azure-cli-telemetry" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "applicationinsights" }, + { name = "portalocker" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/1d/c1cf3663391a271864d866e17f911b913ec257386aef7cac35121ed180a6/azure-cli-telemetry-1.1.0.tar.gz", hash = "sha256:d922379cda1b48952be75fb3bd2ac5e7ceecf569492a6088bab77894c624a278", size = 9233, upload-time = "2023-08-01T02:15:31.121Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/23/5940808a891282e7fdd240d689aedc91bc945b4eb653c15e694f45a4babc/azure_cli_telemetry-1.1.0-py3-none-any.whl", hash = "sha256:2fc12608c0cf0ea6e69b392af9cab92f1249340b8caff7e9674cf91b3becb337", size = 11538, upload-time = "2023-08-01T02:15:37.126Z" }, +] + +[[package]] +name = "azure-common" +version = "1.1.28" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/71/f6f71a276e2e69264a97ad39ef850dca0a04fce67b12570730cb38d0ccac/azure-common-1.1.28.zip", hash = "sha256:4ac0cd3214e36b6a1b6a442686722a5d8cc449603aa833f3f0f40bda836704a3", size = 20914, upload-time = "2022-02-03T19:39:44.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/55/7f118b9c1b23ec15ca05d15a578d8207aa1706bc6f7c87218efffbbf875d/azure_common-1.1.28-py2.py3-none-any.whl", hash = "sha256:5c12d3dcf4ec20599ca6b0d3e09e86e146353d443e7fcc050c9a19c1f9df20ad", size = 14462, upload-time = "2022-02-03T19:39:42.417Z" }, +] + +[[package]] +name = "azure-core" +version = "1.38.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/9b/23893febea484ad8183112c9419b5eb904773adb871492b5fa8ff7b21e09/azure_core-1.38.1.tar.gz", hash = "sha256:9317db1d838e39877eb94a2240ce92fa607db68adf821817b723f0d679facbf6", size = 363323, upload-time = "2026-02-11T02:03:06.051Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/88/aaea2ad269ce70b446660371286272c1f6ba66541a7f6f635baf8b0db726/azure_core-1.38.1-py3-none-any.whl", hash = "sha256:69f08ee3d55136071b7100de5b198994fc1c5f89d2b91f2f43156d20fcf200a4", size = 217930, upload-time = "2026-02-11T02:03:07.548Z" }, +] + +[[package]] +name = "azure-identity" +version = "1.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core" }, + { name = "cryptography" }, + { name = "msal" }, + { name = "msal-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/a1/f1a683672e7a88ea0e3119f57b6c7843ed52650fdcac8bfa66ed84e86e40/azure_identity-1.21.0.tar.gz", hash = "sha256:ea22ce6e6b0f429bc1b8d9212d5b9f9877bd4c82f1724bfa910760612c07a9a6", size = 266445, upload-time = "2025-03-11T20:53:07.463Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/9f/1f9f3ef4f49729ee207a712a5971a9ca747f2ca47d9cbf13cf6953e3478a/azure_identity-1.21.0-py3-none-any.whl", hash = "sha256:258ea6325537352440f71b35c3dffe9d240eae4a5126c1b7ce5efd5766bd9fd9", size = 189190, upload-time = "2025-03-11T20:53:09.197Z" }, +] + +[[package]] +name = "azure-keyvault-certificates" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core" }, + { name = "isodate" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/bc/07497d7118178729f59043153083bcdfdfc9629a365cbc5bc5690b4ff21b/azure_keyvault_certificates-4.10.0.tar.gz", hash = "sha256:004ff47a73152f9f40f678e5a07719b753a3ca86f0460bfeaaf6a23304872e05", size = 179157, upload-time = "2025-06-16T22:52:24.547Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/03/71cfb1ee68a625ca1c211a3b18e1051a4dd9fe8774329e48bb3c67afcf86/azure_keyvault_certificates-4.10.0-py3-none-any.whl", hash = "sha256:fa76cbc329274cb5f4ab61b0ed7d209d44377df4b4d6be2fd01e741c2fbb83a9", size = 155683, upload-time = "2025-06-16T22:52:26.02Z" }, +] + +[[package]] +name = "azure-keyvault-keys" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core" }, + { name = "cryptography" }, + { name = "isodate" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/f9/85c95072c4f396126a8ae145ffb45fb3e7bea660b6cb8ff8b2f702944a57/azure_keyvault_keys-4.10.0.tar.gz", hash = "sha256:511206ae90aec1726a4d6ff5a92d754bd0c0f1e8751891368d30fb70b62955f1", size = 216961, upload-time = "2024-10-17T23:27:52.872Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/63/04fc6ab70fc30bae3448837411ded3c17bb1ace5bf3cfdf353961fe3f7fa/azure_keyvault_keys-4.10.0-py3-none-any.whl", hash = "sha256:210227e0061f641a79755f0e0bcbcf27bbfb4df630a933c43a99a29962283d0d", size = 153660, upload-time = "2024-10-17T23:27:55.213Z" }, +] + +[[package]] +name = "azure-keyvault-secrets" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core" }, + { name = "isodate" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/e5/3074e581b6e8923c4a1f2e42192ea6f390bb52de3600c68baaaed529ef05/azure_keyvault_secrets-4.10.0.tar.gz", hash = "sha256:666fa42892f9cee749563e551a90f060435ab878977c95265173a8246d546a36", size = 129695, upload-time = "2025-06-16T22:52:20.986Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/94/7c902e966b28e7cb5080a8e0dd6bffc22ba44bc907f09c4c633d2b7c4f6a/azure_keyvault_secrets-4.10.0-py3-none-any.whl", hash = "sha256:9dbde256077a4ee1a847646671580692e3f9bea36bcfc189c3cf2b9a94eb38b9", size = 125237, upload-time = "2025-06-16T22:52:22.489Z" }, +] + +[[package]] +name = "azure-mgmt-apimanagement" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-common" }, + { name = "azure-mgmt-core" }, + { name = "isodate" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/e4/3e536f0374bccb203886ef7aeb1148e063de6be31187034179ddcbf1adff/azure_mgmt_apimanagement-5.0.0.tar.gz", hash = "sha256:0ab7fe17e70fe3154cd840ff47d19d7a4610217003eaa7c21acf3511a6e57999", size = 625729, upload-time = "2025-04-22T06:24:05.364Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/d8/1406ff959601b1a93fb662b1a76067ecef7094680efcdd75c007457f9efd/azure_mgmt_apimanagement-5.0.0-py3-none-any.whl", hash = "sha256:b88c42a392333b60722fb86f15d092dfc19a8d67510dccd15c217381dff4e6ec", size = 1150262, upload-time = "2025-04-22T06:24:07.012Z" }, +] + +[[package]] +name = "azure-mgmt-applicationinsights" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-common" }, + { name = "azure-mgmt-core" }, + { name = "isodate" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/8b/f3c8886ecd90d440458c4cc2b1db4ff47215f189cc3c0ba3cf11b7d4d64e/azure_mgmt_applicationinsights-4.1.0.tar.gz", hash = "sha256:15531390f12ce3d767cd3f1949af36aa39077c145c952fec4d80303c86ec7b6c", size = 323696, upload-time = "2025-03-06T08:50:52.14Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/45/f9f050523282d642598396ce9880b7b8be0d41545985935d2134199393b2/azure_mgmt_applicationinsights-4.1.0-py3-none-any.whl", hash = "sha256:9e71f29b01e505a773501451d12fd6a10482cf4b13e9ac2bff72f5380496d979", size = 616137, upload-time = "2025-03-06T08:50:54.044Z" }, +] + +[[package]] +name = "azure-mgmt-authorization" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-common" }, + { name = "azure-mgmt-core" }, + { name = "isodate" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/ab/e79874f166eed24f4456ce4d532b29a926fb4c798c2c609eefd916a3f73d/azure-mgmt-authorization-4.0.0.zip", hash = "sha256:69b85abc09ae64fc72975bd43431170d8c7eb5d166754b98aac5f3845de57dc4", size = 1134795, upload-time = "2023-07-25T04:47:46.033Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/b3/8ec1268082f4d20cc8bf723a1a8e6b9e330bcc338a4dbcee9c7737e9dc1c/azure_mgmt_authorization-4.0.0-py3-none-any.whl", hash = "sha256:d8feeb3842e6ddf1a370963ca4f61fb6edc124e8997b807dd025bc9b2379cd1a", size = 1072620, upload-time = "2023-07-25T04:47:49.26Z" }, +] + +[[package]] +name = "azure-mgmt-compute" +version = "34.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-common" }, + { name = "azure-mgmt-core" }, + { name = "isodate" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/3f/72e09a6f9a12d8afed8f56c929e8e142de21be9c19c27e4b2b94d60eb73a/azure_mgmt_compute-34.0.0.tar.gz", hash = "sha256:58cd01d025efa02870b84dbfb69834a3b23501a135658c03854d2434e8dfee1e", size = 1785981, upload-time = "2025-01-20T06:47:24.162Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/72/93e378927b59921c863a97c1586266857ccc6a4ee2c9904e43942c90b1ee/azure_mgmt_compute-34.0.0-py3-none-any.whl", hash = "sha256:f8f7b1c5c187a26fae4d1f099adf93561244242f28899484d9a42747bf0d5af4", size = 2010334, upload-time = "2025-01-20T06:47:26.861Z" }, +] + +[[package]] +name = "azure-mgmt-containerinstance" +version = "10.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-common" }, + { name = "azure-mgmt-core" }, + { name = "isodate" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/19/cdb22d87560238893f5c014176b4e6868c3befbd6585bb5c44bdb1ddc997/azure-mgmt-containerinstance-10.1.0.zip", hash = "sha256:78d437adb28574f448c838ed5f01f9ced378196098061deb59d9f7031704c17e", size = 106507, upload-time = "2023-04-21T03:30:50.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/11/3bfa586ac0514e1fe224879c065dbec39c0d9d2653b74549ac8080408f07/azure_mgmt_containerinstance-10.1.0-py3-none-any.whl", hash = "sha256:ee7977b7b70f2233e44ec6ce8c99027f3f7892bb3452b4bad46df340d9f98959", size = 87314, upload-time = "2023-04-21T03:30:52.379Z" }, +] + +[[package]] +name = "azure-mgmt-containerregistry" +version = "12.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-common" }, + { name = "azure-mgmt-core" }, + { name = "isodate" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/be/5806085a314e314ca5a53f5fa7ac87de55aad9bcde4c417b12d515edec8e/azure_mgmt_containerregistry-12.0.0.tar.gz", hash = "sha256:f19f8faa7881deaf2b5015c0eb050a92e2380cd9d18dee33cdb5f27d44a06c03", size = 720736, upload-time = "2025-02-24T06:14:57.012Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/d4/98a626a676b230d3bcbbf5aca38ef2c95063186093335a13ab399d6c5fac/azure_mgmt_containerregistry-12.0.0-py3-none-any.whl", hash = "sha256:464abd4d3d9ecc0456ed8f63a6b9b93afc2e3e194f2d34f26a758afb67ad3b5c", size = 1130225, upload-time = "2025-02-24T06:14:58.849Z" }, +] + +[[package]] +name = "azure-mgmt-containerservice" +version = "34.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-common" }, + { name = "azure-mgmt-core" }, + { name = "isodate" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/31/b0/a5a2d6c2b2f9f7404f86732d400786d7cd996a155467f47ecaf786ce56d9/azure_mgmt_containerservice-34.1.0.tar.gz", hash = "sha256:637a6cf8f06636c016ad151d76f9c7ba75bd05d4334b3dd7837eb8b517f30dbe", size = 319609, upload-time = "2025-02-19T04:08:41.59Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/c7/a03d8ea9827442be3f2310e857266e659c5c8aa516367654a4bf90dfe9ab/azure_mgmt_containerservice-34.1.0-py3-none-any.whl", hash = "sha256:1faa1714e0100c6ee4cfb8d2eadb1c270b548a84b0070c74e9fe646056a5cb12", size = 415971, upload-time = "2025-02-19T04:08:44.203Z" }, +] + +[[package]] +name = "azure-mgmt-core" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/99/fa9e7551313d8c7099c89ebf3b03cd31beb12e1b498d575aa19bb59a5d04/azure_mgmt_core-1.6.0.tar.gz", hash = "sha256:b26232af857b021e61d813d9f4ae530465255cb10b3dde945ad3743f7a58e79c", size = 30818, upload-time = "2025-07-03T02:02:24.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/26/c79f962fd3172b577b6f38685724de58b6b4337a51d3aad316a43a4558c6/azure_mgmt_core-1.6.0-py3-none-any.whl", hash = "sha256:0460d11e85c408b71c727ee1981f74432bc641bb25dfcf1bb4e90a49e776dbc4", size = 29310, upload-time = "2025-07-03T02:02:25.203Z" }, +] + +[[package]] +name = "azure-mgmt-cosmosdb" +version = "9.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-common" }, + { name = "azure-mgmt-core" }, + { name = "isodate" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/2a/3240e83aff38443d334a17467d32a46bab269164ab9477bb17d2277b32f8/azure_mgmt_cosmosdb-9.7.0.tar.gz", hash = "sha256:b5072d319f11953d8f12e22459aded1912d5f27e442e1d8b49596a85005410a1", size = 265676, upload-time = "2024-11-19T03:19:26.253Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/71/506877fd7a94f535444eaf6d110254150a7d1db525f0a8f1ae13b034ef5f/azure_mgmt_cosmosdb-9.7.0-py3-none-any.whl", hash = "sha256:be735a554d16995c8cefe413e62119985f8fabae1cb45a6f6ad2c3958bed14da", size = 389922, upload-time = "2024-11-19T03:19:28.39Z" }, +] + +[[package]] +name = "azure-mgmt-databricks" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-common" }, + { name = "azure-mgmt-core" }, + { name = "isodate" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/6d/b5e549a1287a4a0a2a194dcc4578c81af66111b8b18a73f4dbd434539b07/azure-mgmt-databricks-2.0.0.zip", hash = "sha256:70d11362dc2d17f5fb1db0cfe65c1af55b8f136f1a0db9a5b51e7acf760cf5b9", size = 109722, upload-time = "2023-06-29T03:03:15.772Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/ed/2c0af6f1f384e18f9bd7dec7124fc20fba9b9ab814d3223f29039c75fbd6/azure_mgmt_databricks-2.0.0-py3-none-any.whl", hash = "sha256:0c29434a7339e74231bd171a6c08dcdf8153abaebd332658d7f66b8ea143fa17", size = 97759, upload-time = "2023-06-29T03:03:17.565Z" }, +] + +[[package]] +name = "azure-mgmt-datafactory" +version = "9.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-common" }, + { name = "azure-mgmt-core" }, + { name = "isodate" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/7a/253f8d0b4c3971ad0f11408b80c3429dc75408834e219ed2f0d928b4a586/azure_mgmt_datafactory-9.2.0.tar.gz", hash = "sha256:5132e9c24c441ac225f2a60225924baa55079ca81eff7db99a70d661d64bb0d7", size = 484275, upload-time = "2025-04-21T04:21:28.539Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/dd/2e7b6bac7420a05bc4ad17cb64099948117b12ca50ea68d6d4665867085e/azure_mgmt_datafactory-9.2.0-py3-none-any.whl", hash = "sha256:d870a7a6099227e91d1c258a956c2aa32c2ea4c0a4409913d8f215887349f128", size = 554231, upload-time = "2025-04-21T04:21:30.956Z" }, +] + +[[package]] +name = "azure-mgmt-eventgrid" +version = "10.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-common" }, + { name = "azure-mgmt-core" }, + { name = "isodate" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/68/24a2921b66827a571a3da75cc12289b7029a7c436087c0c5b2b4ca387aa3/azure_mgmt_eventgrid-10.4.0.tar.gz", hash = "sha256:303e5e27cf4bb5ec833ba4e5a9ef70b5bc410e190412ec47cde59d82e413fb7e", size = 258796, upload-time = "2025-03-24T03:32:20.39Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/c7/6dfac325b4b081038aa3495e048740b44d0a68b7da97d922e0aa47a67351/azure_mgmt_eventgrid-10.4.0-py3-none-any.whl", hash = "sha256:5e4637245bbff33298d5f427971b870dbb03d873a3ef68f328190a7b7a38c56f", size = 353207, upload-time = "2025-03-24T03:32:22.847Z" }, +] + +[[package]] +name = "azure-mgmt-eventhub" +version = "11.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-common" }, + { name = "azure-mgmt-core" }, + { name = "isodate" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/41/5080742de3a0c7a2c579fcf6995c531d48a83f9c588b52334b41bd84d666/azure_mgmt_eventhub-11.2.0.tar.gz", hash = "sha256:31c47f18f73d2d83345cde5909568e28858c2548a35b10e23194b4767a9ce7e3", size = 626966, upload-time = "2025-01-21T07:06:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/a0/1142295958af5531f394662adda09ec49f0583f04475b977b4b22373336a/azure_mgmt_eventhub-11.2.0-py3-none-any.whl", hash = "sha256:a7e2618eca58d8e52c7ff7d4a04a4fae12685351746e6d01b933b43e7ea3b906", size = 1054754, upload-time = "2025-01-21T07:06:49.056Z" }, +] + +[[package]] +name = "azure-mgmt-keyvault" +version = "10.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-common" }, + { name = "azure-mgmt-core" }, + { name = "isodate" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/c9/c9cd047729de3996656da854e361636dafa4f5e9b35af449abe23ec75582/azure-mgmt-keyvault-10.3.1.tar.gz", hash = "sha256:34b92956aefbdd571cae5a03f7078e037d8087b2c00cfa6748835dc73abb5a30", size = 512188, upload-time = "2024-07-17T03:33:27.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/e8/45dee15ba05ecd27891bea507ac39121463881620c7eb09240955bdbd03b/azure_mgmt_keyvault-10.3.1-py3-none-any.whl", hash = "sha256:a18a27a06551482d31f92bc43ac8b0846af02cd69511f80090865b4c5caa3c21", size = 901390, upload-time = "2024-07-17T03:33:29.878Z" }, +] + +[[package]] +name = "azure-mgmt-loganalytics" +version = "12.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-common" }, + { name = "azure-mgmt-core" }, + { name = "msrest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/66/99802a1711cadb1c41e322dd42aaa3acfb58c70aba13e0f0cdbead21e6c4/azure-mgmt-loganalytics-12.0.0.zip", hash = "sha256:da128a7e0291be7fa2063848df92a9180cf5c16d42adc09d2bc2efd711536bfb", size = 179483, upload-time = "2021-11-17T02:17:43.918Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/8a/8fcbd6f7549f4789a00cbe523d9e4bcc9f686aaeea3a6c9fb47e6c90cac1/azure_mgmt_loganalytics-12.0.0-py2.py3-none-any.whl", hash = "sha256:75ac1d47dd81179905c40765be8834643d8994acff31056ddc1863017f3faa02", size = 146191, upload-time = "2021-11-17T02:17:42.023Z" }, +] + +[[package]] +name = "azure-mgmt-logic" +version = "10.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-common" }, + { name = "azure-mgmt-core" }, + { name = "msrest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/89/47e9b71d49ee05782cebcf3a9e4b04e974c76b19f4b2d68fd9ea9c4e358a/azure-mgmt-logic-10.0.0.zip", hash = "sha256:b3fa4864f14aaa7af41d778d925f051ed29b6016f46344765ecd0f49d0f04dd6", size = 250261, upload-time = "2022-06-13T01:38:29.884Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/66/0d8ae9ca4d75e57746026a1f9a10a7e25029511c128cf20166fce516bda9/azure_mgmt_logic-10.0.0-py3-none-any.whl", hash = "sha256:525c78afedf3edb35eb0a16152c8beba89769ee1bc6af01bcdc42842a551e443", size = 235433, upload-time = "2022-06-13T01:38:27.333Z" }, +] + +[[package]] +name = "azure-mgmt-monitor" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-common" }, + { name = "azure-mgmt-core" }, + { name = "isodate" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/31/ebabafe0be1a177428880a8ec0fc44d681ac9dc1ae66a70d859cb5c7fbc3/azure-mgmt-monitor-6.0.2.tar.gz", hash = "sha256:5ffbf500e499ab7912b1ba6d26cef26480d9ae411532019bb78d72562196e07b", size = 760481, upload-time = "2023-08-22T08:31:30.722Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/47/82ccbdb757da87c3c00d9ca0a7b7a28ab95f8dab040d1877bb15bf1c1e38/azure_mgmt_monitor-6.0.2-py3-none-any.whl", hash = "sha256:fe4cf41e6680b74a228f81451dc5522656d599c6f343ecf702fc790fda9a357b", size = 1305191, upload-time = "2023-08-22T08:31:35.989Z" }, +] + +[[package]] +name = "azure-mgmt-network" +version = "28.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-common" }, + { name = "azure-mgmt-core" }, + { name = "isodate" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/a3/8d2fa6e33107354c8cd2abcca4e0f02138bda4c6024984ae5fce5cf23b27/azure_mgmt_network-28.1.0.tar.gz", hash = "sha256:8c84bffb5ec75c6e0244e58ecf07c00d5fc421d616b0cb369c6fe585af33cf87", size = 651528, upload-time = "2024-12-20T05:56:54.599Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/68/d1604383635f1f1b16cd7f1e27004db40a1f0493c57b2da9fb36dc775a79/azure_mgmt_network-28.1.0-py3-none-any.whl", hash = "sha256:8ddb0e9ec8f10c9c152d60fc945908d113e4591f397ea3e40b92290ec2b01658", size = 575260, upload-time = "2024-12-20T05:56:57.672Z" }, +] + +[[package]] +name = "azure-mgmt-postgresqlflexibleservers" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-common" }, + { name = "azure-mgmt-core" }, + { name = "isodate" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/03/c3dacaff6551fdd482a13f4e4b3628ab9fdada888159ab9547ebc1763d7c/azure_mgmt_postgresqlflexibleservers-1.1.0.tar.gz", hash = "sha256:9ede9d8ba63e9d2879cb74adc903c649af3bc5460a02787287b0cd18d754af14", size = 115321, upload-time = "2025-03-25T07:31:55.806Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/ae/7bed1540285c20dc09bf794c91f242e0f9c1c6a0508910b669d1c4eabcb5/azure_mgmt_postgresqlflexibleservers-1.1.0-py3-none-any.whl", hash = "sha256:87ddb5a5e6d12c45769485d234cfe0322140e3a0a7636d0e61fb00ac544b5d20", size = 220299, upload-time = "2025-03-25T07:31:56.959Z" }, +] + +[[package]] +name = "azure-mgmt-rdbms" +version = "10.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-common" }, + { name = "azure-mgmt-core" }, + { name = "msrest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/3c/c1e03a11cf3dc2567ba947cc196d695d125d0d0e86af6731a7c067c5404a/azure-mgmt-rdbms-10.1.0.zip", hash = "sha256:a87d401c876c84734cdd4888af551e4a1461b4b328d9816af60cb8ac5979f035", size = 671167, upload-time = "2022-03-07T05:33:37.515Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/13/d39466bbbb1da667354aebe5329e575b6b211f40985e426cfb730d87b30f/azure_mgmt_rdbms-10.1.0-py3-none-any.whl", hash = "sha256:8eac17d1341a91d7ed914435941ba917b5ef1568acabc3e65653603966a7cc88", size = 639668, upload-time = "2022-03-07T05:33:34.171Z" }, +] + +[[package]] +name = "azure-mgmt-recoveryservices" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-common" }, + { name = "azure-mgmt-core" }, + { name = "isodate" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/52/b1b2047ffa71cda33250e5f2286966ed805feeff58f763e771fbd855a1e6/azure_mgmt_recoveryservices-3.1.0.tar.gz", hash = "sha256:7f2db98401708cf145322f50bc491caf7967bec4af3bf7b0984b9f07d3092687", size = 73743, upload-time = "2025-06-05T02:57:29.315Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/db/6282d6bcffc1816752a784f819d2ac3b3df8ecc24c52dbfb45a4e864c798/azure_mgmt_recoveryservices-3.1.0-py3-none-any.whl", hash = "sha256:21c58afdf4ae66806783e95f8cd17e3bec31be7178c48784db21f0b05de7fa66", size = 109722, upload-time = "2025-06-05T02:57:30.522Z" }, +] + +[[package]] +name = "azure-mgmt-recoveryservicesbackup" +version = "9.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-common" }, + { name = "azure-mgmt-core" }, + { name = "isodate" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/28/99997bb991c8d1d53ec1164a4f07adc520e3c10c55b7e0b814f6e6c6043e/azure_mgmt_recoveryservicesbackup-9.2.0.tar.gz", hash = "sha256:c402b3e22a6c3879df56bc37e0063142c3352c5102599ff102d19824f1b32b29", size = 318311, upload-time = "2025-04-17T08:14:51.188Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/4e/87175be17d65e6f4c0419b35d36f6df0da8e8331982eaf8cd2f14cb6bfb3/azure_mgmt_recoveryservicesbackup-9.2.0-py3-none-any.whl", hash = "sha256:c0002858d0166b6a10189a1fd580a49c83dc31b111e98010a5b2ea0f767dfff1", size = 576737, upload-time = "2025-04-17T08:14:52.957Z" }, +] + +[[package]] +name = "azure-mgmt-resource" +version = "24.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-common" }, + { name = "azure-mgmt-core" }, + { name = "isodate" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/4c/b27a3dfbedebbcc8e346a956a803528bd94a19fdf14b1de4bd781b03a6cc/azure_mgmt_resource-24.0.0.tar.gz", hash = "sha256:cf6b8995fcdd407ac9ff1dd474087129429a1d90dbb1ac77f97c19b96237b265", size = 3030022, upload-time = "2025-06-17T08:04:01.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/18/f047cb553dad6fdb65c625c4fe48552e043c4e9a859416a70c5047d07475/azure_mgmt_resource-24.0.0-py3-none-any.whl", hash = "sha256:27b32cd223e2784269f5a0db3c282042886ee4072d79cedc638438ece7cd0df4", size = 3613790, upload-time = "2025-06-17T08:04:04.046Z" }, +] + +[[package]] +name = "azure-mgmt-search" +version = "9.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-common" }, + { name = "azure-mgmt-core" }, + { name = "isodate" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/b7/b9431f1ab621f83849f3ace5ba9d2820c731409fce8466b5f06d330d19f4/azure-mgmt-search-9.1.0.tar.gz", hash = "sha256:53bc6eeadb0974d21f120bb21bb5e6827df6d650e17347460fd83e2d68883599", size = 76258, upload-time = "2023-10-25T07:53:00.328Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/17/1a8fc321662acffb784914dfd916de842c4e2d6724bd87f7bd9202166f15/azure_mgmt_search-9.1.0-py3-none-any.whl", hash = "sha256:488ff81477e980e2b7abf0b857387c74ebbad419e6f6126044e3e6fad2da72b6", size = 110342, upload-time = "2023-10-25T07:53:02.318Z" }, +] + +[[package]] +name = "azure-mgmt-security" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-common" }, + { name = "azure-mgmt-core" }, + { name = "isodate" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/90/13186657355452bdce44f27db6b194b99f78f8c185301b47624fff6d9531/azure-mgmt-security-7.0.0.tar.gz", hash = "sha256:5912eed7e9d3758fdca8d26e1dc26b41943dc4703208a1184266e2c252e1ad66", size = 684730, upload-time = "2024-05-20T05:23:17.392Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/d3/9541d871294dbc7c1eba3ffc9d4eb54cb651005ddbb1513a17f2a795bdfe/azure_mgmt_security-7.0.0-py3-none-any.whl", hash = "sha256:85a6d8b7a5cd74884a548ed53fed034449f54a9989edd64e9020c5837db96933", size = 1397437, upload-time = "2024-05-20T05:23:19.991Z" }, +] + +[[package]] +name = "azure-mgmt-sql" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-common" }, + { name = "azure-mgmt-core" }, + { name = "msrest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/af/398c57d15064ea23475076cd087b1a143b66d33a029e6e47c4688ca32310/azure-mgmt-sql-3.0.1.zip", hash = "sha256:129042cc011225e27aee6ef2697d585fa5722e5d1aeb0038af6ad2451a285457", size = 996625, upload-time = "2021-07-16T02:07:56.492Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/46/db6dd21a237c32eb747c4a1790a6b0aafe1f52c55e07b90f732231027d47/azure_mgmt_sql-3.0.1-py2.py3-none-any.whl", hash = "sha256:1d1dd940d4d41be4ee319aad626341251572a5bf4a2addec71779432d9a1381f", size = 912881, upload-time = "2021-07-16T02:07:53.079Z" }, +] + +[[package]] +name = "azure-mgmt-storage" +version = "22.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-common" }, + { name = "azure-mgmt-core" }, + { name = "isodate" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/e7/1f6a1384a77513c0d636ae411e169d22433a07fd146c8b8b7d6027f90dbb/azure_mgmt_storage-22.1.1.tar.gz", hash = "sha256:25aaa5ae8c40c30e2f91f8aae6f52906b0557e947d5c1b9817d4ff9decc11340", size = 370272, upload-time = "2025-03-01T01:32:45.371Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/e2/2b33808eb5c3d61e19ef44bb60b306cfda3c17d77629ba2648978b3202b6/azure_mgmt_storage-22.1.1-py3-none-any.whl", hash = "sha256:a4a4064918dcfa4f1cbebada5bf064935d66f2a3647a2f46a1f1c9348736f5d9", size = 569480, upload-time = "2025-03-01T01:32:47.576Z" }, +] + +[[package]] +name = "azure-mgmt-subscription" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-common" }, + { name = "azure-mgmt-core" }, + { name = "msrest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/67/14b19a006e13d86f05ee59faf78c39dc443d4fd6967344e9c94f688949c1/azure-mgmt-subscription-3.1.1.zip", hash = "sha256:4e255b4ce9b924357bb8c5009b3c88a2014d3203b2495e2256fa027bf84e800e", size = 96018, upload-time = "2022-09-06T07:30:49.467Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/12/e6de2021c4689f857386670ba0b6d3c4025d4209e45df7dd7cdabe9a4ac1/azure_mgmt_subscription-3.1.1-py3-none-any.whl", hash = "sha256:38d4574a8d47fa17e3587d756e296cb63b82ad8fb21cd8543bcee443a502bf48", size = 79409, upload-time = "2022-09-06T07:30:47.247Z" }, +] + +[[package]] +name = "azure-mgmt-synapse" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-common" }, + { name = "azure-mgmt-core" }, + { name = "msrest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/fa/5a7c375d305ec0ec06978db07ac34c4894d2b8a00087ff3dd9e1435e397f/azure-mgmt-synapse-2.0.0.zip", hash = "sha256:bec6bdfaeb55b4fdd159f2055e8875bf50a720bb0fce80a816e92a2359b898c8", size = 454745, upload-time = "2021-04-08T02:02:29.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/97/4ede2b4a0f53b9b692bf2853c320d8b94cb89253fd3eac6b6d0dc1f40047/azure_mgmt_synapse-2.0.0-py2.py3-none-any.whl", hash = "sha256:e901274009be843a7bf2eedeab32c0941fabb2addea9a1ad1560395073965f0f", size = 442173, upload-time = "2021-04-08T02:02:26.849Z" }, +] + +[[package]] +name = "azure-mgmt-web" +version = "8.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-common" }, + { name = "azure-mgmt-core" }, + { name = "isodate" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/55/5a24bc2d98830f0dc224e2baaf28b0091b7b646b390dc35c8234ae2f4830/azure_mgmt_web-8.0.0.tar.gz", hash = "sha256:c8d9c042c09db7aacb20270a9effed4d4e651e365af32d80897b84dc7bf35098", size = 2056394, upload-time = "2025-01-23T10:23:03.557Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/6f/aececaf10c925195424fe1d2a6734a2e92218875e1c4899c7a0ef3b79733/azure_mgmt_web-8.0.0-py3-none-any.whl", hash = "sha256:0536aac05bfc673b56ed930f2966b77856e84df675d376e782a7af6bb92449af", size = 2503582, upload-time = "2025-01-23T10:23:06.321Z" }, +] + +[[package]] +name = "azure-monitor-query" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core" }, + { name = "isodate" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/c0/e5c760f38224575f1eba35c319842f2be30fab599854ba9bd0b19d39c261/azure_monitor_query-2.0.0.tar.gz", hash = "sha256:7b05f2fcac4fb67fc9f77a7d4c5d98a0f3099fb73b57c69ec1b080773994671b", size = 86658, upload-time = "2025-07-30T22:23:41.534Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/0c/6b08a5a1e5f0bd97cefa13c53bf47f281a9a11732d19a94a86709acbc6bd/azure_monitor_query-2.0.0-py3-none-any.whl", hash = "sha256:8f52d581271d785e12f49cd5aaa144b8910fb843db2373855a7ef94c7fc462ea", size = 71102, upload-time = "2025-07-30T22:23:43.056Z" }, +] + +[[package]] +name = "azure-storage-blob" +version = "12.24.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core" }, + { name = "cryptography" }, + { name = "isodate" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/ff/f6e81d15687510d83a06cafba9ac38d17df71a2bb18f35a0fb169aee3af3/azure_storage_blob-12.24.1.tar.gz", hash = "sha256:052b2a1ea41725ba12e2f4f17be85a54df1129e13ea0321f5a2fcc851cbf47d4", size = 570523, upload-time = "2025-01-22T21:27:20.822Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/3c/3814aba90a63e84c7de0eb6fdf67bd1a9115ac5f99ec5b7a817a5d5278ec/azure_storage_blob-12.24.1-py3-none-any.whl", hash = "sha256:77fb823fdbac7f3c11f7d86a5892e2f85e161e8440a7489babe2195bf248f09e", size = 408432, upload-time = "2025-01-22T21:27:23.082Z" }, +] + +[[package]] +name = "azure-synapse-artifacts" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-common" }, + { name = "azure-mgmt-core" }, + { name = "isodate" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/b6/08aa179e85836089f541b8805e18e9eaca507dc2d8e608f5e9c2e893d4b3/azure_synapse_artifacts-0.21.0.tar.gz", hash = "sha256:d7e37516cf8569e03c604d921e3407d7140cf7523b67b67f757caf999e3c8ee7", size = 460511, upload-time = "2025-09-05T05:42:35.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d4/152923189ee93fc4d810b9fd84a8f97b4b80528d8e3beef0d12fb9e8d16e/azure_synapse_artifacts-0.21.0-py3-none-any.whl", hash = "sha256:3311919df13a2b42f1fb9debf5d512080c35d64d02b9f84ff944848835289a8d", size = 543285, upload-time = "2025-09-05T05:42:37.659Z" }, +] + +[[package]] +name = "backoff" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001, upload-time = "2022-10-05T19:19:32.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" }, +] + +[[package]] +name = "bandit" +version = "1.7.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "stevedore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/a4/ee391b0f046a6d8919eef246aed7c39849e299cc2e50d918b54add397de6/bandit-1.7.9.tar.gz", hash = "sha256:7c395a436743018f7be0a4cbb0a4ea9b902b6d87264ddecf8cfdc73b4f78ff61", size = 4225771, upload-time = "2024-06-12T22:25:04.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/a3/05820b7ce584a1fa01d887ec5e3274bee9f9e02a53aa63de3cb1a5ad7d24/bandit-1.7.9-py3-none-any.whl", hash = "sha256:52077cb339000f337fb25f7e045995c4ad01511e716e5daac37014b9752de8ec", size = 127957, upload-time = "2024-06-12T22:25:01.214Z" }, +] + +[[package]] +name = "billiard" +version = "4.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/23/b12ac0bcdfb7360d664f40a00b1bda139cbbbced012c34e375506dbd0143/billiard-4.2.4.tar.gz", hash = "sha256:55f542c371209e03cd5862299b74e52e4fbcba8250ba611ad94276b369b6a85f", size = 156537, upload-time = "2025-11-30T13:28:48.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/87/8bab77b323f16d67be364031220069f79159117dd5e43eeb4be2fef1ac9b/billiard-4.2.4-py3-none-any.whl", hash = "sha256:525b42bdec68d2b983347ac312f892db930858495db601b5836ac24e6477cde5", size = 87070, upload-time = "2025-11-30T13:28:47.016Z" }, +] + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + +[[package]] +name = "boto3" +version = "1.40.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/f9/6ef8feb52c3cce5ec3967a535a6114b57ac7949fd166b0f3090c2b06e4e5/boto3-1.40.61.tar.gz", hash = "sha256:d6c56277251adf6c2bdd25249feae625abe4966831676689ff23b4694dea5b12", size = 111535, upload-time = "2025-10-28T19:26:57.247Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/24/3bf865b07d15fea85b63504856e137029b6acbc73762496064219cdb265d/boto3-1.40.61-py3-none-any.whl", hash = "sha256:6b9c57b2a922b5d8c17766e29ed792586a818098efe84def27c8f582b33f898c", size = 139321, upload-time = "2025-10-28T19:26:55.007Z" }, +] + +[[package]] +name = "botocore" +version = "1.40.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/a3/81d3a47c2dbfd76f185d3b894f2ad01a75096c006a2dd91f237dca182188/botocore-1.40.61.tar.gz", hash = "sha256:a2487ad69b090f9cccd64cf07c7021cd80ee9c0655ad974f87045b02f3ef52cd", size = 14393956, upload-time = "2025-10-28T19:26:46.108Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/c5/f6ce561004db45f0b847c2cd9b19c67c6bf348a82018a48cb718be6b58b0/botocore-1.40.61-py3-none-any.whl", hash = "sha256:17ebae412692fd4824f99cde0f08d50126dc97954008e5ba2b522eb049238aa7", size = 14055973, upload-time = "2025-10-28T19:26:42.15Z" }, +] + +[[package]] +name = "cartography" +version = "0.135.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "adal" }, + { name = "aioboto3" }, + { name = "azure-cli-core" }, + { name = "azure-identity" }, + { name = "azure-keyvault-certificates" }, + { name = "azure-keyvault-keys" }, + { name = "azure-keyvault-secrets" }, + { name = "azure-mgmt-authorization" }, + { name = "azure-mgmt-compute" }, + { name = "azure-mgmt-containerinstance" }, + { name = "azure-mgmt-containerservice" }, + { name = "azure-mgmt-cosmosdb" }, + { name = "azure-mgmt-datafactory" }, + { name = "azure-mgmt-eventgrid" }, + { name = "azure-mgmt-eventhub" }, + { name = "azure-mgmt-keyvault" }, + { name = "azure-mgmt-logic" }, + { name = "azure-mgmt-monitor" }, + { name = "azure-mgmt-network" }, + { name = "azure-mgmt-resource" }, + { name = "azure-mgmt-security" }, + { name = "azure-mgmt-sql" }, + { name = "azure-mgmt-storage" }, + { name = "azure-mgmt-synapse" }, + { name = "azure-mgmt-web" }, + { name = "azure-synapse-artifacts" }, + { name = "backoff" }, + { name = "boto3" }, + { name = "botocore" }, + { name = "cloudflare" }, + { name = "crowdstrike-falconpy" }, + { name = "cryptography" }, + { name = "dnspython" }, + { name = "duo-client" }, + { name = "google-api-python-client" }, + { name = "google-auth" }, + { name = "google-cloud-asset" }, + { name = "google-cloud-resource-manager" }, + { name = "httpx" }, + { name = "kubernetes" }, + { name = "marshmallow" }, + { name = "msgraph-sdk" }, + { name = "msrestazure" }, + { name = "neo4j" }, + { name = "oci" }, + { name = "okta" }, + { name = "packageurl-python" }, + { name = "packaging" }, + { name = "pagerduty" }, + { name = "policyuniverse" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-dateutil" }, + { name = "python-digitalocean" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "scaleway" }, + { name = "slack-sdk" }, + { name = "statsd" }, + { name = "typer" }, + { name = "types-aiobotocore-ecr" }, + { name = "workos" }, + { name = "xmltodict" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/47/606851d2403a983b63813b9e95427a5dd896e49bc5a501868c041262e9a5/cartography-0.135.0.tar.gz", hash = "sha256:3f500cd22c3b392d00e8b49f62acc95fd4dcd559ce514aafe2eb8101133c7a49", size = 9106458, upload-time = "2026-04-10T16:25:34.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/e1/99a26b3e662202be77961aba73338e1448623490710b81783e53a4bbef15/cartography-0.135.0-py3-none-any.whl", hash = "sha256:c62c32a6917b8f23a8b98fe2b6c7c4a918b50f55918482966c4dae1cf5f538e1", size = 1590545, upload-time = "2026-04-10T16:25:37.669Z" }, +] + +[[package]] +name = "celery" +version = "5.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "billiard" }, + { name = "click" }, + { name = "click-didyoumean" }, + { name = "click-plugins" }, + { name = "click-repl" }, + { name = "kombu" }, + { name = "python-dateutil" }, + { name = "tzlocal" }, + { name = "vine" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/9d/3d13596519cfa7207a6f9834f4b082554845eb3cd2684b5f8535d50c7c44/celery-5.6.2.tar.gz", hash = "sha256:4a8921c3fcf2ad76317d3b29020772103581ed2454c4c042cc55dcc43585009b", size = 1718802, upload-time = "2026-01-04T12:35:58.012Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/bd/9ecd619e456ae4ba73b6583cc313f26152afae13e9a82ac4fe7f8856bfd1/celery-5.6.2-py3-none-any.whl", hash = "sha256:3ffafacbe056951b629c7abcf9064c4a2366de0bdfc9fdba421b97ebb68619a5", size = 445502, upload-time = "2026-01-04T12:35:55.894Z" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "circuitbreaker" +version = "2.1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/ac/de7a92c4ed39cba31fe5ad9203b76a25ca67c530797f6bb420fff5f65ccb/circuitbreaker-2.1.3.tar.gz", hash = "sha256:1a4baee510f7bea3c91b194dcce7c07805fe96c4423ed5594b75af438531d084", size = 10787, upload-time = "2025-03-31T08:12:08.963Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/34/15f08edd4628f65217de1fc3c1a27c82e46fe357d60c217fc9881e12ebcc/circuitbreaker-2.1.3-py3-none-any.whl", hash = "sha256:87ba6a3ed03fdc7032bc175561c2b04d52ade9d5faf94ca2b035fbdc5e6b1dd1", size = 7737, upload-time = "2025-03-31T08:12:07.802Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "click-didyoumean" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/ce/217289b77c590ea1e7c24242d9ddd6e249e52c795ff10fac2c50062c48cb/click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463", size = 3089, upload-time = "2024-03-24T08:22:07.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/5b/974430b5ffdb7a4f1941d13d83c64a0395114503cc357c6b9ae4ce5047ed/click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c", size = 3631, upload-time = "2024-03-24T08:22:06.356Z" }, +] + +[[package]] +name = "click-plugins" +version = "1.1.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/a4/34847b59150da33690a36da3681d6bbc2ec14ee9a846bc30a6746e5984e4/click_plugins-1.1.1.2.tar.gz", hash = "sha256:d7af3984a99d243c131aa1a828331e7630f4a88a9741fd05c927b204bcf92261", size = 8343, upload-time = "2025-06-25T00:47:37.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/9a/2abecb28ae875e39c8cad711eb1186d8d14eab564705325e77e4e6ab9ae5/click_plugins-1.1.1.2-py2.py3-none-any.whl", hash = "sha256:008d65743833ffc1f5417bf0e78e8d2c23aab04d9745ba817bd3e71b0feb6aa6", size = 11051, upload-time = "2025-06-25T00:47:36.731Z" }, +] + +[[package]] +name = "click-repl" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "prompt-toolkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/a2/57f4ac79838cfae6912f997b4d1a64a858fb0c86d7fcaae6f7b58d267fca/click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9", size = 10449, upload-time = "2023-06-15T12:43:51.141Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/40/9d857001228658f0d59e97ebd4c346fe73e138c6de1bce61dc568a57c7f8/click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812", size = 10289, upload-time = "2023-06-15T12:43:48.626Z" }, +] + +[[package]] +name = "cloudflare" +version = "4.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/48/e481c0a9b9010a5c41b5ca78ff9fbe00dc8a9a4d39da5af610a4ec49c7f7/cloudflare-4.3.1.tar.gz", hash = "sha256:b1e1c6beeb8d98f63bfe0a1cba874fc4e22e000bcc490544f956c689b3b5b258", size = 1933187, upload-time = "2025-06-16T21:43:18.716Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/8f/c6c543565efd3144da4304efa5917aac06b6416a8663a6defe0e9b2b7569/cloudflare-4.3.1-py3-none-any.whl", hash = "sha256:6927135a5ee5633d6e2e1952ca0484745e933727aeeb189996d2ad9d292071c6", size = 4406465, upload-time = "2025-06-16T21:43:17.3Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "contextlib2" +version = "21.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/13/37ea7805ae3057992e96ecb1cffa2fa35c2ef4498543b846f90dd2348d8f/contextlib2-21.6.0.tar.gz", hash = "sha256:ab1e2bfe1d01d968e1b7e8d9023bc51ef3509bba217bb730cee3827e1ee82869", size = 43795, upload-time = "2021-06-27T06:54:40.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/56/6d6872f79d14c0cb02f1646cbb4592eef935857c0951a105874b7b62a0c3/contextlib2-21.6.0-py2.py3-none-any.whl", hash = "sha256:3fbdb64466afd23abaf6c977627b75b6139a5a3e8ce38405c5b413aed7a0471f", size = 13277, upload-time = "2021-06-27T06:54:20.972Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, +] + +[[package]] +name = "coverage" +version = "7.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/05/31553dc038667012853d0a248b57987d8d70b2d67ea885605f87bcb1baba/coverage-7.5.4.tar.gz", hash = "sha256:a44963520b069e12789d0faea4e9fdb1e410cdc4aab89d94f7f55cbb7fef0353", size = 793238, upload-time = "2024-06-22T21:51:05.233Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/92/f56bf17b10efdb21311b7aa6853afc39eb962af0f9595a24408f7df3f694/coverage-7.5.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:db14f552ac38f10758ad14dd7b983dbab424e731588d300c7db25b6f89e335b5", size = 205211, upload-time = "2024-06-22T21:49:37.595Z" }, + { url = "https://files.pythonhosted.org/packages/b8/69/a3bdace4d667f592b7730c0d636ac9ff9195f678fb4e61b5469b91e49919/coverage-7.5.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3257fdd8e574805f27bb5342b77bc65578e98cbc004a92232106344053f319ba", size = 205664, upload-time = "2024-06-22T21:49:39.702Z" }, + { url = "https://files.pythonhosted.org/packages/cd/bd/8515e955724baab11e8220a3872dc3d1c895b841b281ac8865834257ae2e/coverage-7.5.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a6612c99081d8d6134005b1354191e103ec9705d7ba2754e848211ac8cacc6b", size = 237695, upload-time = "2024-06-22T21:49:41.485Z" }, + { url = "https://files.pythonhosted.org/packages/41/d5/f4f9d2d86e3bd0c3ae761e2511c4033abcdce1de8f1926f8e7c98952540d/coverage-7.5.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d45d3cbd94159c468b9b8c5a556e3f6b81a8d1af2a92b77320e887c3e7a5d080", size = 235270, upload-time = "2024-06-22T21:49:43.578Z" }, + { url = "https://files.pythonhosted.org/packages/1e/62/e33595d35c9fa7cbcca5df2c3745b595532ec94b68c49ca2877629c4aca1/coverage-7.5.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed550e7442f278af76d9d65af48069f1fb84c9f745ae249c1a183c1e9d1b025c", size = 236966, upload-time = "2024-06-22T21:49:46.006Z" }, + { url = "https://files.pythonhosted.org/packages/62/ea/e5ae9c845bef94369a3b9b66eb1e0857289c0a769b20078fcf5a5e6021be/coverage-7.5.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a892be37ca35eb5019ec85402c3371b0f7cda5ab5056023a7f13da0961e60da", size = 235891, upload-time = "2024-06-22T21:49:48.379Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/068a5d05ca6c89295bc8b7ae7ad5ed9d7b0286305a2444eb4d1eb42cb902/coverage-7.5.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8192794d120167e2a64721d88dbd688584675e86e15d0569599257566dec9bf0", size = 234548, upload-time = "2024-06-22T21:49:50.726Z" }, + { url = "https://files.pythonhosted.org/packages/15/a6/bbeeb4c0447a0ae8993e7d9b7ac8c8538ffb1a4210d106573238233f58c8/coverage-7.5.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:820bc841faa502e727a48311948e0461132a9c8baa42f6b2b84a29ced24cc078", size = 235329, upload-time = "2024-06-22T21:49:52.534Z" }, + { url = "https://files.pythonhosted.org/packages/01/54/e009827b234225815743303d002a146183ea25e011c088dfa7a87f895fdf/coverage-7.5.4-cp311-cp311-win32.whl", hash = "sha256:6aae5cce399a0f065da65c7bb1e8abd5c7a3043da9dceb429ebe1b289bc07806", size = 207728, upload-time = "2024-06-22T21:49:54.482Z" }, + { url = "https://files.pythonhosted.org/packages/cd/48/8b929edd540634d8e7ed50d78e86790613e8733edf7eb21c2c217bf25176/coverage-7.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:d2e344d6adc8ef81c5a233d3a57b3c7d5181f40e79e05e1c143da143ccb6377d", size = 208614, upload-time = "2024-06-22T21:49:56.374Z" }, + { url = "https://files.pythonhosted.org/packages/6d/96/58bcb3417c2fd38fae862704599f7088451bb6c8786f5cec6887366e78d9/coverage-7.5.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:54317c2b806354cbb2dc7ac27e2b93f97096912cc16b18289c5d4e44fc663233", size = 205392, upload-time = "2024-06-22T21:49:58.102Z" }, + { url = "https://files.pythonhosted.org/packages/2c/63/4f781db529b585a6ef3860ea01390951b006dbea9ada4ea3a3d830e325f4/coverage-7.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:042183de01f8b6d531e10c197f7f0315a61e8d805ab29c5f7b51a01d62782747", size = 205634, upload-time = "2024-06-22T21:49:59.841Z" }, + { url = "https://files.pythonhosted.org/packages/57/50/c5aadf036078072f31d8f1ae1a6000cc70f3f6cf652939c2d77551174d77/coverage-7.5.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6bb74ed465d5fb204b2ec41d79bcd28afccf817de721e8a807d5141c3426638", size = 238754, upload-time = "2024-06-22T21:50:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a6/57c42994b1686461c7b0b29de3b6d3d60c5f23a656f96460f9c755a31506/coverage-7.5.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3d45ff86efb129c599a3b287ae2e44c1e281ae0f9a9bad0edc202179bcc3a2e", size = 235783, upload-time = "2024-06-22T21:50:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/88/52/7054710a881b09d295e93b9889ac204c241a6847a8c05555fc6e1d8799d5/coverage-7.5.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5013ed890dc917cef2c9f765c4c6a8ae9df983cd60dbb635df8ed9f4ebc9f555", size = 237865, upload-time = "2024-06-22T21:50:07.507Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c3/57ef08c70483b83feb4e0d22345010aaf0afbe442dba015da3b173076c36/coverage-7.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1014fbf665fef86cdfd6cb5b7371496ce35e4d2a00cda501cf9f5b9e6fced69f", size = 237340, upload-time = "2024-06-22T21:50:09.636Z" }, + { url = "https://files.pythonhosted.org/packages/d8/44/465fa8f8edc11a18cbb83673f29b1af20ccf5139a66fbe2768ff67527ff0/coverage-7.5.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3684bc2ff328f935981847082ba4fdc950d58906a40eafa93510d1b54c08a66c", size = 235663, upload-time = "2024-06-22T21:50:11.852Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e5/829ddcfb29ad41661ba8e9cac7dc52100fd2c4853bb93d668a3ebde64862/coverage-7.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:581ea96f92bf71a5ec0974001f900db495488434a6928a2ca7f01eee20c23805", size = 237309, upload-time = "2024-06-22T21:50:14.195Z" }, + { url = "https://files.pythonhosted.org/packages/98/f6/f9c96fbf9b36be3f4d8c252ab2b4944420d99425f235f492784498804182/coverage-7.5.4-cp312-cp312-win32.whl", hash = "sha256:73ca8fbc5bc622e54627314c1a6f1dfdd8db69788f3443e752c215f29fa87a0b", size = 207988, upload-time = "2024-06-22T21:50:16.21Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c1/2b7c7dcf4c273aac7676f12fb2b5524b133671d731ab91bd9a41c21675b9/coverage-7.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:cef4649ec906ea7ea5e9e796e68b987f83fa9a718514fe147f538cfeda76d7a7", size = 208756, upload-time = "2024-06-22T21:50:18.147Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "cron-descriptor" +version = "1.4.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/83/70bd410dc6965e33a5460b7da84cf0c5a7330a68d6d5d4c3dfdb72ca117e/cron_descriptor-1.4.5.tar.gz", hash = "sha256:f51ce4ffc1d1f2816939add8524f206c376a42c87a5fca3091ce26725b3b1bca", size = 30666, upload-time = "2024-08-24T18:16:48.654Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/20/2cfe598ead23a715a00beb716477cfddd3e5948cf203c372d02221e5b0c6/cron_descriptor-1.4.5-py3-none-any.whl", hash = "sha256:736b3ae9d1a99bc3dbfc5b55b5e6e7c12031e7ba5de716625772f8b02dcd6013", size = 50370, upload-time = "2024-08-24T18:16:46.783Z" }, +] + +[[package]] +name = "crowdstrike-falconpy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/c6/81d4e3ac3b2f3119f3dbc5641454699dc88560eea4a640f8dd2b59c9d686/crowdstrike_falconpy-1.6.0.tar.gz", hash = "sha256:663402ac9bc56625478460b4865446371de2d74f5e96cb5d16672119c113346f", size = 9335668, upload-time = "2026-01-01T09:42:33.336Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/35/f957cc90c0cdfb19a26b6b458aee64010e422d48d0f294564e2aad09d28e/crowdstrike_falconpy-1.6.0-py3-none-any.whl", hash = "sha256:2cae39e42510f473e77f3a647d56aa8f55d9de0a1963bad353b195cb5ae61ea4", size = 1008463, upload-time = "2026-01-01T09:42:31.142Z" }, +] + +[[package]] +name = "cryptography" +version = "46.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, + { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, + { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, + { url = "https://files.pythonhosted.org/packages/63/0c/dca8abb64e7ca4f6b2978769f6fea5ad06686a190cec381f0a796fdcaaba/cryptography-46.0.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f", size = 3476879, upload-time = "2026-04-08T01:57:38.664Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/075aac6a84b7c271578d81a2f9968acb6e273002408729f2ddff517fed4a/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", size = 4219700, upload-time = "2026-04-08T01:57:40.625Z" }, + { url = "https://files.pythonhosted.org/packages/6c/7b/1c55db7242b5e5612b29fc7a630e91ee7a6e3c8e7bf5406d22e206875fbd/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", size = 4385982, upload-time = "2026-04-08T01:57:42.725Z" }, + { url = "https://files.pythonhosted.org/packages/cb/da/9870eec4b69c63ef5925bf7d8342b7e13bc2ee3d47791461c4e49ca212f4/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", size = 4219115, upload-time = "2026-04-08T01:57:44.939Z" }, + { url = "https://files.pythonhosted.org/packages/f4/72/05aa5832b82dd341969e9a734d1812a6aadb088d9eb6f0430fc337cc5a8f/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", size = 4385479, upload-time = "2026-04-08T01:57:46.86Z" }, + { url = "https://files.pythonhosted.org/packages/20/2a/1b016902351a523aa2bd446b50a5bc1175d7a7d1cf90fe2ef904f9b84ebc/cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", size = 3412829, upload-time = "2026-04-08T01:57:48.874Z" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "darabonba-core" +version = "1.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "alibabacloud-tea" }, + { name = "requests" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/d3/a7daaee544c904548e665829b51a9fa2572acb82c73ad787a8ff90273002/darabonba_core-1.0.5-py3-none-any.whl", hash = "sha256:671ab8dbc4edc2a8f88013da71646839bb8914f1259efc069353243ef52ea27c", size = 24580, upload-time = "2025-12-12T07:53:59.494Z" }, +] + +[[package]] +name = "dash" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flask" }, + { name = "importlib-metadata" }, + { name = "nest-asyncio" }, + { name = "plotly" }, + { name = "requests" }, + { name = "retrying" }, + { name = "setuptools" }, + { name = "typing-extensions" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/7c/8569c50a4c07fabee6b2428a1515c0c92247e2127367b438a9f99d69723c/dash-3.1.1.tar.gz", hash = "sha256:916b31cec46da0a3339da0e9df9f446126aa7f293c0544e07adf9fe4ba060b18", size = 7544441, upload-time = "2025-06-30T15:31:30.518Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/df/267614cbc1003f6982d7078fd5c7591778f75e07bf36d4771fcb2eab8ff1/dash-3.1.1-py3-none-any.whl", hash = "sha256:66fff37e79c6aa114cd55aea13683d1e9afe0e3f96b35388baca95ff6cfdad23", size = 7885616, upload-time = "2025-06-30T15:31:22.768Z" }, +] + +[[package]] +name = "dash-bootstrap-components" +version = "2.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dash" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/8d/0f641e7c7878ac65b4bb78a2c7cb707db036f82da13fd61948adec44d5aa/dash_bootstrap_components-2.0.3.tar.gz", hash = "sha256:5c161b04a6e7ed19a7d54e42f070c29fd6c385d5a7797e7a82999aa2fc15b1de", size = 115466, upload-time = "2025-05-22T22:30:18.02Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/f6/b4652aacfbc8d684c9ca8efc5178860a50b54abf82cd1960013c59f8258f/dash_bootstrap_components-2.0.3-py3-none-any.whl", hash = "sha256:82754d3d001ad5482b8a82b496c7bf98a1c68d2669d607a89dda7ec627304af5", size = 203706, upload-time = "2025-05-22T22:30:16.304Z" }, +] + +[[package]] +name = "debugpy" +version = "1.8.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/b7/cd8080344452e4874aae67c40d8940e2b4d47b01601a8fd9f44786c757c7/debugpy-1.8.20.tar.gz", hash = "sha256:55bc8701714969f1ab89a6d5f2f3d40c36f91b2cbe2f65d98bf8196f6a6a2c33", size = 1645207, upload-time = "2026-01-29T23:03:28.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/56/c3baf5cbe4dd77427fd9aef99fcdade259ad128feeb8a786c246adb838e5/debugpy-1.8.20-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:eada6042ad88fa1571b74bd5402ee8b86eded7a8f7b827849761700aff171f1b", size = 2208318, upload-time = "2026-01-29T23:03:36.481Z" }, + { url = "https://files.pythonhosted.org/packages/9a/7d/4fa79a57a8e69fe0d9763e98d1110320f9ecd7f1f362572e3aafd7417c9d/debugpy-1.8.20-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:7de0b7dfeedc504421032afba845ae2a7bcc32ddfb07dae2c3ca5442f821c344", size = 3171493, upload-time = "2026-01-29T23:03:37.775Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f2/1e8f8affe51e12a26f3a8a8a4277d6e60aa89d0a66512f63b1e799d424a4/debugpy-1.8.20-cp311-cp311-win32.whl", hash = "sha256:773e839380cf459caf73cc533ea45ec2737a5cc184cf1b3b796cd4fd98504fec", size = 5209240, upload-time = "2026-01-29T23:03:39.109Z" }, + { url = "https://files.pythonhosted.org/packages/d5/92/1cb532e88560cbee973396254b21bece8c5d7c2ece958a67afa08c9f10dc/debugpy-1.8.20-cp311-cp311-win_amd64.whl", hash = "sha256:1f7650546e0eded1902d0f6af28f787fa1f1dbdbc97ddabaf1cd963a405930cb", size = 5233481, upload-time = "2026-01-29T23:03:40.659Z" }, + { url = "https://files.pythonhosted.org/packages/14/57/7f34f4736bfb6e00f2e4c96351b07805d83c9a7b33d28580ae01374430f7/debugpy-1.8.20-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:4ae3135e2089905a916909ef31922b2d733d756f66d87345b3e5e52b7a55f13d", size = 2550686, upload-time = "2026-01-29T23:03:42.023Z" }, + { url = "https://files.pythonhosted.org/packages/ab/78/b193a3975ca34458f6f0e24aaf5c3e3da72f5401f6054c0dfd004b41726f/debugpy-1.8.20-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:88f47850a4284b88bd2bfee1f26132147d5d504e4e86c22485dfa44b97e19b4b", size = 4310588, upload-time = "2026-01-29T23:03:43.314Z" }, + { url = "https://files.pythonhosted.org/packages/c1/55/f14deb95eaf4f30f07ef4b90a8590fc05d9e04df85ee379712f6fb6736d7/debugpy-1.8.20-cp312-cp312-win32.whl", hash = "sha256:4057ac68f892064e5f98209ab582abfee3b543fb55d2e87610ddc133a954d390", size = 5331372, upload-time = "2026-01-29T23:03:45.526Z" }, + { url = "https://files.pythonhosted.org/packages/a1/39/2bef246368bd42f9bd7cba99844542b74b84dacbdbea0833e610f384fee8/debugpy-1.8.20-cp312-cp312-win_amd64.whl", hash = "sha256:a1a8f851e7cf171330679ef6997e9c579ef6dd33c9098458bd9986a0f4ca52e3", size = 5372835, upload-time = "2026-01-29T23:03:47.245Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl", hash = "sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7", size = 5337658, upload-time = "2026-01-29T23:04:17.404Z" }, +] + +[[package]] +name = "decorator" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + +[[package]] +name = "detect-secrets" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/67/382a863fff94eae5a0cf05542179169a1c49a4c8784a9480621e2066ca7d/detect_secrets-1.5.0.tar.gz", hash = "sha256:6bb46dcc553c10df51475641bb30fd69d25645cc12339e46c824c1e0c388898a", size = 97351, upload-time = "2024-05-06T17:46:19.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/5e/4f5fe4b89fde1dc3ed0eb51bd4ce4c0bca406246673d370ea2ad0c58d747/detect_secrets-1.5.0-py3-none-any.whl", hash = "sha256:e24e7b9b5a35048c313e983f76c4bd09dad89f045ff059e354f9943bf45aa060", size = 120341, upload-time = "2024-05-06T17:46:16.628Z" }, +] + +[[package]] +name = "dill" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/e1/56027a71e31b02ddc53c7d65b01e68edf64dea2932122fe7746a516f75d5/dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa", size = 187315, upload-time = "2026-01-19T02:36:56.85Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "dj-rest-auth" +version = "7.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "djangorestframework" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/19/00150c8bedf7b6d4c44ecf7c2be9e58ae2203b42741ca734152d34f549f1/dj-rest-auth-7.0.1.tar.gz", hash = "sha256:3f8c744cbcf05355ff4bcbef0c8a63645da38e29a0fdef3c3332d4aced52fb90", size = 220541, upload-time = "2025-01-04T23:37:38.688Z" } + +[package.optional-dependencies] +with-social = [ + { name = "django-allauth", extra = ["socialaccount"] }, +] + +[[package]] +name = "django" +version = "5.1.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "sqlparse" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/45/1ac68964193cfcc0b0912a0f68025d5bdb54f71ba7b8716e85b959874bd0/django-5.1.15.tar.gz", hash = "sha256:46a356b5ff867bece73fc6365e081f21c569973403ee7e9b9a0316f27d0eb947", size = 10719662, upload-time = "2025-12-02T14:01:31.931Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/79/372e091f0eba4ddb8228245ccd1baaa140e9658711f5e3a0056e540b4c1e/django-5.1.15-py3-none-any.whl", hash = "sha256:117871e58d6eda37f09870b7d73a3d66567b03aecd515b386b1751177c413432", size = 8260901, upload-time = "2025-12-02T14:01:27.352Z" }, +] + +[[package]] +name = "django-allauth" +version = "65.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/c1/d3385f4c3169c1d6eea3c63aed0f36af51478c1d72e46db12bb1a08f8034/django_allauth-65.15.0.tar.gz", hash = "sha256:b404d48cf0c3ee14dacc834c541f30adedba2ff1c433980ecc494d6cb0b395a8", size = 2215709, upload-time = "2026-03-09T13:51:28.675Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/b8/c8411339171bd8bc075c09ef190fb42195e9a2149e5c5026e094fe62fce0/django_allauth-65.15.0-py3-none-any.whl", hash = "sha256:ad9fc49c49a9368eaa5bb95456b76e2a4f377b3c6862ee8443507816578c098d", size = 2022994, upload-time = "2026-03-09T13:51:19.711Z" }, +] + +[package.optional-dependencies] +saml = [ + { name = "python3-saml" }, +] +socialaccount = [ + { name = "oauthlib" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "requests" }, +] + +[[package]] +name = "django-celery-beat" +version = "2.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "celery" }, + { name = "cron-descriptor" }, + { name = "django" }, + { name = "django-timezone-field" }, + { name = "python-crontab" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/45/fc97bc1d9af8e7dc07f1e37044d9551a30e6793249864cef802341e2e3a8/django_celery_beat-2.9.0.tar.gz", hash = "sha256:92404650f52fcb44cf08e2b09635cb1558327c54b1a5d570f0e2d3a22130934c", size = 177667, upload-time = "2026-02-28T16:45:34.749Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/ae/9befa7ae37f5e5c41be636a254fcf47ff30dd5c88bd115070e252f6b9162/django_celery_beat-2.9.0-py3-none-any.whl", hash = "sha256:4a9e5ebe26d6f8d7215e1fc5c46e466016279dc102435a28141108649bdf2157", size = 105013, upload-time = "2026-02-28T16:45:32.822Z" }, +] + +[[package]] +name = "django-celery-results" +version = "2.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "celery" }, + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/b5/9966c28e31014c228305e09d48b19b35522a8f941fe5af5f81f40dc8fa80/django_celery_results-2.6.0.tar.gz", hash = "sha256:9abcd836ae6b61063779244d8887a88fe80bbfaba143df36d3cb07034671277c", size = 83985, upload-time = "2025-04-10T08:23:52.677Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/da/70f0f3c5364735344c4bc89e53413bcaae95b4fc1de4e98a7a3b9fb70c88/django_celery_results-2.6.0-py3-none-any.whl", hash = "sha256:b9ccdca2695b98c7cbbb8dea742311ba9a92773d71d7b4944a676e69a7df1c73", size = 38351, upload-time = "2025-04-10T08:23:49.965Z" }, +] + +[[package]] +name = "django-cors-headers" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/34/f0c7a7241f885cbfc99b1edef0acc7915dd7a3fb749fe27de5e8a9fb2ccb/django_cors_headers-4.4.0.tar.gz", hash = "sha256:92cf4633e22af67a230a1456cb1b7a02bb213d6536d2dcb2a4a24092ea9cebc2", size = 21151, upload-time = "2024-06-19T16:16:45.833Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/0c/4201d5650199b3a36ef3f2ab91f44c4527a70685f3003ce9f3ed8c30780c/django_cors_headers-4.4.0-py3-none-any.whl", hash = "sha256:5c6e3b7fe870876a1efdfeb4f433782c3524078fa0dc9e0195f6706ce7a242f6", size = 12789, upload-time = "2024-06-19T16:16:42.189Z" }, +] + +[[package]] +name = "django-environ" +version = "0.11.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/0b/f2c024529ee4bbf8b95176eebeb86c6e695192a9ce0e91059cb83a33c1d3/django-environ-0.11.2.tar.gz", hash = "sha256:f32a87aa0899894c27d4e1776fa6b477e8164ed7f6b3e410a62a6d72caaf64be", size = 54326, upload-time = "2023-09-01T21:03:02.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/f1/468b49cccba3b42dda571063a14c668bb0b53a1d5712426d18e36663bd53/django_environ-0.11.2-py2.py3-none-any.whl", hash = "sha256:0ff95ab4344bfeff693836aa978e6840abef2e2f1145adff7735892711590c05", size = 19141, upload-time = "2023-09-01T21:02:59.88Z" }, +] + +[[package]] +name = "django-filter" +version = "24.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/bc/dc19ae39c235332926dd0efe0951f663fa1a9fc6be8430737ff7fd566b20/django_filter-24.3.tar.gz", hash = "sha256:d8ccaf6732afd21ca0542f6733b11591030fa98669f8d15599b358e24a2cd9c3", size = 144444, upload-time = "2024-08-02T13:27:58.132Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/b1/92f1c30b47c1ebf510c35a2ccad9448f73437e5891bbd2b4febe357cc3de/django_filter-24.3-py3-none-any.whl", hash = "sha256:c4852822928ce17fb699bcfccd644b3574f1a2d80aeb2b4ff4f16b02dd49dc64", size = 95011, upload-time = "2024-08-02T13:27:55.616Z" }, +] + +[[package]] +name = "django-guid" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/12/629715b6aefc76b3626934614e5139e7cf4be76e3a5cc07dedb57416fdd2/django_guid-3.5.0.tar.gz", hash = "sha256:5f32f70287e4f36addc79f29f2a7b2f56fc5f4e4cfb2023141525be8baa35d9e", size = 14111, upload-time = "2024-04-25T15:48:53.603Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/34/2a8c3635e096ddc311c264a8aa34afb7df7118b872dfc06b56b22ea476e5/django_guid-3.5.0-py3-none-any.whl", hash = "sha256:28f52cfeac47e8e22ea889a3845bc2b1c604dd842e495dadd44ad5184db72c76", size = 19686, upload-time = "2024-04-25T15:48:51.619Z" }, +] + +[[package]] +name = "django-postgres-extra" +version = "2.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/b0/0275caa2ced7a3eae3ccbe7fefcc062374e76edfcd2bf882840069f08ea3/django_postgres_extra-2.0.9.tar.gz", hash = "sha256:9e47c436d033712d0e2611b8c1583566dd4f97700e5360001bf3623913a92046", size = 92055, upload-time = "2025-07-11T07:19:40.045Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/0d/2e611d8977b6dad077296b6880e8ee2d7d320e55f834601c6ad5eb87b982/django_postgres_extra-2.0.9-py3-none-any.whl", hash = "sha256:0a4f9fd7f843e2ef5cfe7a291fcb883bd72d2ca8e4b369d0070866205e00b404", size = 90512, upload-time = "2025-07-11T07:19:39.035Z" }, +] + +[[package]] +name = "django-silk" +version = "5.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "autopep8" }, + { name = "django" }, + { name = "gprof2dot" }, + { name = "sqlparse" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/9f/ac0bf2f1bba661a9f8937bccd0b8dd93ba87b6b1ebbf3884981f5e27c2b0/django_silk-5.3.2.tar.gz", hash = "sha256:b0db54eebedb8d16f572321bd6daccac0bd3f547ae2618bb45d96fe8fc02229d", size = 4493706, upload-time = "2024-12-14T08:06:03.225Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/4d/92872928197099aefad0a3e4a055e6c22295e7770dd7b3388a6f2909c95c/django_silk-5.3.2-py3-none-any.whl", hash = "sha256:49f1caebfda28b1707f0cfef524e0476beb82b8c5e40f5ccff7f73a6b4f6d3ac", size = 1943114, upload-time = "2024-12-14T08:06:21.512Z" }, +] + +[[package]] +name = "django-timezone-field" +version = "7.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/05/9b93a66452cdb8a08ab26f08d5766d2332673e659a8b2aeb73f2a904d421/django_timezone_field-7.2.1.tar.gz", hash = "sha256:def846f9e7200b7b8f2a28fcce2b78fb2d470f6a9f272b07c4e014f6ba4c6d2e", size = 13096, upload-time = "2025-12-06T23:50:44.591Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/7f/d885667401515b467f84569c56075bc9add72c9fd425fca51a25f4c997e1/django_timezone_field-7.2.1-py3-none-any.whl", hash = "sha256:276915b72c5816f57c3baf9e43f816c695ef940d1b21f91ebf6203c09bf4ad44", size = 13284, upload-time = "2025-12-06T23:50:43.302Z" }, +] + +[[package]] +name = "djangorestframework" +version = "3.15.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/ce/31482eb688bdb4e271027076199e1aa8d02507e530b6d272ab8b4481557c/djangorestframework-3.15.2.tar.gz", hash = "sha256:36fe88cd2d6c6bec23dca9804bab2ba5517a8bb9d8f47ebc68981b56840107ad", size = 1067420, upload-time = "2024-06-19T07:59:32.891Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/b6/fa99d8f05eff3a9310286ae84c4059b08c301ae4ab33ae32e46e8ef76491/djangorestframework-3.15.2-py3-none-any.whl", hash = "sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20", size = 1071235, upload-time = "2024-06-19T07:59:26.106Z" }, +] + +[[package]] +name = "djangorestframework-jsonapi" +version = "7.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "djangorestframework" }, + { name = "inflection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/d5/5d1ce5edd01abdd4636189aea4f0aad0318ba0485bfa47880276b92e9e0c/djangorestframework-jsonapi-7.0.2.tar.gz", hash = "sha256:d6c72a2bee539f1093dd86620e862af2d1a0e60408e38a710146286dbde71d75", size = 130641, upload-time = "2024-06-28T15:17:43.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/6a/234c0aebb841345db887c2c94a325789cce72de9eb3742a0a85ea0481bc0/djangorestframework_jsonapi-7.0.2-py2.py3-none-any.whl", hash = "sha256:be457adb50aac77eec8893048bf46ad6926dcd26204aa10965a1430610828d50", size = 354734, upload-time = "2024-06-28T15:17:40.575Z" }, +] + +[[package]] +name = "djangorestframework-simplejwt" +version = "5.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "djangorestframework" }, + { name = "pyjwt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/27/2874a325c11112066139769f7794afae238a07ce6adf96259f08fd37a9d7/djangorestframework_simplejwt-5.5.1.tar.gz", hash = "sha256:e72c5572f51d7803021288e2057afcbd03f17fe11d484096f40a460abc76e87f", size = 101265, upload-time = "2025-07-21T16:52:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/94/fdfb7b2f0b16cd3ed4d4171c55c1c07a2d1e3b106c5978c8ad0c15b4a48b/djangorestframework_simplejwt-5.5.1-py3-none-any.whl", hash = "sha256:2c30f3707053d384e9f315d11c2daccfcb548d4faa453111ca19a542b732e469", size = 107674, upload-time = "2025-07-21T16:52:07.493Z" }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "docker" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834, upload-time = "2024-05-23T11:13:57.216Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774, upload-time = "2024-05-23T11:13:55.01Z" }, +] + +[[package]] +name = "dogpile-cache" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "decorator" }, + { name = "stevedore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/c8/301ff89746e76745b937606df4753c032787c59ecb37dd4d4250bddc8929/dogpile_cache-1.5.0.tar.gz", hash = "sha256:849c5573c9a38f155cd4173103c702b637ede0361c12e864876877d0cd125eec", size = 947962, upload-time = "2025-10-11T17:35:36.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/80/12235e5b75bb2c586733280854f131b86051e0bbdfb55349ff70d0f72cf9/dogpile_cache-1.5.0-py3-none-any.whl", hash = "sha256:dc7b47d37844db15e8fdc0243c1b58857a2ddc52a5118237a97127bac200e18d", size = 64447, upload-time = "2025-10-11T17:35:38.573Z" }, +] + +[[package]] +name = "drf-extensions" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "djangorestframework" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/5a/57e3cc6f2bb4e1e84ad85eed0a12ddf22d5217b6b87c39e611d10e677a63/drf_extensions-0.8.0.tar.gz", hash = "sha256:c3f27bca74a2def53e8454a5c7b327595195df51e121743120b2f51ef5a52aaa", size = 173718, upload-time = "2025-04-10T07:50:05.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/c1/b9721c9239b465c0ceac32b33e732c08f6ea0afd8156700e417416a47733/drf_extensions-0.8.0-py2.py3-none-any.whl", hash = "sha256:ab7bd854c9061c27ab55233b66d758001e5c2d81aaa9d117cbbe1c9ea49c77ab", size = 21589, upload-time = "2025-04-10T07:50:02.573Z" }, +] + +[[package]] +name = "drf-nested-routers" +version = "0.95.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "djangorestframework" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/b2/070908ccd41bd6f025f5507d7f6e50009abe8be48393cc697d159b539f0b/drf_nested_routers-0.95.0.tar.gz", hash = "sha256:815978f802e578fd7035c74040c104909cbe97615de89a275d77e928f4029891", size = 23318, upload-time = "2025-09-09T02:01:58.201Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/61/69d951a6e8031389f5feade38531432325019e1047b23d5c6036a86c5e8f/drf_nested_routers-0.95.0-py2.py3-none-any.whl", hash = "sha256:dd489c33d667aaa81383ffaa8c74781d2b353d8f0795716ae37fc59ee297b7c4", size = 36407, upload-time = "2025-09-09T02:01:56.999Z" }, +] + +[[package]] +name = "drf-simple-apikey" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "django" }, + { name = "djangorestframework" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/fb/2829a2053fc4f545f20a41630f77475beaf22359f25a84fc0528cb924cc1/drf_simple_apikey-2.2.1.tar.gz", hash = "sha256:e5a52804bbac12c8db80c10a3d51a8514fc59fc8385b5e751099a2bc944ad25d", size = 24832, upload-time = "2025-05-10T18:35:55.132Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/d8/51c883e850f0565cb21d54686c695f4601a2b60f4885b5604141008fd530/drf_simple_apikey-2.2.1-py2.py3-none-any.whl", hash = "sha256:2a60b35676d14f907c47dee179dd0fa7425a84c34d6ff5b48d08d3b87ff32809", size = 26877, upload-time = "2025-05-10T18:35:53.889Z" }, +] + +[[package]] +name = "drf-spectacular" +version = "0.27.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "djangorestframework" }, + { name = "inflection" }, + { name = "jsonschema" }, + { name = "pyyaml" }, + { name = "uritemplate" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/c9/55311dcbdae9a437eeb8f898f8421a6a3eabf08725c23ddf458cf2479b78/drf-spectacular-0.27.2.tar.gz", hash = "sha256:a199492f2163c4101055075ebdbb037d59c6e0030692fc83a1a8c0fc65929981", size = 235131, upload-time = "2024-04-01T18:00:21.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/cd/84c44a5d435f6544e58a9b138305f59bca232157ae4ecb658f9787f87d1c/drf_spectacular-0.27.2-py3-none-any.whl", hash = "sha256:b1c04bf8b2fbbeaf6f59414b4ea448c8787aba4d32f76055c3b13335cf7ec37b", size = 102930, upload-time = "2024-04-01T18:00:17.937Z" }, +] + +[[package]] +name = "drf-spectacular-jsonapi" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "djangorestframework" }, + { name = "djangorestframework-jsonapi" }, + { name = "drf-extensions" }, + { name = "drf-spectacular" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/a7/198704b65b5a0682ce5d4cc027113aa64faee33ba7f05e54a6e81013cfdf/drf-spectacular-jsonapi-0.5.1.tar.gz", hash = "sha256:e45f87f3cce2692f4f546e0785d8fcc32c6b49770fff858065c267ae8f9cfde5", size = 15718, upload-time = "2024-09-09T13:32:37.206Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/90/a2968757dac52d172ca48b0814c72513fc2e9653980b1a3df16b56ee3b8a/drf_spectacular_jsonapi-0.5.1-py3-none-any.whl", hash = "sha256:abac728abd83e2544408cc900d682d532ca2088f2f9321d1c9101bcdfdabca78", size = 13526, upload-time = "2024-09-09T13:32:36.227Z" }, +] + +[[package]] +name = "dulwich" +version = "0.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/ac/ba58cf420640c7bc77ae8e1b31e174d83c9117750c63cf9ea3b5e202e5c4/dulwich-0.23.0.tar.gz", hash = "sha256:0aa6c2489dd5e978b27e9b75983b7331a66c999f0efc54ebe37cab808ed322ae", size = 575116, upload-time = "2025-06-21T17:56:47.494Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/11/f6bbba8583f69cf19ef4bd7f5fde1a6b5ccaf8b6951781cec8db247116f4/dulwich-0.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d68498fdda13ab00791b483daab3bcfe9f9721c037aa458695e6ad81640c57cc", size = 972658, upload-time = "2025-06-21T17:56:13.505Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9d/2720e0ab58666378a33c752a61543f936cd6b06dfe5d84a2215ddc0914b0/dulwich-0.23.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:cb7bb930b12471a1cfcea4b3d25a671dc0ad32573f0ad25684684298959a1527", size = 1049813, upload-time = "2025-06-21T17:56:14.884Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f3/81d8075141dfcc0a0449c2093596e58d3e11444e3af54e819eca63b84dd0/dulwich-0.23.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a2abbce32fd2bc7902bcc5f69b10bf22576810de21651baaa864b78fd7aec261", size = 1051639, upload-time = "2025-06-21T17:56:16.437Z" }, + { url = "https://files.pythonhosted.org/packages/4f/0d/c06ccb227b096aef5906142fe78b5c79f9070a0ea6152fc219941186d540/dulwich-0.23.0-cp311-cp311-win32.whl", hash = "sha256:9e3151f10ce2a9ff91bca64c74345217f53bdd947dc958032343822009832f7a", size = 642918, upload-time = "2025-06-21T17:56:18.373Z" }, + { url = "https://files.pythonhosted.org/packages/d7/1c/1e99aa34c9aead9e641b2d9934f0a3d00257f75027cf5cdecc8a1a6c18ae/dulwich-0.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:3ae9f1d9dc92d4e9a3f89ba2c55221f7b6442c5dd93b3f6f539a3c9eb3f37bdd", size = 659010, upload-time = "2025-06-21T17:56:19.947Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d7/1e6fba0235babe912e8467b036062e37d11672cbbeb0d8074f9d4559057b/dulwich-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:52cdef66a7994d29528ca79ca59452518bbba3fd56a9c61c61f6c467c1c7956e", size = 960292, upload-time = "2025-06-21T17:56:21.308Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6a/23f0c487ec03f2752600cab4a8e0dedb38186246c475bf3fa90a8db830d5/dulwich-0.23.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:d473888a6ab9ed5d4a4c3f053cbe5b77f72d54b6efdf5688fed76094316e571e", size = 1047892, upload-time = "2025-06-21T17:56:22.989Z" }, + { url = "https://files.pythonhosted.org/packages/c7/e2/8f3d216be5fd0ee1180d917b59b34b54b9896384cf139f319b5d3a8f16b4/dulwich-0.23.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:19fcf20224c641a61c774da92f098fbaae9938c7e17a52841e64092adf7e78f9", size = 1048699, upload-time = "2025-06-21T17:56:24.602Z" }, + { url = "https://files.pythonhosted.org/packages/8f/c4/18e6223cd4ad1ae9334eb4e6aa5952fd8f5c3d75762918eb90c209fec4ba/dulwich-0.23.0-cp312-cp312-win32.whl", hash = "sha256:7fc8b76b704ef35cd001e993e3aa4e1d666a2064bf467c07c560f12b2959dcaf", size = 641268, upload-time = "2025-06-21T17:56:26.18Z" }, + { url = "https://files.pythonhosted.org/packages/b8/9c/65bfbbac62d8a2967e13f6a1512371c5eb6b906a61fb6dead992669cad0e/dulwich-0.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:cb0566b888b578325350b4d67c61a0de35d417e9877560e3a6df88cae4576a59", size = 657837, upload-time = "2025-06-21T17:56:27.821Z" }, + { url = "https://files.pythonhosted.org/packages/35/31/49318ee9db4b402e6d8b9b01bd4cae9298f59e1bb9bd56cf4a94e48fa069/dulwich-0.23.0-py3-none-any.whl", hash = "sha256:d8da6694ca332bb48775e35ee2215aa4673821164a91b83062f699c69f7cd135", size = 313776, upload-time = "2025-06-21T17:56:46.221Z" }, +] + +[[package]] +name = "duo-client" +version = "5.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/c8/4e8416eec72bc77ad947b736f760ceedf818df0c48153816b1bfb0fab854/duo_client-5.5.0.tar.gz", hash = "sha256:303109e047fe7525ba4fc4a294c1f3deb4125066e89c10d33f7430378867b1d6", size = 93475, upload-time = "2025-03-24T15:35:19.175Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/4b/dd927d3111e1f4c78c1f5944d3b64b4fb6ab655487c23100a3feff5cdaa4/duo_client-5.5.0-py3-none-any.whl", hash = "sha256:4fbf1e97a2b25ef64e9f88171ab817162cf45bafc1c63026af4883baf8892a12", size = 51313, upload-time = "2025-03-24T15:35:18.243Z" }, +] + +[[package]] +name = "durationpy" +version = "0.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/a4/e44218c2b394e31a6dd0d6b095c4e1f32d0be54c2a4b250032d717647bab/durationpy-0.10.tar.gz", hash = "sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba", size = 3335, upload-time = "2025-05-17T13:52:37.26Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/0d/9feae160378a3553fa9a339b0e9c1a048e147a4127210e286ef18b730f03/durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286", size = 3922, upload-time = "2025-05-17T13:52:36.463Z" }, +] + +[[package]] +name = "email-validator" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/ce/13508a1ec3f8bb981ae4ca79ea40384becc868bfae97fd1c942bb3a001b1/email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7", size = 48967, upload-time = "2024-06-20T11:30:30.034Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/ee/bf0adb559ad3c786f12bcbc9296b3f5675f529199bef03e2df281fa1fadb/email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631", size = 33521, upload-time = "2024-06-20T11:30:28.248Z" }, +] + +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + +[[package]] +name = "filelock" +version = "3.20.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, +] + +[[package]] +name = "flask" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" }, +] + +[[package]] +name = "fonttools" +version = "4.62.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/08/7012b00a9a5874311b639c3920270c36ee0c445b69d9989a85e5c92ebcb0/fonttools-4.62.1.tar.gz", hash = "sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d", size = 3580737, upload-time = "2026-03-13T13:54:25.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/23ff32561ec8d45a4d48578b4d241369d9270dc50926c017570e60893701/fonttools-4.62.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:40975849bac44fb0b9253d77420c6d8b523ac4dcdcefeff6e4d706838a5b80f7", size = 2871039, upload-time = "2026-03-13T13:52:33.127Z" }, + { url = "https://files.pythonhosted.org/packages/24/7f/66d3f8a9338a9b67fe6e1739f47e1cd5cee78bd3bc1206ef9b0b982289a5/fonttools-4.62.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9dde91633f77fa576879a0c76b1d89de373cae751a98ddf0109d54e173b40f14", size = 2416346, upload-time = "2026-03-13T13:52:35.676Z" }, + { url = "https://files.pythonhosted.org/packages/aa/53/5276ceba7bff95da7793a07c5284e1da901cf00341ce5e2f3273056c0cca/fonttools-4.62.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6acb4109f8bee00fec985c8c7afb02299e35e9c94b57287f3ea542f28bd0b0a7", size = 5100897, upload-time = "2026-03-13T13:52:38.102Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a1/40a5c4d8e28b0851d53a8eeeb46fbd73c325a2a9a165f290a5ed90e6c597/fonttools-4.62.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1c5c25671ce8805e0d080e2ffdeca7f1e86778c5cbfbeae86d7f866d8830517b", size = 5071078, upload-time = "2026-03-13T13:52:41.305Z" }, + { url = "https://files.pythonhosted.org/packages/e3/be/d378fca4c65ea1956fee6d90ace6e861776809cbbc5af22388a090c3c092/fonttools-4.62.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a5d8825e1140f04e6c99bb7d37a9e31c172f3bc208afbe02175339e699c710e1", size = 5076908, upload-time = "2026-03-13T13:52:44.122Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d9/ae6a1d0693a4185a84605679c8a1f719a55df87b9c6e8e817bfdd9ef5936/fonttools-4.62.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:268abb1cb221e66c014acc234e872b7870d8b5d4657a83a8f4205094c32d2416", size = 5202275, upload-time = "2026-03-13T13:52:46.591Z" }, + { url = "https://files.pythonhosted.org/packages/54/6c/af95d9c4efb15cabff22642b608342f2bd67137eea6107202d91b5b03184/fonttools-4.62.1-cp311-cp311-win32.whl", hash = "sha256:942b03094d7edbb99bdf1ae7e9090898cad7bf9030b3d21f33d7072dbcb51a53", size = 2293075, upload-time = "2026-03-13T13:52:48.711Z" }, + { url = "https://files.pythonhosted.org/packages/d3/97/bf54c5b3f2be34e1f143e6db838dfdc54f2ffa3e68c738934c82f3b2a08d/fonttools-4.62.1-cp311-cp311-win_amd64.whl", hash = "sha256:e8514f4924375f77084e81467e63238b095abda5107620f49421c368a6017ed2", size = 2344593, upload-time = "2026-03-13T13:52:50.725Z" }, + { url = "https://files.pythonhosted.org/packages/47/d4/dbacced3953544b9a93088cc10ef2b596d348c983d5c67a404fa41ec51ba/fonttools-4.62.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:90365821debbd7db678809c7491ca4acd1e0779b9624cdc6ddaf1f31992bf974", size = 2870219, upload-time = "2026-03-13T13:52:53.664Z" }, + { url = "https://files.pythonhosted.org/packages/66/9e/a769c8e99b81e5a87ab7e5e7236684de4e96246aae17274e5347d11ebd78/fonttools-4.62.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12859ff0b47dd20f110804c3e0d0970f7b832f561630cd879969011541a464a9", size = 2414891, upload-time = "2026-03-13T13:52:56.493Z" }, + { url = "https://files.pythonhosted.org/packages/69/64/f19a9e3911968c37e1e620e14dfc5778299e1474f72f4e57c5ec771d9489/fonttools-4.62.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c125ffa00c3d9003cdaaf7f2c79e6e535628093e14b5de1dccb08859b680936", size = 5033197, upload-time = "2026-03-13T13:52:59.179Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8a/99c8b3c3888c5c474c08dbfd7c8899786de9604b727fcefb055b42c84bba/fonttools-4.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:149f7d84afca659d1a97e39a4778794a2f83bf344c5ee5134e09995086cc2392", size = 4988768, upload-time = "2026-03-13T13:53:02.761Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c6/0f904540d3e6ab463c1243a0d803504826a11604c72dd58c2949796a1762/fonttools-4.62.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0aa72c43a601cfa9273bb1ae0518f1acadc01ee181a6fc60cd758d7fdadffc04", size = 4971512, upload-time = "2026-03-13T13:53:05.678Z" }, + { url = "https://files.pythonhosted.org/packages/29/0b/5cbef6588dc9bd6b5c9ad6a4d5a8ca384d0cea089da31711bbeb4f9654a6/fonttools-4.62.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:19177c8d96c7c36359266e571c5173bcee9157b59cfc8cb0153c5673dc5a3a7d", size = 5122723, upload-time = "2026-03-13T13:53:08.662Z" }, + { url = "https://files.pythonhosted.org/packages/4a/47/b3a5342d381595ef439adec67848bed561ab7fdb1019fa522e82101b7d9c/fonttools-4.62.1-cp312-cp312-win32.whl", hash = "sha256:a24decd24d60744ee8b4679d38e88b8303d86772053afc29b19d23bb8207803c", size = 2281278, upload-time = "2026-03-13T13:53:10.998Z" }, + { url = "https://files.pythonhosted.org/packages/28/b1/0c2ab56a16f409c6c8a68816e6af707827ad5d629634691ff60a52879792/fonttools-4.62.1-cp312-cp312-win_amd64.whl", hash = "sha256:9e7863e10b3de72376280b515d35b14f5eeed639d1aa7824f4cf06779ec65e42", size = 2331414, upload-time = "2026-03-13T13:53:13.992Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ba/56147c165442cc5ba7e82ecf301c9a68353cede498185869e6e02b4c264f/fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd", size = 1152647, upload-time = "2026-03-13T13:54:22.735Z" }, +] + +[[package]] +name = "freezegun" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/ef/722b8d71ddf4d48f25f6d78aa2533d505bf3eec000a7cacb8ccc8de61f2f/freezegun-1.5.1.tar.gz", hash = "sha256:b29dedfcda6d5e8e083ce71b2b542753ad48cfec44037b3fc79702e2980a89e9", size = 33697, upload-time = "2024-05-11T17:32:53.911Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/0b/0d7fee5919bccc1fdc1c2a7528b98f65c6f69b223a3fd8f809918c142c36/freezegun-1.5.1-py3-none-any.whl", hash = "sha256:bf111d7138a8abe55ab48a71755673dbaa4ab87f4cff5634a4442dfec34c15f1", size = 17569, upload-time = "2024-05-11T17:32:51.715Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, + { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, + { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, + { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "gevent" +version = "25.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation == 'CPython' and sys_platform == 'win32'" }, + { name = "greenlet", marker = "platform_python_implementation == 'CPython'" }, + { name = "zope-event" }, + { name = "zope-interface" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/48/b3ef2673ffb940f980966694e40d6d32560f3ffa284ecaeb5ea3a90a6d3f/gevent-25.9.1.tar.gz", hash = "sha256:adf9cd552de44a4e6754c51ff2e78d9193b7fa6eab123db9578a210e657235dd", size = 5059025, upload-time = "2025-09-17T16:15:34.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/86/03f8db0704fed41b0fa830425845f1eb4e20c92efa3f18751ee17809e9c6/gevent-25.9.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5aff9e8342dc954adb9c9c524db56c2f3557999463445ba3d9cbe3dada7b7", size = 1792418, upload-time = "2025-09-17T15:41:24.384Z" }, + { url = "https://files.pythonhosted.org/packages/5f/35/f6b3a31f0849a62cfa2c64574bcc68a781d5499c3195e296e892a121a3cf/gevent-25.9.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1cdf6db28f050ee103441caa8b0448ace545364f775059d5e2de089da975c457", size = 1875700, upload-time = "2025-09-17T15:48:59.652Z" }, + { url = "https://files.pythonhosted.org/packages/66/1e/75055950aa9b48f553e061afa9e3728061b5ccecca358cef19166e4ab74a/gevent-25.9.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:812debe235a8295be3b2a63b136c2474241fa5c58af55e6a0f8cfc29d4936235", size = 1831365, upload-time = "2025-09-17T15:49:19.426Z" }, + { url = "https://files.pythonhosted.org/packages/31/e8/5c1f6968e5547e501cfa03dcb0239dff55e44c3660a37ec534e32a0c008f/gevent-25.9.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b28b61ff9216a3d73fe8f35669eefcafa957f143ac534faf77e8a19eb9e6883a", size = 2122087, upload-time = "2025-09-17T15:15:12.329Z" }, + { url = "https://files.pythonhosted.org/packages/c0/2c/ebc5d38a7542af9fb7657bfe10932a558bb98c8a94e4748e827d3823fced/gevent-25.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5e4b6278b37373306fc6b1e5f0f1cf56339a1377f67c35972775143d8d7776ff", size = 1808776, upload-time = "2025-09-17T15:52:40.16Z" }, + { url = "https://files.pythonhosted.org/packages/e6/26/e1d7d6c8ffbf76fe1fbb4e77bdb7f47d419206adc391ec40a8ace6ebbbf0/gevent-25.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d99f0cb2ce43c2e8305bf75bee61a8bde06619d21b9d0316ea190fc7a0620a56", size = 2179141, upload-time = "2025-09-17T15:24:09.895Z" }, + { url = "https://files.pythonhosted.org/packages/1d/6c/bb21fd9c095506aeeaa616579a356aa50935165cc0f1e250e1e0575620a7/gevent-25.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:72152517ecf548e2f838c61b4be76637d99279dbaa7e01b3924df040aa996586", size = 1677941, upload-time = "2025-09-17T19:59:50.185Z" }, + { url = "https://files.pythonhosted.org/packages/f7/49/e55930ba5259629eb28ac7ee1abbca971996a9165f902f0249b561602f24/gevent-25.9.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:46b188248c84ffdec18a686fcac5dbb32365d76912e14fda350db5dc0bfd4f86", size = 2955991, upload-time = "2025-09-17T14:52:30.568Z" }, + { url = "https://files.pythonhosted.org/packages/aa/88/63dc9e903980e1da1e16541ec5c70f2b224ec0a8e34088cb42794f1c7f52/gevent-25.9.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f2b54ea3ca6f0c763281cd3f96010ac7e98c2e267feb1221b5a26e2ca0b9a692", size = 1808503, upload-time = "2025-09-17T15:41:25.59Z" }, + { url = "https://files.pythonhosted.org/packages/7a/8d/7236c3a8f6ef7e94c22e658397009596fa90f24c7d19da11ad7ab3a9248e/gevent-25.9.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7a834804ac00ed8a92a69d3826342c677be651b1c3cd66cc35df8bc711057aa2", size = 1890001, upload-time = "2025-09-17T15:49:01.227Z" }, + { url = "https://files.pythonhosted.org/packages/4f/63/0d7f38c4a2085ecce26b50492fc6161aa67250d381e26d6a7322c309b00f/gevent-25.9.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:323a27192ec4da6b22a9e51c3d9d896ff20bc53fdc9e45e56eaab76d1c39dd74", size = 1855335, upload-time = "2025-09-17T15:49:20.582Z" }, + { url = "https://files.pythonhosted.org/packages/95/18/da5211dfc54c7a57e7432fd9a6ffeae1ce36fe5a313fa782b1c96529ea3d/gevent-25.9.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6ea78b39a2c51d47ff0f130f4c755a9a4bbb2dd9721149420ad4712743911a51", size = 2109046, upload-time = "2025-09-17T15:15:13.817Z" }, + { url = "https://files.pythonhosted.org/packages/a6/5a/7bb5ec8e43a2c6444853c4a9f955f3e72f479d7c24ea86c95fb264a2de65/gevent-25.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:dc45cd3e1cc07514a419960af932a62eb8515552ed004e56755e4bf20bad30c5", size = 1827099, upload-time = "2025-09-17T15:52:41.384Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d4/b63a0a60635470d7d986ef19897e893c15326dd69e8fb342c76a4f07fe9e/gevent-25.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34e01e50c71eaf67e92c186ee0196a039d6e4f4b35670396baed4a2d8f1b347f", size = 2172623, upload-time = "2025-09-17T15:24:12.03Z" }, + { url = "https://files.pythonhosted.org/packages/d5/98/caf06d5d22a7c129c1fb2fc1477306902a2c8ddfd399cd26bbbd4caf2141/gevent-25.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:4acd6bcd5feabf22c7c5174bd3b9535ee9f088d2bbce789f740ad8d6554b18f3", size = 1682837, upload-time = "2025-09-17T19:48:47.318Z" }, +] + +[[package]] +name = "google-api-core" +version = "2.29.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "googleapis-common-protos" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/10/05572d33273292bac49c2d1785925f7bc3ff2fe50e3044cf1062c1dde32e/google_api_core-2.29.0.tar.gz", hash = "sha256:84181be0f8e6b04006df75ddfe728f24489f0af57c96a529ff7cf45bc28797f7", size = 177828, upload-time = "2026-01-08T22:21:39.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/b6/85c4d21067220b9a78cfb81f516f9725ea6befc1544ec9bd2c1acd97c324/google_api_core-2.29.0-py3-none-any.whl", hash = "sha256:d30bc60980daa36e314b5d5a3e5958b0200cb44ca8fa1be2b614e932b75a3ea9", size = 173906, upload-time = "2026-01-08T22:21:36.093Z" }, +] + +[package.optional-dependencies] +grpc = [ + { name = "grpcio" }, + { name = "grpcio-status" }, +] + +[[package]] +name = "google-api-python-client" +version = "2.163.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, + { name = "google-auth-httplib2" }, + { name = "httplib2" }, + { name = "uritemplate" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/9f/535346bb1469ec91139c38f0438ad70bd229a6b11452367065fe49303860/google_api_python_client-2.163.0.tar.gz", hash = "sha256:88dee87553a2d82176e2224648bf89272d536c8f04dcdda37ef0a71473886dd7", size = 12588615, upload-time = "2025-03-05T16:56:22.715Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/3d/203d1c18cb239313ac125721f284e94c01c7eee947adb2ef2d9a85ac8d66/google_api_python_client-2.163.0-py2.py3-none-any.whl", hash = "sha256:080e8bc0669cb4c1fb8efb8da2f5b91a2625d8f0e7796cfad978f33f7016c6c4", size = 13097882, upload-time = "2025-03-05T16:56:19.197Z" }, +] + +[[package]] +name = "google-auth" +version = "2.48.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyasn1-modules" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/41/242044323fbd746615884b1c16639749e73665b718209946ebad7ba8a813/google_auth-2.48.0.tar.gz", hash = "sha256:4f7e706b0cd3208a3d940a19a822c37a476ddba5450156c3e6624a71f7c841ce", size = 326522, upload-time = "2026-01-26T19:22:47.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/1d/d6466de3a5249d35e832a52834115ca9d1d0de6abc22065f049707516d47/google_auth-2.48.0-py3-none-any.whl", hash = "sha256:2e2a537873d449434252a9632c28bfc268b0adb1e53f9fb62afc5333a975903f", size = 236499, upload-time = "2026-01-26T19:22:45.099Z" }, +] + +[[package]] +name = "google-auth-httplib2" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "httplib2" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/be/217a598a818567b28e859ff087f347475c807a5649296fb5a817c58dacef/google-auth-httplib2-0.2.0.tar.gz", hash = "sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05", size = 10842, upload-time = "2023-12-12T17:40:30.722Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/8a/fe34d2f3f9470a27b01c9e76226965863f153d5fbe276f83608562e49c04/google_auth_httplib2-0.2.0-py2.py3-none-any.whl", hash = "sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d", size = 9253, upload-time = "2023-12-12T17:40:13.055Z" }, +] + +[[package]] +name = "google-cloud-access-context-manager" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/eb/f8dcd4e3c596733f872732b43836df6486fa09966ce5586443d4e9e288fb/google_cloud_access_context_manager-0.3.0.tar.gz", hash = "sha256:f3aa35c9225b7aaef85ecdacedcc1577789be8d458b7a41b6ad23b504786e5f9", size = 46643, upload-time = "2025-10-17T02:33:32.028Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/94/24b010493660dd55e2d9769ae7ef44164aebd7e1f4a9266cf9459affd687/google_cloud_access_context_manager-0.3.0-py3-none-any.whl", hash = "sha256:5d15ad51547f06c281e35f16b4ffcb3e98bb2d898b01470f88b94edfb2eeb0a3", size = 58852, upload-time = "2025-10-17T02:30:33.768Z" }, +] + +[[package]] +name = "google-cloud-asset" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "google-cloud-access-context-manager" }, + { name = "google-cloud-org-policy" }, + { name = "google-cloud-os-config" }, + { name = "grpc-google-iam-v1" }, + { name = "grpcio" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/7e/53cbc8352da27168f7ddefd8faa4e2cdde9309a6543e32c6abfd95e15f2e/google_cloud_asset-4.2.0.tar.gz", hash = "sha256:1734906cfd9b6ea6922861c8f1b4fcabe90d53ca267ee88499e8532b7593b35f", size = 305130, upload-time = "2026-01-15T13:14:16.184Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/88/9a43fae1d2fed94d7f5f46b6f4c44bd15e5ea0e8657632108b5ec5f53d9d/google_cloud_asset-4.2.0-py3-none-any.whl", hash = "sha256:fd7ea04c64948a4779790343204cd5b41d4772d6ab1d05a9125e28a637ac0862", size = 282707, upload-time = "2026-01-09T14:53:03.081Z" }, +] + +[[package]] +name = "google-cloud-org-policy" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "grpcio" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/6d/4330ac61bf1b441d04bc0c3eb61b1cc1c00327c53c187ca7ed79d4d373eb/google_cloud_org_policy-1.16.0.tar.gz", hash = "sha256:c72147127d88d9809af8738b2abe34806eac529c3cdc57aa915cc08a1b842a13", size = 100280, upload-time = "2026-01-15T13:15:17.295Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/f6/65bb8770fe5f0a5e53886901731a9453df5ef9d44c86941a67e2ddb0e350/google_cloud_org_policy-1.16.0-py3-none-any.whl", hash = "sha256:96d1ed38f795182600a58f8eb2879e1577ce663b6b27df0b8a3050960cff87a5", size = 85598, upload-time = "2026-01-15T13:12:50.044Z" }, +] + +[[package]] +name = "google-cloud-os-config" +version = "1.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "grpcio" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/99/b23efdf6a316d92675ef2cec45c470b543f5f3078ba7a82b7a8f68883940/google_cloud_os_config-1.23.0.tar.gz", hash = "sha256:a629cf55b3ede36b2df89814c6ccf3c1d43c7f1b43db6c7c02eb4860851baf3a", size = 304811, upload-time = "2026-01-15T13:04:50.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/81/cf65aeda47c34ac5bf8cf9f8684d6ff9bd015025332f297e856163a97870/google_cloud_os_config-1.23.0-py3-none-any.whl", hash = "sha256:fea865018391abca42a9a74d270ddab516ae0865d6e9ad3bcb503286ca01c069", size = 264712, upload-time = "2026-01-15T13:02:48.689Z" }, +] + +[[package]] +name = "google-cloud-resource-manager" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "grpc-google-iam-v1" }, + { name = "grpcio" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/7f/db00b2820475793a52958dc55fe9ec2eb8e863546e05fcece9b921f86ebe/google_cloud_resource_manager-1.16.0.tar.gz", hash = "sha256:cc938f87cc36c2672f062b1e541650629e0d954c405a4dac35ceedee70c267c3", size = 459840, upload-time = "2026-01-15T13:04:07.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/ff/4b28bcc791d9d7e4ac8fea00fbd90ccb236afda56746a3b4564d2ae45df3/google_cloud_resource_manager-1.16.0-py3-none-any.whl", hash = "sha256:fb9a2ad2b5053c508e1c407ac31abfd1a22e91c32876c1892830724195819a28", size = 400218, upload-time = "2026-01-15T13:02:47.378Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.72.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" }, +] + +[package.optional-dependencies] +grpc = [ + { name = "grpcio" }, +] + +[[package]] +name = "gprof2dot" +version = "2025.4.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/fd/cad13fa1f7a463a607176432c4affa33ea162f02f58cc36de1d40d3e6b48/gprof2dot-2025.4.14.tar.gz", hash = "sha256:35743e2d2ca027bf48fa7cba37021aaf4a27beeae1ae8e05a50b55f1f921a6ce", size = 39536, upload-time = "2025-04-14T07:21:45.76Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/ed/89d760cb25279109b89eb52975a7b5479700d3114a2421ce735bfb2e7513/gprof2dot-2025.4.14-py3-none-any.whl", hash = "sha256:0742e4c0b4409a5e8777e739388a11e1ed3750be86895655312ea7c20bd0090e", size = 37555, upload-time = "2025-04-14T07:21:43.319Z" }, +] + +[[package]] +name = "graphemeu" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/20/d012f71e7d00e0d5bb25176b9a96c5313d0e30cf947153bfdfa78089f4bb/graphemeu-0.7.2.tar.gz", hash = "sha256:42bbe373d7c146160f286cd5f76b1a8ad29172d7333ce10705c5cc282462a4f8", size = 307626, upload-time = "2025-01-15T09:48:59.488Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/18/36503ea63e1ecd0a95590d7b6b8b7d227a1e4541a154e1612a231def1bdc/graphemeu-0.7.2-py3-none-any.whl", hash = "sha256:1444520f6899fd30114fc2a39f297d86d10fa0f23bf7579f772f8bc7efaa2542", size = 22670, upload-time = "2025-01-15T09:48:57.241Z" }, +] + +[[package]] +name = "greenlet" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/99/1cd3411c56a410994669062bd73dd58270c00cc074cac15f385a1fd91f8a/greenlet-3.3.1.tar.gz", hash = "sha256:41848f3230b58c08bb43dee542e74a2a2e34d3c59dc3076cec9151aeeedcae98", size = 184690, upload-time = "2026-01-23T15:31:02.076Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/e8/2e1462c8fdbe0f210feb5ac7ad2d9029af8be3bf45bd9fa39765f821642f/greenlet-3.3.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5fd23b9bc6d37b563211c6abbb1b3cab27db385a4449af5c32e932f93017080c", size = 274974, upload-time = "2026-01-23T15:31:02.891Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a8/530a401419a6b302af59f67aaf0b9ba1015855ea7e56c036b5928793c5bd/greenlet-3.3.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f51496a0bfbaa9d74d36a52d2580d1ef5ed4fdfcff0a73730abfbbbe1403dd", size = 577175, upload-time = "2026-01-23T16:00:56.213Z" }, + { url = "https://files.pythonhosted.org/packages/8e/89/7e812bb9c05e1aaef9b597ac1d0962b9021d2c6269354966451e885c4e6b/greenlet-3.3.1-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb0feb07fe6e6a74615ee62a880007d976cf739b6669cce95daa7373d4fc69c5", size = 590401, upload-time = "2026-01-23T16:05:26.365Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/e2d5f0e59b94a2269b68a629173263fa40b63da32f5c231307c349315871/greenlet-3.3.1-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:67ea3fc73c8cd92f42467a72b75e8f05ed51a0e9b1d15398c913416f2dafd49f", size = 601161, upload-time = "2026-01-23T16:15:53.456Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ae/8d472e1f5ac5efe55c563f3eabb38c98a44b832602e12910750a7c025802/greenlet-3.3.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39eda9ba259cc9801da05351eaa8576e9aa83eb9411e8f0c299e05d712a210f2", size = 590272, upload-time = "2026-01-23T15:32:49.411Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/0fde34bebfcadc833550717eade64e35ec8738e6b097d5d248274a01258b/greenlet-3.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e2e7e882f83149f0a71ac822ebf156d902e7a5d22c9045e3e0d1daf59cee2cc9", size = 1550729, upload-time = "2026-01-23T16:04:20.867Z" }, + { url = "https://files.pythonhosted.org/packages/16/c9/2fb47bee83b25b119d5a35d580807bb8b92480a54b68fef009a02945629f/greenlet-3.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80aa4d79eb5564f2e0a6144fcc744b5a37c56c4a92d60920720e99210d88db0f", size = 1615552, upload-time = "2026-01-23T15:33:45.743Z" }, + { url = "https://files.pythonhosted.org/packages/1f/54/dcf9f737b96606f82f8dd05becfb8d238db0633dd7397d542a296fe9cad3/greenlet-3.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:32e4ca9777c5addcbf42ff3915d99030d8e00173a56f80001fb3875998fe410b", size = 226462, upload-time = "2026-01-23T15:36:50.422Z" }, + { url = "https://files.pythonhosted.org/packages/91/37/61e1015cf944ddd2337447d8e97fb423ac9bc21f9963fb5f206b53d65649/greenlet-3.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:da19609432f353fed186cc1b85e9440db93d489f198b4bdf42ae19cc9d9ac9b4", size = 225715, upload-time = "2026-01-23T15:33:17.298Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c8/9d76a66421d1ae24340dfae7e79c313957f6e3195c144d2c73333b5bfe34/greenlet-3.3.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7e806ca53acf6d15a888405880766ec84721aa4181261cd11a457dfe9a7a4975", size = 276443, upload-time = "2026-01-23T15:30:10.066Z" }, + { url = "https://files.pythonhosted.org/packages/81/99/401ff34bb3c032d1f10477d199724f5e5f6fbfb59816ad1455c79c1eb8e7/greenlet-3.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d842c94b9155f1c9b3058036c24ffb8ff78b428414a19792b2380be9cecf4f36", size = 597359, upload-time = "2026-01-23T16:00:57.394Z" }, + { url = "https://files.pythonhosted.org/packages/2b/bc/4dcc0871ed557792d304f50be0f7487a14e017952ec689effe2180a6ff35/greenlet-3.3.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20fedaadd422fa02695f82093f9a98bad3dab5fcda793c658b945fcde2ab27ba", size = 607805, upload-time = "2026-01-23T16:05:28.068Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cd/7a7ca57588dac3389e97f7c9521cb6641fd8b6602faf1eaa4188384757df/greenlet-3.3.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c620051669fd04ac6b60ebc70478210119c56e2d5d5df848baec4312e260e4ca", size = 622363, upload-time = "2026-01-23T16:15:54.754Z" }, + { url = "https://files.pythonhosted.org/packages/cf/05/821587cf19e2ce1f2b24945d890b164401e5085f9d09cbd969b0c193cd20/greenlet-3.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14194f5f4305800ff329cbf02c5fcc88f01886cadd29941b807668a45f0d2336", size = 609947, upload-time = "2026-01-23T15:32:51.004Z" }, + { url = "https://files.pythonhosted.org/packages/a4/52/ee8c46ed9f8babaa93a19e577f26e3d28a519feac6350ed6f25f1afee7e9/greenlet-3.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7b2fe4150a0cf59f847a67db8c155ac36aed89080a6a639e9f16df5d6c6096f1", size = 1567487, upload-time = "2026-01-23T16:04:22.125Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7c/456a74f07029597626f3a6db71b273a3632aecb9afafeeca452cfa633197/greenlet-3.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49f4ad195d45f4a66a0eb9c1ba4832bb380570d361912fa3554746830d332149", size = 1636087, upload-time = "2026-01-23T15:33:47.486Z" }, + { url = "https://files.pythonhosted.org/packages/34/2f/5e0e41f33c69655300a5e54aeb637cf8ff57f1786a3aba374eacc0228c1d/greenlet-3.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:cc98b9c4e4870fa983436afa999d4eb16b12872fab7071423d5262fa7120d57a", size = 227156, upload-time = "2026-01-23T15:34:34.808Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ab/717c58343cf02c5265b531384b248787e04d8160b8afe53d9eec053d7b44/greenlet-3.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:bfb2d1763d777de5ee495c85309460f6fd8146e50ec9d0ae0183dbf6f0a829d1", size = 226403, upload-time = "2026-01-23T15:31:39.372Z" }, +] + +[[package]] +name = "grpc-google-iam-v1" +version = "0.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos", extra = ["grpc"] }, + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/1e/1011451679a983f2f5c6771a1682542ecb027776762ad031fd0d7129164b/grpc_google_iam_v1-0.14.3.tar.gz", hash = "sha256:879ac4ef33136c5491a6300e27575a9ec760f6cdf9a2518798c1b8977a5dc389", size = 23745, upload-time = "2025-10-15T21:14:53.318Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/bd/330a1bbdb1afe0b96311249e699b6dc9cfc17916394fd4503ac5aca2514b/grpc_google_iam_v1-0.14.3-py3-none-any.whl", hash = "sha256:7a7f697e017a067206a3dfef44e4c634a34d3dee135fe7d7a4613fe3e59217e6", size = 32690, upload-time = "2025-10-15T21:14:51.72Z" }, +] + +[[package]] +name = "grpcio" +version = "1.76.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182, upload-time = "2025-10-21T16:23:12.106Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/00/8163a1beeb6971f66b4bbe6ac9457b97948beba8dd2fc8e1281dce7f79ec/grpcio-1.76.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2e1743fbd7f5fa713a1b0a8ac8ebabf0ec980b5d8809ec358d488e273b9cf02a", size = 5843567, upload-time = "2025-10-21T16:20:52.829Z" }, + { url = "https://files.pythonhosted.org/packages/10/c1/934202f5cf335e6d852530ce14ddb0fef21be612ba9ecbbcbd4d748ca32d/grpcio-1.76.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:a8c2cf1209497cf659a667d7dea88985e834c24b7c3b605e6254cbb5076d985c", size = 11848017, upload-time = "2025-10-21T16:20:56.705Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/8dec16b1863d74af6eb3543928600ec2195af49ca58b16334972f6775663/grpcio-1.76.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:08caea849a9d3c71a542827d6df9d5a69067b0a1efbea8a855633ff5d9571465", size = 6412027, upload-time = "2025-10-21T16:20:59.3Z" }, + { url = "https://files.pythonhosted.org/packages/d7/64/7b9e6e7ab910bea9d46f2c090380bab274a0b91fb0a2fe9b0cd399fffa12/grpcio-1.76.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f0e34c2079d47ae9f6188211db9e777c619a21d4faba6977774e8fa43b085e48", size = 7075913, upload-time = "2025-10-21T16:21:01.645Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/093c46e9546073cefa789bd76d44c5cb2abc824ca62af0c18be590ff13ba/grpcio-1.76.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8843114c0cfce61b40ad48df65abcfc00d4dba82eae8718fab5352390848c5da", size = 6615417, upload-time = "2025-10-21T16:21:03.844Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b6/5709a3a68500a9c03da6fb71740dcdd5ef245e39266461a03f31a57036d8/grpcio-1.76.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8eddfb4d203a237da6f3cc8a540dad0517d274b5a1e9e636fd8d2c79b5c1d397", size = 7199683, upload-time = "2025-10-21T16:21:06.195Z" }, + { url = "https://files.pythonhosted.org/packages/91/d3/4b1f2bf16ed52ce0b508161df3a2d186e4935379a159a834cb4a7d687429/grpcio-1.76.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:32483fe2aab2c3794101c2a159070584e5db11d0aa091b2c0ea9c4fc43d0d749", size = 8163109, upload-time = "2025-10-21T16:21:08.498Z" }, + { url = "https://files.pythonhosted.org/packages/5c/61/d9043f95f5f4cf085ac5dd6137b469d41befb04bd80280952ffa2a4c3f12/grpcio-1.76.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dcfe41187da8992c5f40aa8c5ec086fa3672834d2be57a32384c08d5a05b4c00", size = 7626676, upload-time = "2025-10-21T16:21:10.693Z" }, + { url = "https://files.pythonhosted.org/packages/36/95/fd9a5152ca02d8881e4dd419cdd790e11805979f499a2e5b96488b85cf27/grpcio-1.76.0-cp311-cp311-win32.whl", hash = "sha256:2107b0c024d1b35f4083f11245c0e23846ae64d02f40b2b226684840260ed054", size = 3997688, upload-time = "2025-10-21T16:21:12.746Z" }, + { url = "https://files.pythonhosted.org/packages/60/9c/5c359c8d4c9176cfa3c61ecd4efe5affe1f38d9bae81e81ac7186b4c9cc8/grpcio-1.76.0-cp311-cp311-win_amd64.whl", hash = "sha256:522175aba7af9113c48ec10cc471b9b9bd4f6ceb36aeb4544a8e2c80ed9d252d", size = 4709315, upload-time = "2025-10-21T16:21:15.26Z" }, + { url = "https://files.pythonhosted.org/packages/bf/05/8e29121994b8d959ffa0afd28996d452f291b48cfc0875619de0bde2c50c/grpcio-1.76.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8", size = 5799718, upload-time = "2025-10-21T16:21:17.939Z" }, + { url = "https://files.pythonhosted.org/packages/d9/75/11d0e66b3cdf998c996489581bdad8900db79ebd83513e45c19548f1cba4/grpcio-1.76.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280", size = 11825627, upload-time = "2025-10-21T16:21:20.466Z" }, + { url = "https://files.pythonhosted.org/packages/28/50/2f0aa0498bc188048f5d9504dcc5c2c24f2eb1a9337cd0fa09a61a2e75f0/grpcio-1.76.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4", size = 6359167, upload-time = "2025-10-21T16:21:23.122Z" }, + { url = "https://files.pythonhosted.org/packages/66/e5/bbf0bb97d29ede1d59d6588af40018cfc345b17ce979b7b45424628dc8bb/grpcio-1.76.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9f8f757bebaaea112c00dba718fc0d3260052ce714e25804a03f93f5d1c6cc11", size = 7044267, upload-time = "2025-10-21T16:21:25.995Z" }, + { url = "https://files.pythonhosted.org/packages/f5/86/f6ec2164f743d9609691115ae8ece098c76b894ebe4f7c94a655c6b03e98/grpcio-1.76.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6", size = 6573963, upload-time = "2025-10-21T16:21:28.631Z" }, + { url = "https://files.pythonhosted.org/packages/60/bc/8d9d0d8505feccfdf38a766d262c71e73639c165b311c9457208b56d92ae/grpcio-1.76.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f92f88e6c033db65a5ae3d97905c8fea9c725b63e28d5a75cb73b49bda5024d8", size = 7164484, upload-time = "2025-10-21T16:21:30.837Z" }, + { url = "https://files.pythonhosted.org/packages/67/e6/5d6c2fc10b95edf6df9b8f19cf10a34263b7fd48493936fffd5085521292/grpcio-1.76.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4baf3cbe2f0be3289eb68ac8ae771156971848bb8aaff60bad42005539431980", size = 8127777, upload-time = "2025-10-21T16:21:33.577Z" }, + { url = "https://files.pythonhosted.org/packages/3f/c8/dce8ff21c86abe025efe304d9e31fdb0deaaa3b502b6a78141080f206da0/grpcio-1.76.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882", size = 7594014, upload-time = "2025-10-21T16:21:41.882Z" }, + { url = "https://files.pythonhosted.org/packages/e0/42/ad28191ebf983a5d0ecef90bab66baa5a6b18f2bfdef9d0a63b1973d9f75/grpcio-1.76.0-cp312-cp312-win32.whl", hash = "sha256:45d59a649a82df5718fd9527ce775fd66d1af35e6d31abdcdc906a49c6822958", size = 3984750, upload-time = "2025-10-21T16:21:44.006Z" }, + { url = "https://files.pythonhosted.org/packages/9e/00/7bd478cbb851c04a48baccaa49b75abaa8e4122f7d86da797500cccdd771/grpcio-1.76.0-cp312-cp312-win_amd64.whl", hash = "sha256:c088e7a90b6017307f423efbb9d1ba97a22aa2170876223f9709e9d1de0b5347", size = 4704003, upload-time = "2025-10-21T16:21:46.244Z" }, +] + +[[package]] +name = "grpcio-status" +version = "1.76.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/46/e9f19d5be65e8423f886813a2a9d0056ba94757b0c5007aa59aed1a961fa/grpcio_status-1.76.0.tar.gz", hash = "sha256:25fcbfec74c15d1a1cb5da3fab8ee9672852dc16a5a9eeb5baf7d7a9952943cd", size = 13679, upload-time = "2025-10-21T16:28:52.545Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/cc/27ba60ad5a5f2067963e6a858743500df408eb5855e98be778eaef8c9b02/grpcio_status-1.76.0-py3-none-any.whl", hash = "sha256:380568794055a8efbbd8871162df92012e0228a5f6dffaf57f2a00c534103b18", size = 14425, upload-time = "2025-10-21T16:28:40.853Z" }, +] + +[[package]] +name = "gunicorn" +version = "23.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload-time = "2024-08-10T20:25:27.378Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "h2" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hpack" }, + { name = "hyperframe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" }, +] + +[[package]] +name = "hpack" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276, upload-time = "2025-01-22T21:44:58.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httplib2" +version = "0.31.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyparsing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/1f/e86365613582c027dda5ddb64e1010e57a3d53e99ab8a72093fa13d565ec/httplib2-0.31.2.tar.gz", hash = "sha256:385e0869d7397484f4eab426197a4c020b606edd43372492337c0b4010ae5d24", size = 250800, upload-time = "2026-01-23T11:04:44.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/90/fd509079dfcab01102c0fdd87f3a9506894bc70afcf9e9785ef6b2b3aff6/httplib2-0.31.2-py3-none-any.whl", hash = "sha256:dbf0c2fa3862acf3c55c078ea9c0bc4481d7dc5117cae71be9514912cf9f8349", size = 91099, upload-time = "2026-01-23T11:04:42.78Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[package.optional-dependencies] +http2 = [ + { name = "h2" }, +] + +[[package]] +name = "humanfriendly" +version = "10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyreadline3", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702, upload-time = "2021-09-17T21:40:43.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794, upload-time = "2021-09-17T21:40:39.897Z" }, +] + +[[package]] +name = "hyperframe" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" }, +] + +[[package]] +name = "iamdata" +version = "0.1.202602021" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/93/5e/8179963b7a528c548824a8e4088150509d9fa8571dd622b7399f6d2d5680/iamdata-0.1.202602021.tar.gz", hash = "sha256:c24265fc3694076f65da91a8aa9361b60da25f7b8cfd8ba4ddd6aa1b9bb5153e", size = 771233, upload-time = "2026-02-02T05:49:56.76Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/9e/ae7a3019aa5a27d70412b74da4f0304695efa5d9a88f0689f37ea2602ea2/iamdata-0.1.202602021-py3-none-any.whl", hash = "sha256:48419662d75dd0e1ea22b9cc98fd70201d4c72760c6897acc46ad9ab90633d18", size = 1226614, upload-time = "2026-02-02T05:49:54.735Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "inflection" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/7e/691d061b7329bc8d54edbf0ec22fbfb2afe61facb681f9aaa9bff7a27d04/inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417", size = 15091, upload-time = "2020-08-22T08:16:29.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/91/aa6bde563e0085a02a435aa99b49ef75b0a4b062635e606dab23ce18d720/inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2", size = 9454, upload-time = "2020-08-22T08:16:27.816Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "iso8601" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/f3/ef59cee614d5e0accf6fd0cbba025b93b272e626ca89fb70a3e9187c5d15/iso8601-2.1.0.tar.gz", hash = "sha256:6b1d3829ee8921c4301998c909f7829fa9ed3cbdac0d3b16af2d743aed1ba8df", size = 6522, upload-time = "2023-10-03T00:25:39.317Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/0c/f37b6a241f0759b7653ffa7213889d89ad49a2b76eb2ddf3b57b2738c347/iso8601-2.1.0-py3-none-any.whl", hash = "sha256:aac4145c4dcb66ad8b648a02830f5e2ff6c24af20f4f482689be402db2429242", size = 7545, upload-time = "2023-10-03T00:25:32.304Z" }, +] + +[[package]] +name = "isodate" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, +] + +[[package]] +name = "isort" +version = "5.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/87/f9/c1eb8635a24e87ade2efce21e3ce8cd6b8630bb685ddc9cdaca1349b2eb5/isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", size = 175303, upload-time = "2023-12-13T20:37:26.124Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/b3/8def84f539e7d2289a02f0524b944b15d7c75dab7628bedf1c4f0992029c/isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6", size = 92310, upload-time = "2023-12-13T20:37:23.244Z" }, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jiter" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/29/499f8c9eaa8a16751b1c0e45e6f5f1761d180da873d417996cc7bddc8eef/jiter-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ea026e70a9a28ebbdddcbcf0f1323128a8db66898a06eaad3a4e62d2f554d096", size = 311157, upload-time = "2026-02-02T12:35:37.758Z" }, + { url = "https://files.pythonhosted.org/packages/50/f6/566364c777d2ab450b92100bea11333c64c38d32caf8dc378b48e5b20c46/jiter-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911", size = 319729, upload-time = "2026-02-02T12:35:39.246Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/560f13ec5e4f116d8ad2658781646cca91b617ae3b8758d4a5076b278f70/jiter-0.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701", size = 354766, upload-time = "2026-02-02T12:35:40.662Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0d/061faffcfe94608cbc28a0d42a77a74222bdf5055ccdbe5fd2292b94f510/jiter-0.13.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c", size = 362587, upload-time = "2026-02-02T12:35:42.025Z" }, + { url = "https://files.pythonhosted.org/packages/92/c9/c66a7864982fd38a9773ec6e932e0398d1262677b8c60faecd02ffb67bf3/jiter-0.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4", size = 487537, upload-time = "2026-02-02T12:35:43.459Z" }, + { url = "https://files.pythonhosted.org/packages/6c/86/84eb4352cd3668f16d1a88929b5888a3fe0418ea8c1dfc2ad4e7bf6e069a/jiter-0.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165", size = 373717, upload-time = "2026-02-02T12:35:44.928Z" }, + { url = "https://files.pythonhosted.org/packages/6e/09/9fe4c159358176f82d4390407a03f506a8659ed13ca3ac93a843402acecf/jiter-0.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018", size = 362683, upload-time = "2026-02-02T12:35:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5e/85f3ab9caca0c1d0897937d378b4a515cae9e119730563572361ea0c48ae/jiter-0.13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9da38b4fedde4fb528c740c2564628fbab737166a0e73d6d46cb4bb5463ff411", size = 392345, upload-time = "2026-02-02T12:35:48.088Z" }, + { url = "https://files.pythonhosted.org/packages/12/4c/05b8629ad546191939e6f0c2f17e29f542a398f4a52fb987bc70b6d1eb8b/jiter-0.13.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5", size = 517775, upload-time = "2026-02-02T12:35:49.482Z" }, + { url = "https://files.pythonhosted.org/packages/4d/88/367ea2eb6bc582c7052e4baf5ddf57ebe5ab924a88e0e09830dfb585c02d/jiter-0.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3", size = 551325, upload-time = "2026-02-02T12:35:51.104Z" }, + { url = "https://files.pythonhosted.org/packages/f3/12/fa377ffb94a2f28c41afaed093e0d70cfe512035d5ecb0cad0ae4792d35e/jiter-0.13.0-cp311-cp311-win32.whl", hash = "sha256:0f0c065695f616a27c920a56ad0d4fc46415ef8b806bf8fc1cacf25002bd24e1", size = 204709, upload-time = "2026-02-02T12:35:52.467Z" }, + { url = "https://files.pythonhosted.org/packages/cb/16/8e8203ce92f844dfcd3d9d6a5a7322c77077248dbb12da52d23193a839cd/jiter-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:0733312953b909688ae3c2d58d043aa040f9f1a6a75693defed7bc2cc4bf2654", size = 204560, upload-time = "2026-02-02T12:35:53.925Z" }, + { url = "https://files.pythonhosted.org/packages/44/26/97cc40663deb17b9e13c3a5cf29251788c271b18ee4d262c8f94798b8336/jiter-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:5d9b34ad56761b3bf0fbe8f7e55468704107608512350962d3317ffd7a4382d5", size = 189608, upload-time = "2026-02-02T12:35:55.304Z" }, + { url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958, upload-time = "2026-02-02T12:35:57.165Z" }, + { url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597, upload-time = "2026-02-02T12:35:58.591Z" }, + { url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821, upload-time = "2026-02-02T12:36:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163, upload-time = "2026-02-02T12:36:01.937Z" }, + { url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709, upload-time = "2026-02-02T12:36:03.41Z" }, + { url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480, upload-time = "2026-02-02T12:36:04.791Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735, upload-time = "2026-02-02T12:36:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814, upload-time = "2026-02-02T12:36:08.368Z" }, + { url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990, upload-time = "2026-02-02T12:36:09.993Z" }, + { url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021, upload-time = "2026-02-02T12:36:11.376Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024, upload-time = "2026-02-02T12:36:12.682Z" }, + { url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424, upload-time = "2026-02-02T12:36:13.93Z" }, + { url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818, upload-time = "2026-02-02T12:36:15.308Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/3c29819a27178d0e461a8571fb63c6ae38be6dc36b78b3ec2876bbd6a910/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b1cbfa133241d0e6bdab48dcdc2604e8ba81512f6bbd68ec3e8e1357dd3c316c", size = 307016, upload-time = "2026-02-02T12:37:42.755Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ae/60993e4b07b1ac5ebe46da7aa99fdbb802eb986c38d26e3883ac0125c4e0/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2", size = 305024, upload-time = "2026-02-02T12:37:44.774Z" }, + { url = "https://files.pythonhosted.org/packages/77/fa/2227e590e9cf98803db2811f172b2d6460a21539ab73006f251c66f44b14/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434", size = 339337, upload-time = "2026-02-02T12:37:46.668Z" }, + { url = "https://files.pythonhosted.org/packages/2d/92/015173281f7eb96c0ef580c997da8ef50870d4f7f4c9e03c845a1d62ae04/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d", size = 346395, upload-time = "2026-02-02T12:37:48.09Z" }, + { url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169, upload-time = "2026-02-02T12:37:50.376Z" }, + { url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808, upload-time = "2026-02-02T12:37:52.092Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384, upload-time = "2026-02-02T12:37:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" }, +] + +[[package]] +name = "jmespath" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, +] + +[[package]] +name = "jsonpatch" +version = "1.33" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpointer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" }, +] + +[[package]] +name = "jsonpickle" +version = "4.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/a6/d07afcfdef402900229bcca795f80506b207af13a838d4d99ad45abf530c/jsonpickle-4.1.1.tar.gz", hash = "sha256:f86e18f13e2b96c1c1eede0b7b90095bbb61d99fedc14813c44dc2f361dbbae1", size = 316885, upload-time = "2025-06-02T20:36:11.57Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/73/04df8a6fa66d43a9fd45c30f283cc4afff17da671886e451d52af60bdc7e/jsonpickle-4.1.1-py3-none-any.whl", hash = "sha256:bb141da6057898aa2438ff268362b126826c812a1721e31cf08a6e142910dc91", size = 47125, upload-time = "2025-06-02T20:36:08.647Z" }, +] + +[[package]] +name = "jsonpointer" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload-time = "2024-06-10T19:24:42.462Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/2e/03362ee4034a4c917f697890ccd4aec0800ccf9ded7f511971c75451deec/jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", size = 325778, upload-time = "2024-07-08T18:40:05.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/4a/4f9dbeb84e8850557c02365a0eee0649abe5eb1d84af92a25731c6c0f922/jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566", size = 88462, upload-time = "2024-07-08T18:40:00.165Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "jwcrypto" +version = "1.5.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/90/f065668004d22715c1940d6e88e4c3afc8ee16d5664e4478d2c8fd23a250/jwcrypto-1.5.7.tar.gz", hash = "sha256:70204d7cca406eda8c82352e3c41ba2d946610dafd19e54403f0a1f4f18633c6", size = 89535, upload-time = "2026-04-07T00:35:36.116Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/24/fb7da4d6613de7001feaf540d4b5969c6b5a1c42839043b0196cb13aa057/jwcrypto-1.5.7-py3-none-any.whl", hash = "sha256:729463fefe28b6de5cf1ebfda3e94f1a1b41d2799148ef98a01cb9678ebe2bb0", size = 94799, upload-time = "2026-04-07T00:35:35.085Z" }, +] + +[[package]] +name = "keystoneauth1" +version = "5.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "iso8601" }, + { name = "os-service-types" }, + { name = "pbr" }, + { name = "requests" }, + { name = "stevedore" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e5/2b/5a08163c8e878811f11dc8c78c041c2384cd424c71f75fb7a6b19969047f/keystoneauth1-5.13.0.tar.gz", hash = "sha256:57c9ca407207899b50d8ff1ca8abb4a4e7427461bfc1877eb8519c3989ce63ec", size = 288721, upload-time = "2026-01-19T10:47:02.467Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/99/76476a1057b349c860bae72e45d6ef438feb877c84ee7d565faf464e54c3/keystoneauth1-5.13.0-py3-none-any.whl", hash = "sha256:5ab81412eb0923ceb9c602cc3decce514b399523cb83d16b409ed3b0f9b03d41", size = 343585, upload-time = "2026-01-19T10:47:00.762Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" }, + { url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596, upload-time = "2025-08-10T21:25:56.861Z" }, + { url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548, upload-time = "2025-08-10T21:25:58.246Z" }, + { url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618, upload-time = "2025-08-10T21:25:59.857Z" }, + { url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437, upload-time = "2025-08-10T21:26:01.105Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742, upload-time = "2025-08-10T21:26:02.675Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810, upload-time = "2025-08-10T21:26:04.009Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579, upload-time = "2025-08-10T21:26:05.317Z" }, + { url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071, upload-time = "2025-08-10T21:26:06.686Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840, upload-time = "2025-08-10T21:26:07.94Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159, upload-time = "2025-08-10T21:26:09.048Z" }, + { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" }, + { url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" }, + { url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" }, + { url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" }, + { url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" }, + { url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" }, + { url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" }, + { url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" }, + { url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" }, + { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" }, + { url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" }, + { url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" }, + { url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009, upload-time = "2025-08-10T21:27:46.376Z" }, + { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" }, +] + +[[package]] +name = "knack" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argcomplete" }, + { name = "jmespath" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "pyyaml" }, + { name = "tabulate" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/5b/7cc69b2941a11bdace4faffef8f023543feefd14ab0222b6e62a318c53b9/knack-0.11.0.tar.gz", hash = "sha256:eb6568001e9110b1b320941431c51033d104cc98cda2254a5c2b09ba569fd494", size = 72308, upload-time = "2023-07-26T06:23:32.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/6b/caf27d5a40618c7e945a1c68e1961c2d3637edfce9ebb0edc27c9ff53c1c/knack-0.11.0-py3-none-any.whl", hash = "sha256:6704c867840978a119a193914a90e2e98c7be7dff764c8fcd8a2286c5a978d00", size = 60848, upload-time = "2023-07-26T06:23:30.221Z" }, +] + +[[package]] +name = "kombu" +version = "5.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "amqp" }, + { name = "packaging" }, + { name = "tzdata" }, + { name = "vine" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/a5/607e533ed6c83ae1a696969b8e1c137dfebd5759a2e9682e26ff1b97740b/kombu-5.6.2.tar.gz", hash = "sha256:8060497058066c6f5aed7c26d7cd0d3b574990b09de842a8c5aaed0b92cc5a55", size = 472594, upload-time = "2025-12-29T20:30:07.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/0f/834427d8c03ff1d7e867d3db3d176470c64871753252b21b4f4897d1fa45/kombu-5.6.2-py3-none-any.whl", hash = "sha256:efcfc559da324d41d61ca311b0c64965ea35b4c55cc04ee36e55386145dace93", size = 214219, upload-time = "2025-12-29T20:30:05.74Z" }, +] + +[[package]] +name = "kubernetes" +version = "32.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "durationpy" }, + { name = "google-auth" }, + { name = "oauthlib" }, + { name = "python-dateutil" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "requests-oauthlib" }, + { name = "six" }, + { name = "urllib3" }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/e8/0598f0e8b4af37cd9b10d8b87386cf3173cb8045d834ab5f6ec347a758b3/kubernetes-32.0.1.tar.gz", hash = "sha256:42f43d49abd437ada79a79a16bd48a604d3471a117a8347e87db693f2ba0ba28", size = 946691, upload-time = "2025-02-18T21:06:34.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/10/9f8af3e6f569685ce3af7faab51c8dd9d93b9c38eba339ca31c746119447/kubernetes-32.0.1-py2.py3-none-any.whl", hash = "sha256:35282ab8493b938b08ab5526c7ce66588232df00ef5e1dbe88a419107dc10998", size = 1988070, upload-time = "2025-02-18T21:06:31.391Z" }, +] + +[[package]] +name = "lxml" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/28/30/9abc9e34c657c33834eaf6cd02124c61bdf5944d802aa48e69be8da3585d/lxml-6.1.0.tar.gz", hash = "sha256:bfd57d8008c4965709a919c3e9a98f76c2c7cb319086b3d26858250620023b13", size = 4197006, upload-time = "2026-04-18T04:32:51.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/5d/3bccad330292946f97962df9d5f2d3ae129cce6e212732a781e856b91e07/lxml-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cec05be8c876f92a5aa07b01d60bbb4d11cfbdd654cad0561c0d7b5c043a61b9", size = 8526232, upload-time = "2026-04-18T04:27:40.389Z" }, + { url = "https://files.pythonhosted.org/packages/a7/51/adc8826570a112f83bb4ddb3a2ab510bbc2ccd62c1b9fe1f34fae2d90b57/lxml-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9c03e048b6ce8e77b09c734e931584894ecd58d08296804ca2d0b184c933ce50", size = 4595448, upload-time = "2026-04-18T04:27:44.208Z" }, + { url = "https://files.pythonhosted.org/packages/54/84/5a9ec07cbe1d2334a6465f863b949a520d2699a755738986dcd3b6b89e3f/lxml-6.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:942454ff253da14218f972b23dc72fa4edf6c943f37edd19cd697618b626fac5", size = 4923771, upload-time = "2026-04-18T04:32:17.402Z" }, + { url = "https://files.pythonhosted.org/packages/a7/23/851cfa33b6b38adb628e45ad51fb27105fa34b2b3ba9d1d4aa7a9428dfe0/lxml-6.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d036ee7b99d5148072ac7c9b847193decdfeac633db350363f7bce4fff108f0e", size = 5068101, upload-time = "2026-04-18T04:32:21.437Z" }, + { url = "https://files.pythonhosted.org/packages/b0/38/41bf99c2023c6b79916ba057d83e9db21d642f473cac210201222882d38b/lxml-6.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ae5d8d5427f3cc317e7950f2da7ad276df0cfa37b8de2f5658959e618ea8512", size = 5002573, upload-time = "2026-04-18T04:32:25.373Z" }, + { url = "https://files.pythonhosted.org/packages/c2/20/053aa10bdc39747e1e923ce2d45413075e84f70a136045bb09e5eaca41d3/lxml-6.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:363e47283bde87051b821826e71dde47f107e08614e1aa312ba0c5711e77738c", size = 5202816, upload-time = "2026-04-18T04:32:29.393Z" }, + { url = "https://files.pythonhosted.org/packages/9a/da/bc710fad8bf04b93baee752c192eaa2210cd3a84f969d0be7830fea55802/lxml-6.1.0-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:f504d861d9f2a8f94020130adac88d66de93841707a23a86244263d1e54682f5", size = 5329999, upload-time = "2026-04-18T04:32:34.019Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cb/bf035dedbdf7fab49411aa52e4236f3445e98d38647d85419e6c0d2806b9/lxml-6.1.0-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:23a5dc68e08ed13331d61815c08f260f46b4a60fdd1640bbeb82cf89a9d90289", size = 4659643, upload-time = "2026-04-18T04:32:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4f/22be31f33727a5e4c7b01b0a874503026e50329b259d3587e0b923cf964b/lxml-6.1.0-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f15401d8d3dbf239e23c818afc10c7207f7b95f9a307e092122b6f86dd43209a", size = 5265963, upload-time = "2026-04-18T04:32:41.881Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2b/d44d0e5c79226017f4ab8c87a802ebe4f89f97e6585a8e4166dffcdd7b6e/lxml-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fcf3da95e93349e0647d48d4b36a12783105bcc74cb0c416952f9988410846a3", size = 5045444, upload-time = "2026-04-18T04:32:44.512Z" }, + { url = "https://files.pythonhosted.org/packages/d3/c3/3f034fec1594c331a6dbf9491238fdcc9d66f68cc529e109ec75b97197e1/lxml-6.1.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:0d082495c5fcf426e425a6e28daaba1fcb6d8f854a4ff01effb1f1f381203eb9", size = 4712703, upload-time = "2026-04-18T04:32:47.16Z" }, + { url = "https://files.pythonhosted.org/packages/12/16/0b83fccc158218aca75a7aa33e97441df737950734246b9fffa39301603d/lxml-6.1.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:e3c4f84b24a1fcba435157d111c4b755099c6ff00a3daee1ad281817de75ed11", size = 5252745, upload-time = "2026-04-18T04:32:50.427Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ee/12e6c1b39a77666c02eaa77f94a870aaf63c4ac3a497b2d52319448b01c6/lxml-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:976a6b39b1b13e8c354ad8d3f261f3a4ac6609518af91bdb5094760a08f132c4", size = 5226822, upload-time = "2026-04-18T04:32:53.437Z" }, + { url = "https://files.pythonhosted.org/packages/34/20/c7852904858b4723af01d2fc14b5d38ff57cb92f01934a127ebd9a9e51aa/lxml-6.1.0-cp311-cp311-win32.whl", hash = "sha256:857efde87d365706590847b916baff69c0bc9252dc5af030e378c9800c0b10e3", size = 3594026, upload-time = "2026-04-18T04:27:31.903Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/d60c732b56da5085175c07c74b2df4e6d181b0c9a61e1691474f06ef4b39/lxml-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:183bfb45a493081943be7ea2b5adfc2b611e1cf377cefa8b8a8be404f45ef9a7", size = 4025114, upload-time = "2026-04-18T04:27:34.077Z" }, + { url = "https://files.pythonhosted.org/packages/c2/df/c84dcc175fd690823436d15b41cb920cd5ba5e14cd8bfb00949d5903b320/lxml-6.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:19f4164243fc206d12ed3d866e80e74f5bc3627966520da1a5f97e42c32a3f39", size = 3667742, upload-time = "2026-04-18T04:27:38.45Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d4/9326838b59dc36dfae42eec9656b97520f9997eee1de47b8316aaeed169c/lxml-6.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d2f17a16cd8751e8eb233a7e41aecdf8e511712e00088bf9be455f604cd0d28d", size = 8570663, upload-time = "2026-04-18T04:27:48.253Z" }, + { url = "https://files.pythonhosted.org/packages/d8/a4/053745ce1f8303ccbb788b86c0db3a91b973675cefc42566a188637b7c40/lxml-6.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f0cea5b1d3e6e77d71bd2b9972eb2446221a69dc52bb0b9c3c6f6e5700592d93", size = 4624024, upload-time = "2026-04-18T04:27:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/90/97/a517944b20f8fd0932ad2109482bee4e29fe721416387a363306667941f6/lxml-6.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fc46da94826188ed45cb53bd8e3fc076ae22675aea2087843d4735627f867c6d", size = 4930895, upload-time = "2026-04-18T04:32:56.29Z" }, + { url = "https://files.pythonhosted.org/packages/94/7c/e08a970727d556caa040a44773c7b7e3ad0f0d73dedc863543e9a8b931f2/lxml-6.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9147d8e386ec3b82c3b15d88927f734f565b0aaadef7def562b853adca45784a", size = 5093820, upload-time = "2026-04-18T04:32:58.94Z" }, + { url = "https://files.pythonhosted.org/packages/88/ee/2a5c2aa2c32016a226ca25d3e1056a8102ea6e1fe308bf50213586635400/lxml-6.1.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5715e0e28736a070f3f34a7ccc09e2fdcba0e3060abbcf61a1a5718ff6d6b105", size = 5005790, upload-time = "2026-04-18T04:33:01.272Z" }, + { url = "https://files.pythonhosted.org/packages/e3/38/a0db9be8f38ad6043ab9429487c128dd1d30f07956ef43040402f8da49e8/lxml-6.1.0-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4937460dc5df0cdd2f06a86c285c28afda06aefa3af949f9477d3e8df430c485", size = 5630827, upload-time = "2026-04-18T04:33:04.036Z" }, + { url = "https://files.pythonhosted.org/packages/31/ba/3c13d3fc24b7cacf675f808a3a1baabf43a30d0cd24c98f94548e9aa58eb/lxml-6.1.0-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc783ee3147e60a25aa0445ea82b3e8aabb83b240f2b95d32cb75587ff781814", size = 5240445, upload-time = "2026-04-18T04:33:06.87Z" }, + { url = "https://files.pythonhosted.org/packages/55/ba/eeef4ccba09b2212fe239f46c1692a98db1878e0872ae320756488878a94/lxml-6.1.0-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:40d9189f80075f2e1f88db21ef815a2b17b28adf8e50aaf5c789bfe737027f32", size = 5350121, upload-time = "2026-04-18T04:33:09.365Z" }, + { url = "https://files.pythonhosted.org/packages/7e/01/1da87c7b587c38d0cbe77a01aae3b9c1c49ed47d76918ef3db8fc151b1ca/lxml-6.1.0-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:05b9b8787e35bec69e68daf4952b2e6dfcfb0db7ecf1a06f8cdfbbac4eb71aad", size = 4694949, upload-time = "2026-04-18T04:33:11.628Z" }, + { url = "https://files.pythonhosted.org/packages/a1/88/7db0fe66d5aaf128443ee1623dec3db1576f3e4c17751ec0ef5866468590/lxml-6.1.0-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0f08beb0182e3e9a86fae124b3c47a7b41b7b69b225e1377db983802404e54", size = 5243901, upload-time = "2026-04-18T04:33:13.95Z" }, + { url = "https://files.pythonhosted.org/packages/00/a8/1346726af7d1f6fca1f11223ba34001462b0a3660416986d37641708d57c/lxml-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73becf6d8c81d4c76b1014dbd3584cb26d904492dcf73ca85dc8bff08dcd6d2d", size = 5048054, upload-time = "2026-04-18T04:33:16.965Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b7/85057012f035d1a0c87e02f8c723ca3c3e6e0728bcf4cb62080b21b1c1e3/lxml-6.1.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1ae225f66e5938f4fa29d37e009a3bb3b13032ac57eb4eb42afa44f6e4054e69", size = 4777324, upload-time = "2026-04-18T04:33:19.832Z" }, + { url = "https://files.pythonhosted.org/packages/75/6c/ad2f94a91073ef570f33718040e8e160d5fb93331cf1ab3ca1323f939e2d/lxml-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:690022c7fae793b0489aa68a658822cea83e0d5933781811cabbf5ea3bcfe73d", size = 5645702, upload-time = "2026-04-18T04:33:22.436Z" }, + { url = "https://files.pythonhosted.org/packages/3b/89/0bb6c0bd549c19004c60eea9dc554dd78fd647b72314ef25d460e0d208c6/lxml-6.1.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:63aeafc26aac0be8aff14af7871249e87ea1319be92090bfd632ec68e03b16a5", size = 5232901, upload-time = "2026-04-18T04:33:26.21Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d9/d609a11fb567da9399f525193e2b49847b5a409cdebe737f06a8b7126bdc/lxml-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:264c605ab9c0e4aa1a679636f4582c4d3313700009fac3ec9c3412ed0d8f3e1d", size = 5261333, upload-time = "2026-04-18T04:33:28.984Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3a/ac3f99ec8ac93089e7dd556f279e0d14c24de0a74a507e143a2e4b496e7c/lxml-6.1.0-cp312-cp312-win32.whl", hash = "sha256:56971379bc5ee8037c5a0f09fa88f66cdb7d37c3e38af3e45cf539f41131ac1f", size = 3596289, upload-time = "2026-04-18T04:27:42.819Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a7/0a915557538593cb1bbeedcd40e13c7a261822c26fecbbdb71dad0c2f540/lxml-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:bba078de0031c219e5dd06cf3e6bf8fb8e6e64a77819b358f53bb132e3e03366", size = 3997059, upload-time = "2026-04-18T04:27:46.764Z" }, + { url = "https://files.pythonhosted.org/packages/92/96/a5dc078cf0126fbfbc35611d77ecd5da80054b5893e28fb213a5613b9e1d/lxml-6.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:c3592631e652afa34999a088f98ba7dfc7d6aff0d535c410bea77a71743f3819", size = 3659552, upload-time = "2026-04-18T04:27:51.133Z" }, + { url = "https://files.pythonhosted.org/packages/f2/88/55143966481409b1740a3ac669e611055f49efd68087a5ce41582325db3e/lxml-6.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:546b66c0dd1bb8d9fa89d7123e5fa19a8aff3a1f2141eb22df96112afb17b842", size = 3930134, upload-time = "2026-04-18T04:32:35.008Z" }, + { url = "https://files.pythonhosted.org/packages/b5/97/28b985c2983938d3cb696dd5501423afb90a8c3e869ef5d3c62569282c0f/lxml-6.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5cfa1a34df366d9dc0d5eaf420f4cf2bb1e1bebe1066d1c2fc28c179f8a4004c", size = 4210749, upload-time = "2026-04-18T04:36:03.626Z" }, + { url = "https://files.pythonhosted.org/packages/29/67/dfab2b7d58214921935ccea7ce9b3df9b7d46f305d12f0f532ac7cf6b804/lxml-6.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db88156fcf544cdbf0d95588051515cfdfd4c876fc66444eb98bceb5d6db76de", size = 4318463, upload-time = "2026-04-18T04:36:06.309Z" }, + { url = "https://files.pythonhosted.org/packages/32/a2/4ac7eb32a4d997dd352c32c32399aae27b3f268d440e6f9cfa405b575d2f/lxml-6.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:07f98f5496f96bf724b1e3c933c107f0cbf2745db18c03d2e13a291c3afd2635", size = 4251124, upload-time = "2026-04-18T04:36:09.056Z" }, + { url = "https://files.pythonhosted.org/packages/33/ef/d6abd850bb4822f9b720cfe36b547a558e694881010ff7d012191e8769c6/lxml-6.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4642e04449a1e164b5ff71ffd901ddb772dfabf5c9adf1b7be5dffe1212bc037", size = 4401758, upload-time = "2026-04-18T04:36:11.803Z" }, + { url = "https://files.pythonhosted.org/packages/40/44/3ee09a5b60cb44c4f2fbc1c9015cfd6ff5afc08f991cab295d3024dcbf2d/lxml-6.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:7da13bb6fbadfafb474e0226a30570a3445cfd47c86296f2446dafbd77079ace", size = 3508860, upload-time = "2026-04-18T04:32:48.619Z" }, +] + +[[package]] +name = "lz4" +version = "4.4.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/51/f1b86d93029f418033dddf9b9f79c8d2641e7454080478ee2aab5123173e/lz4-4.4.5.tar.gz", hash = "sha256:5f0b9e53c1e82e88c10d7c180069363980136b9d7a8306c4dca4f760d60c39f0", size = 172886, upload-time = "2025-11-03T13:02:36.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/5b/6edcd23319d9e28b1bedf32768c3d1fd56eed8223960a2c47dacd2cec2af/lz4-4.4.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d6da84a26b3aa5da13a62e4b89ab36a396e9327de8cd48b436a3467077f8ccd4", size = 207391, upload-time = "2025-11-03T13:01:36.644Z" }, + { url = "https://files.pythonhosted.org/packages/34/36/5f9b772e85b3d5769367a79973b8030afad0d6b724444083bad09becd66f/lz4-4.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61d0ee03e6c616f4a8b69987d03d514e8896c8b1b7cc7598ad029e5c6aedfd43", size = 207146, upload-time = "2025-11-03T13:01:37.928Z" }, + { url = "https://files.pythonhosted.org/packages/04/f4/f66da5647c0d72592081a37c8775feacc3d14d2625bbdaabd6307c274565/lz4-4.4.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:33dd86cea8375d8e5dd001e41f321d0a4b1eb7985f39be1b6a4f466cd480b8a7", size = 1292623, upload-time = "2025-11-03T13:01:39.341Z" }, + { url = "https://files.pythonhosted.org/packages/85/fc/5df0f17467cdda0cad464a9197a447027879197761b55faad7ca29c29a04/lz4-4.4.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:609a69c68e7cfcfa9d894dc06be13f2e00761485b62df4e2472f1b66f7b405fb", size = 1279982, upload-time = "2025-11-03T13:01:40.816Z" }, + { url = "https://files.pythonhosted.org/packages/25/3b/b55cb577aa148ed4e383e9700c36f70b651cd434e1c07568f0a86c9d5fbb/lz4-4.4.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:75419bb1a559af00250b8f1360d508444e80ed4b26d9d40ec5b09fe7875cb989", size = 1368674, upload-time = "2025-11-03T13:01:42.118Z" }, + { url = "https://files.pythonhosted.org/packages/fb/31/e97e8c74c59ea479598e5c55cbe0b1334f03ee74ca97726e872944ed42df/lz4-4.4.5-cp311-cp311-win32.whl", hash = "sha256:12233624f1bc2cebc414f9efb3113a03e89acce3ab6f72035577bc61b270d24d", size = 88168, upload-time = "2025-11-03T13:01:43.282Z" }, + { url = "https://files.pythonhosted.org/packages/18/47/715865a6c7071f417bef9b57c8644f29cb7a55b77742bd5d93a609274e7e/lz4-4.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:8a842ead8ca7c0ee2f396ca5d878c4c40439a527ebad2b996b0444f0074ed004", size = 99491, upload-time = "2025-11-03T13:01:44.167Z" }, + { url = "https://files.pythonhosted.org/packages/14/e7/ac120c2ca8caec5c945e6356ada2aa5cfabd83a01e3170f264a5c42c8231/lz4-4.4.5-cp311-cp311-win_arm64.whl", hash = "sha256:83bc23ef65b6ae44f3287c38cbf82c269e2e96a26e560aa551735883388dcc4b", size = 91271, upload-time = "2025-11-03T13:01:45.016Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ac/016e4f6de37d806f7cc8f13add0a46c9a7cfc41a5ddc2bc831d7954cf1ce/lz4-4.4.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:df5aa4cead2044bab83e0ebae56e0944cc7fcc1505c7787e9e1057d6d549897e", size = 207163, upload-time = "2025-11-03T13:01:45.895Z" }, + { url = "https://files.pythonhosted.org/packages/8d/df/0fadac6e5bd31b6f34a1a8dbd4db6a7606e70715387c27368586455b7fc9/lz4-4.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d0bf51e7745484d2092b3a51ae6eb58c3bd3ce0300cf2b2c14f76c536d5697a", size = 207150, upload-time = "2025-11-03T13:01:47.205Z" }, + { url = "https://files.pythonhosted.org/packages/b7/17/34e36cc49bb16ca73fb57fbd4c5eaa61760c6b64bce91fcb4e0f4a97f852/lz4-4.4.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7b62f94b523c251cf32aa4ab555f14d39bd1a9df385b72443fd76d7c7fb051f5", size = 1292045, upload-time = "2025-11-03T13:01:48.667Z" }, + { url = "https://files.pythonhosted.org/packages/90/1c/b1d8e3741e9fc89ed3b5f7ef5f22586c07ed6bb04e8343c2e98f0fa7ff04/lz4-4.4.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c3ea562c3af274264444819ae9b14dbbf1ab070aff214a05e97db6896c7597e", size = 1279546, upload-time = "2025-11-03T13:01:50.159Z" }, + { url = "https://files.pythonhosted.org/packages/55/d9/e3867222474f6c1b76e89f3bd914595af69f55bf2c1866e984c548afdc15/lz4-4.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24092635f47538b392c4eaeff14c7270d2c8e806bf4be2a6446a378591c5e69e", size = 1368249, upload-time = "2025-11-03T13:01:51.273Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e7/d667d337367686311c38b580d1ca3d5a23a6617e129f26becd4f5dc458df/lz4-4.4.5-cp312-cp312-win32.whl", hash = "sha256:214e37cfe270948ea7eb777229e211c601a3e0875541c1035ab408fbceaddf50", size = 88189, upload-time = "2025-11-03T13:01:52.605Z" }, + { url = "https://files.pythonhosted.org/packages/a5/0b/a54cd7406995ab097fceb907c7eb13a6ddd49e0b231e448f1a81a50af65c/lz4-4.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:713a777de88a73425cf08eb11f742cd2c98628e79a8673d6a52e3c5f0c116f33", size = 99497, upload-time = "2025-11-03T13:01:53.477Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7e/dc28a952e4bfa32ca16fa2eb026e7a6ce5d1411fcd5986cd08c74ec187b9/lz4-4.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:a88cbb729cc333334ccfb52f070463c21560fca63afcf636a9f160a55fac3301", size = 91279, upload-time = "2025-11-03T13:01:54.419Z" }, +] + +[[package]] +name = "markdown" +version = "3.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, +] + +[[package]] +name = "marshmallow" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/7e/1dbd4096eb7c148cd2841841916f78820bb85a4d80a0c25c02d30815a7fb/marshmallow-4.3.0.tar.gz", hash = "sha256:fb43c53b3fe240b8f6af37223d6ef1636f927ad9bea8ab323afad95dff090880", size = 224485, upload-time = "2026-04-03T21:46:32.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/e0/ff24e25218bb59eb6290a530cea40651b14068b6e3659b20f9c175179632/marshmallow-4.3.0-py3-none-any.whl", hash = "sha256:46c4fe6984707e3cbd485dfebbf0a59874f58d695aad05c1668d15e8c6e13b46", size = 49148, upload-time = "2026-04-03T21:46:31.241Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/86/de7e3a1cdcfc941483af70609edc06b83e7c8a0e0dc9ac325200a3f4d220/matplotlib-3.10.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", size = 8251215, upload-time = "2025-12-10T22:55:16.175Z" }, + { url = "https://files.pythonhosted.org/packages/fd/14/baad3222f424b19ce6ad243c71de1ad9ec6b2e4eb1e458a48fdc6d120401/matplotlib-3.10.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", size = 8139625, upload-time = "2025-12-10T22:55:17.712Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a0/7024215e95d456de5883e6732e708d8187d9753a21d32f8ddb3befc0c445/matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", size = 8712614, upload-time = "2025-12-10T22:55:20.8Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f4/b8347351da9a5b3f41e26cf547252d861f685c6867d179a7c9d60ad50189/matplotlib-3.10.8-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2", size = 9540997, upload-time = "2025-12-10T22:55:23.258Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c0/c7b914e297efe0bc36917bf216b2acb91044b91e930e878ae12981e461e5/matplotlib-3.10.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6", size = 9596825, upload-time = "2025-12-10T22:55:25.217Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d3/a4bbc01c237ab710a1f22b4da72f4ff6d77eb4c7735ea9811a94ae239067/matplotlib-3.10.8-cp311-cp311-win_amd64.whl", hash = "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9", size = 8135090, upload-time = "2025-12-10T22:55:27.162Z" }, + { url = "https://files.pythonhosted.org/packages/89/dd/a0b6588f102beab33ca6f5218b31725216577b2a24172f327eaf6417d5c9/matplotlib-3.10.8-cp311-cp311-win_arm64.whl", hash = "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2", size = 8012377, upload-time = "2025-12-10T22:55:29.185Z" }, + { url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453, upload-time = "2025-12-10T22:55:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321, upload-time = "2025-12-10T22:55:33.265Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944, upload-time = "2025-12-10T22:55:34.922Z" }, + { url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099, upload-time = "2025-12-10T22:55:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040, upload-time = "2025-12-10T22:55:38.715Z" }, + { url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717, upload-time = "2025-12-10T22:55:41.103Z" }, + { url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" }, + { url = "https://files.pythonhosted.org/packages/04/30/3afaa31c757f34b7725ab9d2ba8b48b5e89c2019c003e7d0ead143aabc5a/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", size = 8249198, upload-time = "2025-12-10T22:56:45.584Z" }, + { url = "https://files.pythonhosted.org/packages/48/2f/6334aec331f57485a642a7c8be03cb286f29111ae71c46c38b363230063c/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", size = 8136817, upload-time = "2025-12-10T22:56:47.339Z" }, + { url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867, upload-time = "2025-12-10T22:56:48.954Z" }, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "microsoft-kiota-abstractions" +version = "1.9.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, + { name = "std-uritemplate" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/94/37315b82a1bcc08145e5bc2af7396a4be8160ac138ec269611c3b9589b7a/microsoft_kiota_abstractions-1.9.9.tar.gz", hash = "sha256:5df9a8e0517a4568726c2cac6d9789284cc6ffa66043b68eba42ae55749fb861", size = 24468, upload-time = "2026-03-02T21:03:50.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/6a/7d5a1a8131f0eccc6b45839c091aa00ba29661854e7defaa7936cf342fa7/microsoft_kiota_abstractions-1.9.9-py3-none-any.whl", hash = "sha256:8d0a14eda42f3f0ccac2e9512227a338f69998dc9b782fd21cb8ca7c48302caa", size = 44453, upload-time = "2026-03-02T21:03:51.11Z" }, +] + +[[package]] +name = "microsoft-kiota-authentication-azure" +version = "1.9.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "azure-core" }, + { name = "microsoft-kiota-abstractions" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/ce/5ae8b37ee4a50f0ed5e092c2d0105d60b592e6102a190959f76658a0994c/microsoft_kiota_authentication_azure-1.9.9.tar.gz", hash = "sha256:aca5e7dc8a0a28224f9025a479349ac2f9aaf166bfd6bc707f232658b45eec28", size = 5000, upload-time = "2026-03-02T21:04:02.355Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/de/dc504324b776d00a420886cc6f39e04be2cf48cab0e9b18f8450a5efcc29/microsoft_kiota_authentication_azure-1.9.9-py3-none-any.whl", hash = "sha256:73dc21a1a2861ea78a135327291db3322e2255542a18b311dd03fd908342e902", size = 6951, upload-time = "2026-03-02T21:04:03.18Z" }, +] + +[[package]] +name = "microsoft-kiota-http" +version = "1.9.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx", extra = ["http2"] }, + { name = "microsoft-kiota-abstractions" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/3f/fc18eb0d1d845daf6355fd54fd990af7f7e10043ef6a6da39b9e5981cbaf/microsoft_kiota_http-1.9.9.tar.gz", hash = "sha256:ae672b145df71b644f8da0951767a12a4ce47a40576d86eba19b7c22d9e160f9", size = 21493, upload-time = "2026-03-02T21:04:11.662Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/6a/cc1b1055b4b6d4dfc1be7a71917c2f0ef19c070c6a18b16d3c1032d20925/microsoft_kiota_http-1.9.9-py3-none-any.whl", hash = "sha256:a5b1b217ac9afeb4054f12515417e3b1d2be12a9385a70a41d18d64379ea2e7e", size = 31945, upload-time = "2026-03-02T21:04:12.328Z" }, +] + +[[package]] +name = "microsoft-kiota-serialization-form" +version = "1.9.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "microsoft-kiota-abstractions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/b4/18e9fce60a30c8b6ea0a6278fb81cf352127340d48df2d7c52ff1b579488/microsoft_kiota_serialization_form-1.9.9.tar.gz", hash = "sha256:3cdc8b172baec5b5282af72f2ce02715edcd23252ce0b5af96075256edd75114", size = 9015, upload-time = "2026-03-02T21:04:20.39Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/24/eb8436b882f1473bd0a868848d214df3df2d9b3db8e5422d111032f1114f/microsoft_kiota_serialization_form-1.9.9-py3-none-any.whl", hash = "sha256:1c426d4f0d463fc9215c41d7fa0f3dc5fe8d3c80573d555cf63ea67000148d84", size = 10718, upload-time = "2026-03-02T21:04:21.25Z" }, +] + +[[package]] +name = "microsoft-kiota-serialization-json" +version = "1.9.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "microsoft-kiota-abstractions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b8/2f/d36eba916c00136da122d1701acb862c5b1f2e22b6dc6fa4e0f4abda2786/microsoft_kiota_serialization_json-1.9.9.tar.gz", hash = "sha256:9b27479427f49bbac15ead8e8ff0176e47fcdf81153611acc408f5f399342079", size = 9545, upload-time = "2026-03-02T21:04:29.177Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/7b/b3f606ef2dcbdebe12ae27004ed6e7542370cb2494265f11a8877a1de2d1/microsoft_kiota_serialization_json-1.9.9-py3-none-any.whl", hash = "sha256:bb80b93e81bab41dc142e9b254f79bf0b7b9fe49a796ca0c8e8691925bd3967f", size = 11210, upload-time = "2026-03-02T21:04:29.844Z" }, +] + +[[package]] +name = "microsoft-kiota-serialization-multipart" +version = "1.9.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "microsoft-kiota-abstractions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/44/24087f0fac7c5682c13c7fb61468a0c5a5185b9f243de3a99309aa6fcaa7/microsoft_kiota_serialization_multipart-1.9.9.tar.gz", hash = "sha256:f8730be6da5f6c63a6bf4ea310a9723b9998a47a04745887dc156d08f119a829", size = 5162, upload-time = "2026-03-02T21:04:48.1Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/db/6b988fdf771c3d07dff4a116176d575832daf2a43823444d145d71da5b61/microsoft_kiota_serialization_multipart-1.9.9-py3-none-any.whl", hash = "sha256:572e9cbafa2eb946452cdadfb019a4e9245768c0d61c3089d3436d4f5106c550", size = 6696, upload-time = "2026-03-02T21:04:48.98Z" }, +] + +[[package]] +name = "microsoft-kiota-serialization-text" +version = "1.9.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "microsoft-kiota-abstractions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/3c/d244ad08e03003134871698aa54de8243bcc61c0faf3ab114293bb76d6ad/microsoft_kiota_serialization_text-1.9.9.tar.gz", hash = "sha256:18bc0764dda4078a4c953300253344e05d0cdb9c17136f1a2f695d438cedb402", size = 7325, upload-time = "2026-03-02T21:04:37.567Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/f8/43f8d00fed6e090810d3ce0c05e06c23eaa5dee6e87ab1fb89d96ca9559f/microsoft_kiota_serialization_text-1.9.9-py3-none-any.whl", hash = "sha256:84418119d4929a76fde7f31e957e240e003bf145757838b9aa3a0f36dec1b789", size = 8885, upload-time = "2026-03-02T21:04:38.76Z" }, +] + +[[package]] +name = "microsoft-security-utilities-secret-masker" +version = "1.0.0b4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/1a/6fa5c0ba55ed62e17df010af8a3a71ffea701c3d414b4688834c527d5aeb/microsoft_security_utilities_secret_masker-1.0.0b4.tar.gz", hash = "sha256:a30bd361ac18c8b52f6844076bc26465335949ea9c7a004d95f5196ec6fdef3e", size = 10919, upload-time = "2025-03-10T05:46:54.631Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/4b/69befea52f4623da5356e46cb3fc020525d35ceb766a428abb4919628cdc/microsoft_security_utilities_secret_masker-1.0.0b4-py3-none-any.whl", hash = "sha256:0429fcaad10fc8ae3f940ab84fd2926e4f50ede134162144123b35937be831a8", size = 16678, upload-time = "2025-03-10T05:46:55.43Z" }, +] + +[[package]] +name = "msal" +version = "1.35.0b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/7a/6880016fab1720981b54db844c32af6f2e5e90aac21575ad6e54e1840313/msal-1.35.0b1.tar.gz", hash = "sha256:fe8143079183a5c952cd9f3ba66a148fe7bae9fb9952bd0e834272bfbeb34508", size = 157573, upload-time = "2026-01-06T23:51:56.958Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/8e/7090fafcf58e9081767a8fa960431c708211ce273bc4f6e519e9046acacc/msal-1.35.0b1-py3-none-any.whl", hash = "sha256:bf656775c64bbc2103d8255980f5c3c966c7432106795e1fe70ca338a7e43150", size = 117733, upload-time = "2026-01-06T23:51:55.903Z" }, +] + +[package.optional-dependencies] +broker = [ + { name = "pymsalruntime", marker = "sys_platform == 'win32'" }, +] + +[[package]] +name = "msal-extensions" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "msal" }, + { name = "portalocker" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2d/38/ad49272d0a5af95f7a0cb64a79bbd75c9c187f3b789385a143d8d537a5eb/msal_extensions-1.2.0.tar.gz", hash = "sha256:6f41b320bfd2933d631a215c91ca0dd3e67d84bd1a2f50ce917d5874ec646bef", size = 22391, upload-time = "2024-06-23T02:15:37.702Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/69/314d887a01599669fb330da14e5c6ff5f138609e322812a942a74ef9b765/msal_extensions-1.2.0-py3-none-any.whl", hash = "sha256:cf5ba83a2113fa6dc011a254a72f1c223c88d7dfad74cc30617c4679a417704d", size = 19254, upload-time = "2024-06-23T02:15:36.584Z" }, +] + +[[package]] +name = "msgraph-core" +version = "1.3.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx", extra = ["http2"] }, + { name = "microsoft-kiota-abstractions" }, + { name = "microsoft-kiota-authentication-azure" }, + { name = "microsoft-kiota-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/4e/123f9530ec43b306c597bb830c62bedab830ffa76e0edf33ea88a26f756e/msgraph_core-1.3.8.tar.gz", hash = "sha256:6e883f9d4c4ad57501234749e07b010478c1a5f19550ef4cf005bbcac4a63ae7", size = 25506, upload-time = "2025-09-11T22:46:57.267Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/4d/01432f60727ae452787014cad0d5bc9e035c6e11a670f12c23f7fc926d90/msgraph_core-1.3.8-py3-none-any.whl", hash = "sha256:86d83edcf62119946f201d13b7e857c947ef67addb088883940197081de85bea", size = 34473, upload-time = "2025-09-11T22:46:56.026Z" }, +] + +[[package]] +name = "msgraph-sdk" +version = "1.55.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-identity" }, + { name = "microsoft-kiota-serialization-form" }, + { name = "microsoft-kiota-serialization-json" }, + { name = "microsoft-kiota-serialization-multipart" }, + { name = "microsoft-kiota-serialization-text" }, + { name = "msgraph-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/44/0b5a188addf6341b3da10dd207e444417de255f7c1651902ba72016a2843/msgraph_sdk-1.55.0.tar.gz", hash = "sha256:6df691a31954a050d26b8a678968017e157d940fb377f2a8a4e17a9741b98756", size = 6295669, upload-time = "2026-02-20T00:32:29.378Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/a8/de807e62f8ff93003b573aa243cdcee2da2c0618b42efbc9a8e61aa7300d/msgraph_sdk-1.55.0-py3-none-any.whl", hash = "sha256:c8e68ebc4b88af5111de312e7fa910a4e76ddf48a4534feadb1fb8a411c48cfc", size = 25758742, upload-time = "2026-02-20T00:30:40.039Z" }, +] + +[[package]] +name = "msrest" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core" }, + { name = "certifi" }, + { name = "isodate" }, + { name = "requests" }, + { name = "requests-oauthlib" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/77/8397c8fb8fc257d8ea0fa66f8068e073278c65f05acb17dcb22a02bfdc42/msrest-0.7.1.zip", hash = "sha256:6e7661f46f3afd88b75667b7187a92829924446c7ea1d169be8c4bb7eeb788b9", size = 175332, upload-time = "2022-06-13T22:41:25.111Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/cf/f2966a2638144491f8696c27320d5219f48a072715075d168b31d3237720/msrest-0.7.1-py3-none-any.whl", hash = "sha256:21120a810e1233e5e6cc7fe40b474eeb4ec6f757a15d7cf86702c369f9567c32", size = 85384, upload-time = "2022-06-13T22:41:22.42Z" }, +] + +[[package]] +name = "msrestazure" +version = "0.6.4.post1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "adal" }, + { name = "msrest" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/86/06a086e4ed3523765a1917665257b1828f1bf882130768445f082a4c3484/msrestazure-0.6.4.post1.tar.gz", hash = "sha256:39842007569e8c77885ace5c46e4bf2a9108fcb09b1e6efdf85b6e2c642b55d4", size = 47728, upload-time = "2024-04-10T21:00:51.79Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/7e/620e883def84ae56b8a9da382d960f7f801e37518fe930085cf72c148dae/msrestazure-0.6.4.post1-py2.py3-none-any.whl", hash = "sha256:2264493b086c2a0a82ddf5fd87b35b3fffc443819127fed992ac5028354c151e", size = 40789, upload-time = "2024-04-10T21:00:49.359Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626, upload-time = "2026-01-26T02:43:26.485Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", size = 44706, upload-time = "2026-01-26T02:43:27.607Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356, upload-time = "2026-01-26T02:43:28.661Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d2/0a36c8473f0cbaeadd5db6c8b72d15bbceeec275807772bfcd059bef487d/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", size = 244355, upload-time = "2026-01-26T02:43:31.165Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", size = 246433, upload-time = "2026-01-26T02:43:32.581Z" }, + { url = "https://files.pythonhosted.org/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", size = 225376, upload-time = "2026-01-26T02:43:34.417Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", size = 257365, upload-time = "2026-01-26T02:43:35.741Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", size = 254747, upload-time = "2026-01-26T02:43:36.976Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", size = 246293, upload-time = "2026-01-26T02:43:38.258Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", size = 242962, upload-time = "2026-01-26T02:43:40.034Z" }, + { url = "https://files.pythonhosted.org/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", size = 237360, upload-time = "2026-01-26T02:43:41.752Z" }, + { url = "https://files.pythonhosted.org/packages/64/3f/036dfc8c174934d4b55d86ff4f978e558b0e585cef70cfc1ad01adc6bf18/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", size = 245940, upload-time = "2026-01-26T02:43:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", size = 253502, upload-time = "2026-01-26T02:43:44.371Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", size = 247065, upload-time = "2026-01-26T02:43:45.745Z" }, + { url = "https://files.pythonhosted.org/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", size = 241870, upload-time = "2026-01-26T02:43:47.054Z" }, + { url = "https://files.pythonhosted.org/packages/13/bf/9676c0392309b5fdae322333d22a829715b570edb9baa8016a517b55b558/multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", size = 41302, upload-time = "2026-01-26T02:43:48.753Z" }, + { url = "https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", size = 45981, upload-time = "2026-01-26T02:43:49.921Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/9dd5305253fa00cd3c7555dbef69d5bf4133debc53b87ab8d6a44d411665/multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", size = 43159, upload-time = "2026-01-26T02:43:51.635Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + +[[package]] +name = "mypy" +version = "1.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/b9/81e4c6dbb1ec1e72503de3ff2c5fe4b7f224e04613b670f8b9004cd8a4dd/mypy-1.10.1.tar.gz", hash = "sha256:1f8f492d7db9e3593ef42d4f115f04e556130f2819ad33ab84551403e97dd4c0", size = 3022304, upload-time = "2024-06-25T00:12:23.941Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/cf/0645128c6edf70eb9b9687ad42fcb61ea344a7927ed2b78ce2275282fe87/mypy-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bd6f629b67bb43dc0d9211ee98b96d8dabc97b1ad38b9b25f5e4c4d7569a0c6a", size = 10740526, upload-time = "2024-06-25T00:10:58.841Z" }, + { url = "https://files.pythonhosted.org/packages/19/c9/10842953066265e6063c41a85bbee3b877501947c970ea84a1db5f11d32e/mypy-1.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1bbb3a6f5ff319d2b9d40b4080d46cd639abe3516d5a62c070cf0114a457d84", size = 9898375, upload-time = "2024-06-25T00:11:54.767Z" }, + { url = "https://files.pythonhosted.org/packages/e4/9e/551e897f67c5d67aa1976bc3b4951f297d1daf07250c421bb045b2613350/mypy-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8edd4e9bbbc9d7b79502eb9592cab808585516ae1bcc1446eb9122656c6066f", size = 12602338, upload-time = "2024-06-25T00:10:55.898Z" }, + { url = "https://files.pythonhosted.org/packages/2b/a4/55e3635253e5fa7051674dd5a67582f08b0ba8823e1fdbf7241ed5b32d4e/mypy-1.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6166a88b15f1759f94a46fa474c7b1b05d134b1b61fca627dd7335454cc9aa6b", size = 12680741, upload-time = "2024-06-25T00:11:39.882Z" }, + { url = "https://files.pythonhosted.org/packages/7a/cc/aa881ad051f99915887db0b5de8facc0e224295be22f92178c8f77fd8359/mypy-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:5bb9cd11c01c8606a9d0b83ffa91d0b236a0e91bc4126d9ba9ce62906ada868e", size = 9393661, upload-time = "2024-06-25T00:11:11.966Z" }, + { url = "https://files.pythonhosted.org/packages/5d/86/3c3bdaccc3cbd1372acb15667a2c2cb773523a710a22e2748cbda9a7c1e2/mypy-1.10.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d8681909f7b44d0b7b86e653ca152d6dff0eb5eb41694e163c6092124f8246d7", size = 10864022, upload-time = "2024-06-25T00:11:01.838Z" }, + { url = "https://files.pythonhosted.org/packages/ec/05/7c87b26b6a769b70f6c0b8a6daef01fc6f3ae566df89a2fa9d04f690b0d3/mypy-1.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:378c03f53f10bbdd55ca94e46ec3ba255279706a6aacaecac52ad248f98205d3", size = 9857795, upload-time = "2024-06-25T00:12:17.799Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b5/cbccba4dca9703c4c467171e7f61ea6a1a75eae991208aa5bc7d49807f91/mypy-1.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bacf8f3a3d7d849f40ca6caea5c055122efe70e81480c8328ad29c55c69e93e", size = 12647633, upload-time = "2024-06-25T00:10:42.207Z" }, + { url = "https://files.pythonhosted.org/packages/02/3c/1f5e57c8cfab4299f7189821ae8bb4896e8e623a04d293fd32e32eb0e617/mypy-1.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:701b5f71413f1e9855566a34d6e9d12624e9e0a8818a5704d74d6b0402e66c04", size = 12730251, upload-time = "2024-06-25T00:12:14.738Z" }, + { url = "https://files.pythonhosted.org/packages/f9/20/d33608e8dc3bc0f5966fc1f6c2d16671f0725dcca279beec47c3e19afd9d/mypy-1.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c4c2992f6ea46ff7fce0072642cfb62af7a2484efe69017ed8b095f7b39ef31", size = 9491734, upload-time = "2024-06-25T00:11:05.118Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ee/d53a3d4792a09b6cd757978951d6dcf8b10825a8b8522b68e9b5eb53b9a1/mypy-1.10.1-py3-none-any.whl", hash = "sha256:71d8ac0b906354ebda8ef1673e5fde785936ac1f29ff6987c7483cfbd5a4235a", size = 2580108, upload-time = "2024-06-25T00:12:08.18Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "narwhals" +version = "2.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/6f/713be67779028d482c6e0f2dde5bc430021b2578a4808c1c9f6d7ad48257/narwhals-2.16.0.tar.gz", hash = "sha256:155bb45132b370941ba0396d123cf9ed192bf25f39c4cea726f2da422ca4e145", size = 618268, upload-time = "2026-02-02T10:31:00.545Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/cc/7cb74758e6df95e0c4e1253f203b6dd7f348bf2f29cf89e9210a2416d535/narwhals-2.16.0-py3-none-any.whl", hash = "sha256:846f1fd7093ac69d63526e50732033e86c30ea0026a44d9b23991010c7d1485d", size = 443951, upload-time = "2026-02-02T10:30:58.635Z" }, +] + +[[package]] +name = "neo4j" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/01/d6ce65e4647f6cb2b9cca3b813978f7329b54b4e36660aaec1ddf0ccce7a/neo4j-6.1.0.tar.gz", hash = "sha256:b5dde8c0d8481e7b6ae3733569d990dd3e5befdc5d452f531ad1884ed3500b84", size = 239629, upload-time = "2026-01-12T11:27:34.777Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/5c/ee71e2dd955045425ef44283f40ba1da67673cf06404916ca2950ac0cd39/neo4j-6.1.0-py3-none-any.whl", hash = "sha256:3bd93941f3a3559af197031157220af9fd71f4f93a311db687bd69ffa417b67d", size = 325326, upload-time = "2026-01-12T11:27:33.196Z" }, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, +] + +[[package]] +name = "numpy" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/75/10dd1f8116a8b796cb2c737b674e02d02e80454bda953fa7e65d8c12b016/numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78", size = 18902015, upload-time = "2024-08-26T20:19:40.945Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/cf/034500fb83041aa0286e0fb16e7c76e5c8b67c0711bb6e9e9737a717d5fe/numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448", size = 21169137, upload-time = "2024-08-26T20:07:45.345Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d9/32de45561811a4b87fbdee23b5797394e3d1504b4a7cf40c10199848893e/numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195", size = 13703552, upload-time = "2024-08-26T20:08:06.666Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ca/2f384720020c7b244d22508cb7ab23d95f179fcfff33c31a6eeba8d6c512/numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57", size = 5298957, upload-time = "2024-08-26T20:08:15.83Z" }, + { url = "https://files.pythonhosted.org/packages/0e/78/a3e4f9fb6aa4e6fdca0c5428e8ba039408514388cf62d89651aade838269/numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a", size = 6905573, upload-time = "2024-08-26T20:08:27.185Z" }, + { url = "https://files.pythonhosted.org/packages/a0/72/cfc3a1beb2caf4efc9d0b38a15fe34025230da27e1c08cc2eb9bfb1c7231/numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669", size = 13914330, upload-time = "2024-08-26T20:08:48.058Z" }, + { url = "https://files.pythonhosted.org/packages/ba/a8/c17acf65a931ce551fee11b72e8de63bf7e8a6f0e21add4c937c83563538/numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951", size = 19534895, upload-time = "2024-08-26T20:09:16.536Z" }, + { url = "https://files.pythonhosted.org/packages/ba/86/8767f3d54f6ae0165749f84648da9dcc8cd78ab65d415494962c86fac80f/numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9", size = 19937253, upload-time = "2024-08-26T20:09:46.263Z" }, + { url = "https://files.pythonhosted.org/packages/df/87/f76450e6e1c14e5bb1eae6836478b1028e096fd02e85c1c37674606ab752/numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15", size = 14414074, upload-time = "2024-08-26T20:10:08.483Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/0f0f328e1e59f73754f06e1adfb909de43726d4f24c6a3f8805f34f2b0fa/numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4", size = 6470640, upload-time = "2024-08-26T20:10:19.732Z" }, + { url = "https://files.pythonhosted.org/packages/eb/57/3a3f14d3a759dcf9bf6e9eda905794726b758819df4663f217d658a58695/numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc", size = 15910230, upload-time = "2024-08-26T20:10:43.413Z" }, + { url = "https://files.pythonhosted.org/packages/45/40/2e117be60ec50d98fa08c2f8c48e09b3edea93cfcabd5a9ff6925d54b1c2/numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b", size = 20895803, upload-time = "2024-08-26T20:11:13.916Z" }, + { url = "https://files.pythonhosted.org/packages/46/92/1b8b8dee833f53cef3e0a3f69b2374467789e0bb7399689582314df02651/numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e", size = 13471835, upload-time = "2024-08-26T20:11:34.779Z" }, + { url = "https://files.pythonhosted.org/packages/7f/19/e2793bde475f1edaea6945be141aef6c8b4c669b90c90a300a8954d08f0a/numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c", size = 5038499, upload-time = "2024-08-26T20:11:43.902Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ff/ddf6dac2ff0dd50a7327bcdba45cb0264d0e96bb44d33324853f781a8f3c/numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c", size = 6633497, upload-time = "2024-08-26T20:11:55.09Z" }, + { url = "https://files.pythonhosted.org/packages/72/21/67f36eac8e2d2cd652a2e69595a54128297cdcb1ff3931cfc87838874bd4/numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692", size = 13621158, upload-time = "2024-08-26T20:12:14.95Z" }, + { url = "https://files.pythonhosted.org/packages/39/68/e9f1126d757653496dbc096cb429014347a36b228f5a991dae2c6b6cfd40/numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a", size = 19236173, upload-time = "2024-08-26T20:12:44.049Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e9/1f5333281e4ebf483ba1c888b1d61ba7e78d7e910fdd8e6499667041cc35/numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c", size = 19634174, upload-time = "2024-08-26T20:13:13.634Z" }, + { url = "https://files.pythonhosted.org/packages/71/af/a469674070c8d8408384e3012e064299f7a2de540738a8e414dcfd639996/numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded", size = 14099701, upload-time = "2024-08-26T20:13:34.851Z" }, + { url = "https://files.pythonhosted.org/packages/d0/3d/08ea9f239d0e0e939b6ca52ad403c84a2bce1bde301a8eb4888c1c1543f1/numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5", size = 6174313, upload-time = "2024-08-26T20:13:45.653Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b5/4ac39baebf1fdb2e72585c8352c56d063b6126be9fc95bd2bb5ef5770c20/numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a", size = 15606179, upload-time = "2024-08-26T20:14:08.786Z" }, +] + +[[package]] +name = "oauthlib" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, +] + +[[package]] +name = "oci" +version = "2.169.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "circuitbreaker" }, + { name = "cryptography" }, + { name = "pyopenssl" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/f4/3c2eddccc75dd06a692dbb3290f20f4bc733d99dc60de21f22d65efdeae4/oci-2.169.0.tar.gz", hash = "sha256:f3c5fff00b01783b5325ea7b13bf140053ec1e9f41da20bfb9c8a349ee7662fa", size = 16885837, upload-time = "2026-03-31T06:14:58.981Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/bf/19643bd939ab595193779ee25c2c12aef8e9a54e0a68de5ed79f209702e3/oci-2.169.0-py3-none-any.whl", hash = "sha256:c71bb5143f307791082b3e33cc1545c2490a518cfed85ab1948ef5107c36d30b", size = 34460447, upload-time = "2026-03-31T06:14:51.373Z" }, +] + +[[package]] +name = "okta" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aenum" }, + { name = "aiohttp" }, + { name = "blinker" }, + { name = "jwcrypto" }, + { name = "pycryptodomex" }, + { name = "pydantic" }, + { name = "pydash" }, + { name = "pyjwt" }, + { name = "python-dateutil" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "xmltodict" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/58/f38bc707e51c7ca6b745c87ff7eacf1c2cf4591582062eea5d5700b00837/okta-3.4.2.tar.gz", hash = "sha256:b05201056f3f028c5d2d16394f9b47024a689080f5a993c11d4d80f0e1b5ba1e", size = 2399661, upload-time = "2026-04-15T19:22:46.691Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/81/eeb7ac44656bea4ab462bc5318a1fac5fd5d122dffbf7bba62f444f21ee5/okta-3.4.2-py3-none-any.whl", hash = "sha256:b67bcff31de65223c5848894a202153236d0c99e3a8541a54bf7065f81676637", size = 3907638, upload-time = "2026-04-15T19:22:42.187Z" }, +] + +[[package]] +name = "openai" +version = "1.109.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/a1/a303104dc55fc546a3f6914c842d3da471c64eec92043aef8f652eb6c524/openai-1.109.1.tar.gz", hash = "sha256:d173ed8dbca665892a6db099b4a2dfac624f94d20a93f46eb0b56aae940ed869", size = 564133, upload-time = "2025-09-24T13:00:53.075Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/2a/7dd3d207ec669cacc1f186fd856a0f61dbc255d24f6fdc1a6715d6051b0f/openai-1.109.1-py3-none-any.whl", hash = "sha256:6bcaf57086cf59159b8e27447e4e7dd019db5d29a438072fbd49c290c7e65315", size = 948627, upload-time = "2025-09-24T13:00:50.754Z" }, +] + +[[package]] +name = "openstacksdk" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "decorator" }, + { name = "dogpile-cache" }, + { name = "iso8601" }, + { name = "jmespath" }, + { name = "jsonpatch" }, + { name = "keystoneauth1" }, + { name = "os-service-types" }, + { name = "pbr" }, + { name = "platformdirs" }, + { name = "psutil" }, + { name = "pyyaml" }, + { name = "requestsexceptions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/64/e1a90090a5e892f272b73bec5069b676a422bac9ff24193bdac4b868daa6/openstacksdk-4.2.0.tar.gz", hash = "sha256:5cb9450dcce8054a2caf89d8be9e55057ddfa219a954e781032241eb29280445", size = 1236489, upload-time = "2024-12-16T11:31:21.207Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/2a/0569ac23eb82a376ce1d4695d0defead3080a3ca5050b27392370d2423dd/openstacksdk-4.2.0-py3-none-any.whl", hash = "sha256:238be0fa5d9899872b00787ab38e84f92fd6dc87525fde0965dadcdc12196dc6", size = 1748374, upload-time = "2024-12-16T11:31:16.888Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460, upload-time = "2025-12-11T13:32:49.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c", size = 132565, upload-time = "2025-12-11T13:32:35.069Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.60b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935, upload-time = "2025-12-11T13:32:50.487Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982, upload-time = "2025-12-11T13:32:36.955Z" }, +] + +[[package]] +name = "os-service-types" +version = "1.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pbr" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/62/31e39aa8f2ac5bff0b061ce053f0610c9fe659e12aeca20bfb26d1665024/os_service_types-1.8.2.tar.gz", hash = "sha256:ab7648d7232849943196e1bb00a30e2e25e600fa3b57bb241d15b7f521b5b575", size = 27476, upload-time = "2025-11-21T13:55:47.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/26/0937af7b4383f1eba5bca789b8d191c0e09e59bb64962b18f4a14534ce41/os_service_types-1.8.2-py3-none-any.whl", hash = "sha256:f78890d71814deffabf0ed4358288ec2ced579bc4d0bb87a79ae806cbb4deb6e", size = 24876, upload-time = "2025-11-21T13:55:46.093Z" }, +] + +[[package]] +name = "packageurl-python" +version = "0.17.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/d6/3b5a4e3cfaef7a53869a26ceb034d1ff5e5c27c814ce77260a96d50ab7bb/packageurl_python-0.17.6.tar.gz", hash = "sha256:1252ce3a102372ca6f86eb968e16f9014c4ba511c5c37d95a7f023e2ca6e5c25", size = 50618, upload-time = "2025-11-24T15:20:17.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/2f/c7277b7615a93f51b5fbc1eacfc1b75e8103370e786fd8ce2abf6e5c04ab/packageurl_python-0.17.6-py3-none-any.whl", hash = "sha256:31a85c2717bc41dd818f3c62908685ff9eebcb68588213745b14a6ee9e7df7c9", size = 36776, upload-time = "2025-11-24T15:20:16.962Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pagerduty" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/ad/f8029b94b0f3ae0453eb5125b7aa4a411ea7f5260900d07a1bef9231e939/pagerduty-6.1.0.tar.gz", hash = "sha256:84dfba74f68142c4a71c88af4858f1eb8671e7bc564bc133ac41c59daa7b54f8", size = 43553, upload-time = "2025-11-19T17:30:33.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/70/a2c9869b6848e57c990cd35652a6ab6023e27021df7775eacf32d9d5e75e/pagerduty-6.1.0-py3-none-any.whl", hash = "sha256:ca4954b917cb8e92f83e6b4e18d0f81fdaa73768edb7ad6e859edcc8f950f4eb", size = 53858, upload-time = "2025-11-19T17:30:32.165Z" }, +] + +[[package]] +name = "pandas" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213, upload-time = "2024-09-20T13:10:04.827Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/44/d9502bf0ed197ba9bf1103c9867d5904ddcaf869e52329787fc54ed70cc8/pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039", size = 12602222, upload-time = "2024-09-20T13:08:56.254Z" }, + { url = "https://files.pythonhosted.org/packages/52/11/9eac327a38834f162b8250aab32a6781339c69afe7574368fffe46387edf/pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd", size = 11321274, upload-time = "2024-09-20T13:08:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/45/fb/c4beeb084718598ba19aa9f5abbc8aed8b42f90930da861fcb1acdb54c3a/pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698", size = 15579836, upload-time = "2024-09-20T19:01:57.571Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5f/4dba1d39bb9c38d574a9a22548c540177f78ea47b32f99c0ff2ec499fac5/pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc", size = 13058505, upload-time = "2024-09-20T13:09:01.501Z" }, + { url = "https://files.pythonhosted.org/packages/b9/57/708135b90391995361636634df1f1130d03ba456e95bcf576fada459115a/pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3", size = 16744420, upload-time = "2024-09-20T19:02:00.678Z" }, + { url = "https://files.pythonhosted.org/packages/86/4a/03ed6b7ee323cf30404265c284cee9c65c56a212e0a08d9ee06984ba2240/pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32", size = 14440457, upload-time = "2024-09-20T13:09:04.105Z" }, + { url = "https://files.pythonhosted.org/packages/ed/8c/87ddf1fcb55d11f9f847e3c69bb1c6f8e46e2f40ab1a2d2abadb2401b007/pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5", size = 11617166, upload-time = "2024-09-20T13:09:06.917Z" }, + { url = "https://files.pythonhosted.org/packages/17/a3/fb2734118db0af37ea7433f57f722c0a56687e14b14690edff0cdb4b7e58/pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9", size = 12529893, upload-time = "2024-09-20T13:09:09.655Z" }, + { url = "https://files.pythonhosted.org/packages/e1/0c/ad295fd74bfac85358fd579e271cded3ac969de81f62dd0142c426b9da91/pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4", size = 11363475, upload-time = "2024-09-20T13:09:14.718Z" }, + { url = "https://files.pythonhosted.org/packages/c6/2a/4bba3f03f7d07207481fed47f5b35f556c7441acddc368ec43d6643c5777/pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3", size = 15188645, upload-time = "2024-09-20T19:02:03.88Z" }, + { url = "https://files.pythonhosted.org/packages/38/f8/d8fddee9ed0d0c0f4a2132c1dfcf0e3e53265055da8df952a53e7eaf178c/pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319", size = 12739445, upload-time = "2024-09-20T13:09:17.621Z" }, + { url = "https://files.pythonhosted.org/packages/20/e8/45a05d9c39d2cea61ab175dbe6a2de1d05b679e8de2011da4ee190d7e748/pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8", size = 16359235, upload-time = "2024-09-20T19:02:07.094Z" }, + { url = "https://files.pythonhosted.org/packages/1d/99/617d07a6a5e429ff90c90da64d428516605a1ec7d7bea494235e1c3882de/pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a", size = 14056756, upload-time = "2024-09-20T13:09:20.474Z" }, + { url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248, upload-time = "2024-09-20T13:09:23.137Z" }, +] + +[[package]] +name = "pbr" +version = "7.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/ab/1de9a4f730edde1bdbbc2b8d19f8fa326f036b4f18b2f72cfbea7dc53c26/pbr-7.0.3.tar.gz", hash = "sha256:b46004ec30a5324672683ec848aed9e8fc500b0d261d40a3229c2d2bbfcedc29", size = 135625, upload-time = "2025-11-03T17:04:56.274Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/db/61efa0d08a99f897ef98256b03e563092d36cc38dc4ebe4a85020fe40b31/pbr-7.0.3-py2.py3-none-any.whl", hash = "sha256:ff223894eb1cd271a98076b13d3badff3bb36c424074d26334cd25aebeecea6b", size = 131898, upload-time = "2025-11-03T17:04:54.875Z" }, +] + +[[package]] +name = "pillow" +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" }, + { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" }, + { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" }, + { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" }, + { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" }, + { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" }, + { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" }, + { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, + { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, + { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, + { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, + { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, + { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" }, + { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" }, + { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" }, + { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" }, +] + +[[package]] +name = "pkginfo" +version = "1.12.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/03/e26bf3d6453b7fda5bd2b84029a426553bb373d6277ef6b5ac8863421f87/pkginfo-1.12.1.2.tar.gz", hash = "sha256:5cd957824ac36f140260964eba3c6be6442a8359b8c48f4adf90210f33a04b7b", size = 451828, upload-time = "2025-02-19T15:27:37.188Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/3d/f4f2ba829efb54b6cd2d91349c7463316a9cc55a43fc980447416c88540f/pkginfo-1.12.1.2-py3-none-any.whl", hash = "sha256:c783ac885519cab2c34927ccfa6bf64b5a704d7c69afaea583dd9b7afe969343", size = 32717, upload-time = "2025-02-19T15:27:33.071Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, +] + +[[package]] +name = "plotly" +version = "6.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "narwhals" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/4f/8a10a9b9f5192cb6fdef62f1d77fa7d834190b2c50c0cd256bd62879212b/plotly-6.5.2.tar.gz", hash = "sha256:7478555be0198562d1435dee4c308268187553cc15516a2f4dd034453699e393", size = 7015695, upload-time = "2026-01-14T21:26:51.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/67/f95b5460f127840310d2187f916cf0023b5875c0717fdf893f71e1325e87/plotly-6.5.2-py3-none-any.whl", hash = "sha256:91757653bd9c550eeea2fa2404dba6b85d1e366d54804c340b2c874e5a7eb4a4", size = 9895973, upload-time = "2026-01-14T21:26:47.135Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "policyuniverse" +version = "1.5.1.20231109" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/a2/6cf14186b746fbcab73e507968e0b1927ad2e91dcb67af967f65d6cbe6c1/policyuniverse-1.5.1.20231109.tar.gz", hash = "sha256:74e56d410560915c2c5132e361b0130e4bffe312a2f45230eac50d7c094bc40a", size = 469645, upload-time = "2023-11-30T19:12:46.297Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/f5/65b66420c275e9b26513fdd6d84687403d11ac8be4650b67d1e5572b8f48/policyuniverse-1.5.1.20231109-py2.py3-none-any.whl", hash = "sha256:0b0ece0ee8285af31fc39ce09c82a551ca62e62bc2842e23952503bccb973321", size = 484251, upload-time = "2023-11-30T19:12:43.463Z" }, +] + +[[package]] +name = "portalocker" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/d3/c6c64067759e87af98cc668c1cc75171347d0f1577fab7ca3749134e3cd4/portalocker-2.10.1.tar.gz", hash = "sha256:ef1bf844e878ab08aee7e40184156e1151f228f103aa5c6bd0724cc330960f8f", size = 40891, upload-time = "2024-07-13T23:15:34.86Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/fb/a70a4214956182e0d7a9099ab17d50bfcba1056188e9b14f35b9e2b62a0d/portalocker-2.10.1-py3-none-any.whl", hash = "sha256:53a5984ebc86a025552264b459b46a2086e269b21823cb572f8f28ee759e45bf", size = 18423, upload-time = "2024-07-13T23:15:32.602Z" }, +] + +[[package]] +name = "prek" +version = "0.3.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/ff/5b7a2a9c4fa3dd2ffc8b13a9ec22aa550deda5b39ab273f8e02863b12642/prek-0.3.9.tar.gz", hash = "sha256:f82b92d81f42f1f90a47f5fbbf492373e25ef1f790080215b2722dd6da66510e", size = 423801, upload-time = "2026-04-13T12:30:38.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/08/c11a6b7834b461223763b6b1552f32c9199393685d52d555de621e900ee7/prek-0.3.9-py3-none-linux_armv6l.whl", hash = "sha256:3ed793d51bfaa27bddb64d525d7acb77a7c8644f549412d82252e3eb0b88aad8", size = 5337784, upload-time = "2026-04-13T12:30:46.044Z" }, + { url = "https://files.pythonhosted.org/packages/15/d9/974b02832a645c6411069c713e3191ce807f9962006da108e4727efd2fa1/prek-0.3.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:399c58400c0bd0b82a93a3c09dc1bfd88d8d0cfb242d414d2ed247187b06ead1", size = 5713864, upload-time = "2026-04-13T12:30:27.007Z" }, + { url = "https://files.pythonhosted.org/packages/40/e1/4ed14bef15eb30039a75177b0807ac007095a5a110284706ccf900a8d512/prek-0.3.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e2ea1ffb124e92f081b8e2ca5b5a623a733efb3be0c5b1f4b7ffe2ee17d1f20c", size = 5290437, upload-time = "2026-04-13T12:30:30.658Z" }, + { url = "https://files.pythonhosted.org/packages/67/80/d5c3015e9da161dede566bfeef41f098f92470613157daa4f7377ab08d58/prek-0.3.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:aaf639f95b7301639298311d8d44aad0d0b4864e9736083ad3c71ce9765d37ab", size = 5536208, upload-time = "2026-04-13T12:30:47.964Z" }, + { url = "https://files.pythonhosted.org/packages/c8/54/8cdc5eb1018437d7828740defd322e7a96459c02fc8961160c4120325313/prek-0.3.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff104863b187fa443ea8451ca55d51e2c6e94f99f00d88784b5c3c4c623f1ebe", size = 5251785, upload-time = "2026-04-13T12:30:39.78Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e2/a5fc35a0fd3167224a000ca1b6235ecbdea0ac77e24af5979a75b0e6b5a4/prek-0.3.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:039ecaf87c63a3e67cca645ebd5bc5eb6aafa6c9d929e9a27b2921e7849d7ef9", size = 5668548, upload-time = "2026-04-13T12:30:24.914Z" }, + { url = "https://files.pythonhosted.org/packages/09/e8/a189ee79f401c259f66f8af587f899d4d5bfb04e0ca371bfd01e49871007/prek-0.3.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3bde2a3d045705095983c7f78ba04f72a7565fe1c2b4e85f5628502a254754ff", size = 6660927, upload-time = "2026-04-13T12:30:44.495Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5a/54117316e98ff62a14911ad1488a3a0945530242a2ce3e92f7a40b6ccc02/prek-0.3.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28a0960a21543563e2c8e19aaad176cc8423a87aac3c914d0f313030d7a9244a", size = 5932244, upload-time = "2026-04-13T12:30:49.532Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/e88d4361f59be7adeeb3a8a3819d69d286d86fe6f7606840af6734362675/prek-0.3.9-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:0dfb5d5171d7523271909246ee306b4dc3d5b63752e7dd7c7e8a8908fc9490d1", size = 5542139, upload-time = "2026-04-13T12:30:41.266Z" }, + { url = "https://files.pythonhosted.org/packages/11/1f/204837115087bb8d063bda754a7fe975428c5d5b6548c30dd749f8ab85d4/prek-0.3.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:82b791bd36c1430c84d3ae7220a85152babc7eaf00f70adcb961bd594e756ba3", size = 5392519, upload-time = "2026-04-13T12:30:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/bd/00/de57b5795e670b6d38e7eda6d9ac6fd6d757ca22f725e5054b042104cd53/prek-0.3.9-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:6eac6d2f736b041118f053a1487abed468a70dd85a8688eaf87bb42d3dcecf20", size = 5222780, upload-time = "2026-04-13T12:30:36.576Z" }, + { url = "https://files.pythonhosted.org/packages/f5/14/0bc055c305d92980b151f2ec00c14d28fe94c6d51180ca07fded28771cbf/prek-0.3.9-py3-none-musllinux_1_1_i686.whl", hash = "sha256:5517e46e761367a3759b3168eabc120840ffbca9dfbc53187167298a98f87dc4", size = 5524310, upload-time = "2026-04-13T12:30:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d1/eebc2b69be0de36cd84adbe0a0710f4deb468a90e30525be027d6db02d54/prek-0.3.9-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:92024778cf78683ca32687bb249ab6a7d5c33887b5ee1d1a9f6d0c14228f4cf3", size = 6043751, upload-time = "2026-04-13T12:30:29.101Z" }, + { url = "https://files.pythonhosted.org/packages/46/cb/be98c04e702cbc0b0328cd745ff4634ace69ad5a84461bde36f88a7be873/prek-0.3.9-py3-none-win32.whl", hash = "sha256:7f89c55e5f480f5d073769e319924ad69d4bf9f98c5cb46a83082e26e634c958", size = 5045940, upload-time = "2026-04-13T12:30:42.882Z" }, + { url = "https://files.pythonhosted.org/packages/a6/b6/b51771d69f6282e34edeb73f23d956da34f2cabbb5ba16ba175cc0a056f9/prek-0.3.9-py3-none-win_amd64.whl", hash = "sha256:7722f3372eaa83b147e70a43cb7b9fe2128c13d0c78d8a1cdbf2a8ec2ee071eb", size = 5435204, upload-time = "2026-04-13T12:30:51.482Z" }, + { url = "https://files.pythonhosted.org/packages/30/8a/f8a87c15b095460eccd67c8d89a086b7a37aac8d363f89544b8ce6ec653d/prek-0.3.9-py3-none-win_arm64.whl", hash = "sha256:0bced6278d6cc8a4b46048979e36bc9da034611dc8facd77ab123177b833a929", size = 5279552, upload-time = "2026-04-13T12:30:53.011Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, + { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, + { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, + { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, + { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, + { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, + { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, + { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +] + +[[package]] +name = "proto-plus" +version = "1.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/89/9cbe2f4bba860e149108b683bc2efec21f14d5f7ed6e25562ad86acbc373/proto_plus-1.27.0.tar.gz", hash = "sha256:873af56dd0d7e91836aee871e5799e1c6f1bda86ac9a983e0bb9f0c266a568c4", size = 56158, upload-time = "2025-12-16T13:46:25.729Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/24/3b7a0818484df9c28172857af32c2397b6d8fcd99d9468bd4684f98ebf0a/proto_plus-1.27.0-py3-none-any.whl", hash = "sha256:1baa7f81cf0f8acb8bc1f6d085008ba4171eaf669629d1b6d1673b21ed1c0a82", size = 50205, upload-time = "2025-12-16T13:46:24.76Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/25/7c72c307aafc96fa87062aa6291d9f7c94836e43214d43722e86037aac02/protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c", size = 444465, upload-time = "2026-01-29T21:51:33.494Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/79/af92d0a8369732b027e6d6084251dd8e782c685c72da161bd4a2e00fbabb/protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b", size = 425769, upload-time = "2026-01-29T21:51:21.751Z" }, + { url = "https://files.pythonhosted.org/packages/55/75/bb9bc917d10e9ee13dee8607eb9ab963b7cf8be607c46e7862c748aa2af7/protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c", size = 437118, upload-time = "2026-01-29T21:51:24.022Z" }, + { url = "https://files.pythonhosted.org/packages/a2/6b/e48dfc1191bc5b52950246275bf4089773e91cb5ba3592621723cdddca62/protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5", size = 427766, upload-time = "2026-01-29T21:51:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b1/c79468184310de09d75095ed1314b839eb2f72df71097db9d1404a1b2717/protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190", size = 324638, upload-time = "2026-01-29T21:51:26.423Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f5/65d838092fd01c44d16037953fd4c2cc851e783de9b8f02b27ec4ffd906f/protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd", size = 339411, upload-time = "2026-01-29T21:51:27.446Z" }, + { url = "https://files.pythonhosted.org/packages/9b/53/a9443aa3ca9ba8724fdfa02dd1887c1bcd8e89556b715cfbacca6b63dbec/protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0", size = 323465, upload-time = "2026-01-29T21:51:28.925Z" }, + { url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" }, +] + +[[package]] +name = "prowler" +version = "5.27.0" +source = { git = "https://github.com/prowler-cloud/prowler.git?rev=master#0abbb7fc590eaf7de6ed354dd5a217bca261d2b0" } +dependencies = [ + { name = "alibabacloud-actiontrail20200706" }, + { name = "alibabacloud-credentials" }, + { name = "alibabacloud-cs20151215" }, + { name = "alibabacloud-ecs20140526" }, + { name = "alibabacloud-gateway-oss-util" }, + { name = "alibabacloud-oss20190517" }, + { name = "alibabacloud-ram20150501" }, + { name = "alibabacloud-rds20140815" }, + { name = "alibabacloud-sas20181203" }, + { name = "alibabacloud-sls20201230" }, + { name = "alibabacloud-sts20150401" }, + { name = "alibabacloud-tea-openapi" }, + { name = "alibabacloud-vpc20160428" }, + { name = "alive-progress" }, + { name = "awsipranges" }, + { name = "azure-identity" }, + { name = "azure-keyvault-keys" }, + { name = "azure-mgmt-apimanagement" }, + { name = "azure-mgmt-applicationinsights" }, + { name = "azure-mgmt-authorization" }, + { name = "azure-mgmt-compute" }, + { name = "azure-mgmt-containerregistry" }, + { name = "azure-mgmt-containerservice" }, + { name = "azure-mgmt-cosmosdb" }, + { name = "azure-mgmt-databricks" }, + { name = "azure-mgmt-keyvault" }, + { name = "azure-mgmt-loganalytics" }, + { name = "azure-mgmt-monitor" }, + { name = "azure-mgmt-network" }, + { name = "azure-mgmt-postgresqlflexibleservers" }, + { name = "azure-mgmt-rdbms" }, + { name = "azure-mgmt-recoveryservices" }, + { name = "azure-mgmt-recoveryservicesbackup" }, + { name = "azure-mgmt-resource" }, + { name = "azure-mgmt-search" }, + { name = "azure-mgmt-security" }, + { name = "azure-mgmt-sql" }, + { name = "azure-mgmt-storage" }, + { name = "azure-mgmt-subscription" }, + { name = "azure-mgmt-web" }, + { name = "azure-monitor-query" }, + { name = "azure-storage-blob" }, + { name = "boto3" }, + { name = "botocore" }, + { name = "cloudflare" }, + { name = "colorama" }, + { name = "cryptography" }, + { name = "dash" }, + { name = "dash-bootstrap-components" }, + { name = "defusedxml" }, + { name = "detect-secrets" }, + { name = "dulwich" }, + { name = "google-api-python-client" }, + { name = "google-auth-httplib2" }, + { name = "h2" }, + { name = "jsonschema" }, + { name = "kubernetes" }, + { name = "markdown" }, + { name = "microsoft-kiota-abstractions" }, + { name = "msgraph-sdk" }, + { name = "numpy" }, + { name = "oci" }, + { name = "okta" }, + { name = "openstacksdk" }, + { name = "pandas" }, + { name = "py-iam-expand" }, + { name = "py-ocsf-models" }, + { name = "pydantic" }, + { name = "pygithub" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "schema" }, + { name = "shodan" }, + { name = "slack-sdk" }, + { name = "tabulate" }, + { name = "tzlocal" }, + { name = "uuid6" }, +] + +[[package]] +name = "prowler-api" +version = "1.30.0" +source = { virtual = "." } +dependencies = [ + { name = "cartography" }, + { name = "celery" }, + { name = "defusedxml" }, + { name = "dj-rest-auth", extra = ["with-social"] }, + { name = "django" }, + { name = "django-allauth", extra = ["saml"] }, + { name = "django-celery-beat" }, + { name = "django-celery-results" }, + { name = "django-cors-headers" }, + { name = "django-environ" }, + { name = "django-filter" }, + { name = "django-guid" }, + { name = "django-postgres-extra" }, + { name = "djangorestframework" }, + { name = "djangorestframework-jsonapi" }, + { name = "djangorestframework-simplejwt" }, + { name = "drf-nested-routers" }, + { name = "drf-simple-apikey" }, + { name = "drf-spectacular" }, + { name = "drf-spectacular-jsonapi" }, + { name = "fonttools" }, + { name = "gevent" }, + { name = "gunicorn" }, + { name = "h2" }, + { name = "lxml" }, + { name = "markdown" }, + { name = "matplotlib" }, + { name = "neo4j" }, + { name = "openai" }, + { name = "prowler" }, + { name = "psycopg2-binary" }, + { name = "pytest-celery", extra = ["redis"] }, + { name = "reportlab" }, + { name = "sentry-sdk", extra = ["django"] }, + { name = "sqlparse" }, + { name = "uuid6" }, + { name = "werkzeug" }, + { name = "xmlsec" }, +] + +[package.dev-dependencies] +dev = [ + { name = "bandit" }, + { name = "coverage" }, + { name = "django-silk" }, + { name = "docker" }, + { name = "filelock" }, + { name = "freezegun" }, + { name = "mypy" }, + { name = "prek" }, + { name = "pylint" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-django" }, + { name = "pytest-env" }, + { name = "pytest-randomly" }, + { name = "pytest-xdist" }, + { name = "ruff" }, + { name = "tqdm" }, + { name = "vulture" }, +] + +[package.metadata] +requires-dist = [ + { name = "cartography", specifier = "==0.135.0" }, + { name = "celery", specifier = "==5.6.2" }, + { name = "defusedxml", specifier = "==0.7.1" }, + { name = "dj-rest-auth", extras = ["with-social", "jwt"], specifier = "==7.0.1" }, + { name = "django", specifier = "==5.1.15" }, + { name = "django-allauth", extras = ["saml"], specifier = "==65.15.0" }, + { name = "django-celery-beat", specifier = "==2.9.0" }, + { name = "django-celery-results", specifier = "==2.6.0" }, + { name = "django-cors-headers", specifier = "==4.4.0" }, + { name = "django-environ", specifier = "==0.11.2" }, + { name = "django-filter", specifier = "==24.3" }, + { name = "django-guid", specifier = "==3.5.0" }, + { name = "django-postgres-extra", specifier = "==2.0.9" }, + { name = "djangorestframework", specifier = "==3.15.2" }, + { name = "djangorestframework-jsonapi", specifier = "==7.0.2" }, + { name = "djangorestframework-simplejwt", specifier = "==5.5.1" }, + { name = "drf-nested-routers", specifier = "==0.95.0" }, + { name = "drf-simple-apikey", specifier = "==2.2.1" }, + { name = "drf-spectacular", specifier = "==0.27.2" }, + { name = "drf-spectacular-jsonapi", specifier = "==0.5.1" }, + { name = "fonttools", specifier = "==4.62.1" }, + { name = "gevent", specifier = "==25.9.1" }, + { name = "gunicorn", specifier = "==23.0.0" }, + { name = "h2", specifier = "==4.3.0" }, + { name = "lxml", specifier = "==6.1.0" }, + { name = "markdown", specifier = "==3.10.2" }, + { name = "matplotlib", specifier = "==3.10.8" }, + { name = "neo4j", specifier = "==6.1.0" }, + { name = "openai", specifier = "==1.109.1" }, + { name = "prowler", git = "https://github.com/prowler-cloud/prowler.git?rev=master" }, + { name = "psycopg2-binary", specifier = "==2.9.9" }, + { name = "pytest-celery", extras = ["redis"], specifier = "==1.3.0" }, + { name = "reportlab", specifier = "==4.4.10" }, + { name = "sentry-sdk", extras = ["django"], specifier = "==2.56.0" }, + { name = "sqlparse", specifier = "==0.5.5" }, + { name = "uuid6", specifier = "==2024.7.10" }, + { name = "werkzeug", specifier = "==3.1.7" }, + { name = "xmlsec", specifier = "==1.3.17" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "bandit", specifier = "==1.7.9" }, + { name = "coverage", specifier = "==7.5.4" }, + { name = "django-silk", specifier = "==5.3.2" }, + { name = "docker", specifier = "==7.1.0" }, + { name = "filelock", specifier = "==3.20.3" }, + { name = "freezegun", specifier = "==1.5.1" }, + { name = "mypy", specifier = "==1.10.1" }, + { name = "prek", specifier = "==0.3.9" }, + { name = "pylint", specifier = "==3.2.5" }, + { name = "pytest", specifier = "==9.0.3" }, + { name = "pytest-cov", specifier = "==5.0.0" }, + { name = "pytest-django", specifier = "==4.8.0" }, + { name = "pytest-env", specifier = "==1.1.3" }, + { name = "pytest-randomly", specifier = "==3.15.0" }, + { name = "pytest-xdist", specifier = "==3.6.1" }, + { name = "ruff", specifier = "==0.5.0" }, + { name = "tqdm", specifier = "==4.67.1" }, + { name = "vulture", specifier = "==2.14" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "psycopg2-binary" +version = "2.9.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/07/e720e53bfab016ebcc34241695ccc06a9e3d91ba19b40ca81317afbdc440/psycopg2-binary-2.9.9.tar.gz", hash = "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c", size = 384973, upload-time = "2023-10-03T12:48:55.128Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/ac/702d300f3df169b9d0cbef0340d9f34a78bc18dc2dbafbcb39ff0f165cf8/psycopg2_binary-2.9.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26", size = 2822581, upload-time = "2023-10-03T12:46:30.64Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1f/a6cf0cdf944253f7c45d90fbc876cc8bed5cc9942349306245715c0d88d6/psycopg2_binary-2.9.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f", size = 2552633, upload-time = "2023-10-03T12:46:32.808Z" }, + { url = "https://files.pythonhosted.org/packages/81/0b/3adf561107c865928455891156d1dde5325253f7f4316fe56cd2c3f73570/psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2", size = 2851075, upload-time = "2023-10-03T12:46:35.138Z" }, + { url = "https://files.pythonhosted.org/packages/f7/98/c2fedcbf0a9607519a010dcf88571138b2251062dbde3610cdba5ba1eee1/psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0", size = 3080509, upload-time = "2023-10-03T12:46:37.44Z" }, + { url = "https://files.pythonhosted.org/packages/c2/05/81e8bc7fca95574c9323e487d9ce1b58a4cfcc17f89b8fe843af46361211/psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53", size = 3264303, upload-time = "2023-10-03T12:46:40.73Z" }, + { url = "https://files.pythonhosted.org/packages/ce/85/62825cabc6aad53104b7b6d12eb2ad74737d268630032d07b74d4444cb72/psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be", size = 3019515, upload-time = "2023-10-03T12:46:43.038Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b0/9ca2b8e01a0912c9a14234fd5df7a241a1e44778c5797bf4b8eaa8dc3d3a/psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27", size = 2355892, upload-time = "2023-10-03T12:46:45.632Z" }, + { url = "https://files.pythonhosted.org/packages/73/17/ba28bb0022db5e2015a82d2df1c4b0d419c37fa07a588b3aff3adc4939f6/psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359", size = 2534903, upload-time = "2023-10-03T12:46:47.934Z" }, + { url = "https://files.pythonhosted.org/packages/3b/92/b463556409cdc12791cd8b1dae0072bf8efe817ef68b7ea3d9cf7d0e5656/psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2", size = 2486597, upload-time = "2023-10-03T12:46:50.598Z" }, + { url = "https://files.pythonhosted.org/packages/92/57/96576e07132d7f7a1ac1df939575e6fdd8951aea337ee152b586bb51a971/psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc", size = 2454908, upload-time = "2023-10-03T12:46:52.903Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ae/cedd56e1f4a2b0e37213283caf3733a875c4c76f3372241e19c0d2a87355/psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d", size = 1024240, upload-time = "2023-10-03T12:46:55.009Z" }, + { url = "https://files.pythonhosted.org/packages/25/1f/7ae31759142999a8d06b3e250c1346c4abcdcada8fa884376775dc1de686/psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417", size = 1163655, upload-time = "2023-10-03T12:46:57.038Z" }, + { url = "https://files.pythonhosted.org/packages/a7/d0/5f2db14e7b53552276ab613399a83f83f85b173a862d3f20580bc7231139/psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf", size = 2823784, upload-time = "2023-10-03T12:47:00.404Z" }, + { url = "https://files.pythonhosted.org/packages/18/ca/da384fd47233e300e3e485c90e7aab5d7def896d1281239f75901faf87d4/psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d", size = 2553308, upload-time = "2023-11-01T10:40:33.984Z" }, + { url = "https://files.pythonhosted.org/packages/50/66/fa53d2d3d92f6e1ef469d92afc6a4fe3f6e8a9a04b687aa28fb1f1d954ee/psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212", size = 2851283, upload-time = "2023-10-03T12:47:02.736Z" }, + { url = "https://files.pythonhosted.org/packages/04/37/2429360ac5547378202db14eec0dde76edbe1f6627df5a43c7e164922859/psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493", size = 3081839, upload-time = "2023-10-03T12:47:05.027Z" }, + { url = "https://files.pythonhosted.org/packages/62/2a/c0530b59d7e0d09824bc2102ecdcec0456b8ca4d47c0caa82e86fce3ed4c/psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996", size = 3264488, upload-time = "2023-10-03T12:47:08.962Z" }, + { url = "https://files.pythonhosted.org/packages/19/57/9f172b900795ea37246c78b5f52e00f4779984370855b3e161600156906d/psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119", size = 3020700, upload-time = "2023-10-03T12:47:12.23Z" }, + { url = "https://files.pythonhosted.org/packages/94/68/1176fc14ea76861b7b8360be5176e87fb20d5091b137c76570eb4e237324/psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba", size = 2355968, upload-time = "2023-10-03T12:47:14.817Z" }, + { url = "https://files.pythonhosted.org/packages/70/bb/aec2646a705a09079d008ce88073401cd61fc9b04f92af3eb282caa3a2ec/psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07", size = 2536101, upload-time = "2023-10-03T12:47:17.454Z" }, + { url = "https://files.pythonhosted.org/packages/14/33/12818c157e333cb9d9e6753d1b2463b6f60dbc1fade115f8e4dc5c52cac4/psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb", size = 2487064, upload-time = "2023-10-03T12:47:20.717Z" }, + { url = "https://files.pythonhosted.org/packages/56/a2/7851c68fe8768f3c9c246198b6356ee3e4a8a7f6820cc798443faada3400/psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe", size = 2456257, upload-time = "2023-10-03T12:47:23.004Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ee/3ba07c6dc7c3294e717e94720da1597aedc82a10b1b180203ce183d4631a/psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93", size = 1024709, upload-time = "2023-10-28T09:37:24.991Z" }, + { url = "https://files.pythonhosted.org/packages/7b/08/9c66c269b0d417a0af9fb969535f0371b8c538633535a7a6a5ca3f9231e2/psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab", size = 1163864, upload-time = "2023-10-28T09:37:28.155Z" }, +] + +[[package]] +name = "py-deviceid" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/fe/1beb99282853f4f6fd32af50dc1f77d15e8883627bf5014a14a7eb024963/py_deviceid-0.1.1.tar.gz", hash = "sha256:c3e7577ada23666e7f39e69370dfdaa76fe9de79c02635376d6aa0229bfa30e3", size = 5023, upload-time = "2025-03-07T17:51:25.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/a5/a598864cd753af149a4714482da1396d3f772b089d1a26579a3e4545b7a3/py_deviceid-0.1.1-py3-none-any.whl", hash = "sha256:c0e32815e87a08087a0811c18f4402ee88b28a321f997753d75ecdaab570321b", size = 5735, upload-time = "2025-03-07T17:51:25.94Z" }, +] + +[[package]] +name = "py-iam-expand" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "iamdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/99/8d31a30b37825577275bb3663885b55075fba80257fcd6813b85d3aaffa8/py_iam_expand-0.1.0.tar.gz", hash = "sha256:5a2884dc267ac59a02c3a80fefc0b34c309dac681baa0f87c436067c6cf53a96", size = 10228, upload-time = "2025-04-30T07:15:35.304Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/19/482c2e0768cda7afaed07918e4fbd951e2418255fb5d1d9b35b284871716/py_iam_expand-0.1.0-py3-none-any.whl", hash = "sha256:b845ce7b50ac895b02b4f338e09c62a68ea51849794f76e189b02009bd388510", size = 12522, upload-time = "2025-04-30T07:15:33.799Z" }, +] + +[[package]] +name = "py-ocsf-models" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "email-validator" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/70/61e2f9ce3d7e83aa5339ed6ae17e473c15c7a36f161c6dbea0e939e3af0c/py_ocsf_models-0.8.1.tar.gz", hash = "sha256:c9045237857f951e073c9f9d1f57954c90d86875b469260725292d47f7a7d73c", size = 36540, upload-time = "2026-02-12T16:50:15.233Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/18/63790884bf33f820e2c60f8d5038b5d6de967a03343ddf237c054e1d6d08/py_ocsf_models-0.8.1-py3-none-any.whl", hash = "sha256:061eb446c4171534c09a8b37f5a9d2a2fe9f87c5db32edbd1182446bc5fd097e", size = 64354, upload-time = "2026-02-12T16:50:12.983Z" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + +[[package]] +name = "pycodestyle" +version = "2.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pycryptodomex" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/85/e24bf90972a30b0fcd16c73009add1d7d7cd9140c2498a68252028899e41/pycryptodomex-3.23.0.tar.gz", hash = "sha256:71909758f010c82bc99b0abf4ea12012c98962fbf0583c2164f8b84533c2e4da", size = 4922157, upload-time = "2025-05-17T17:23:41.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/9c/1a8f35daa39784ed8adf93a694e7e5dc15c23c741bbda06e1d45f8979e9e/pycryptodomex-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:06698f957fe1ab229a99ba2defeeae1c09af185baa909a31a5d1f9d42b1aaed6", size = 2499240, upload-time = "2025-05-17T17:22:46.953Z" }, + { url = "https://files.pythonhosted.org/packages/7a/62/f5221a191a97157d240cf6643747558759126c76ee92f29a3f4aee3197a5/pycryptodomex-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b2c2537863eccef2d41061e82a881dcabb04944c5c06c5aa7110b577cc487545", size = 1644042, upload-time = "2025-05-17T17:22:49.098Z" }, + { url = "https://files.pythonhosted.org/packages/8c/fd/5a054543c8988d4ed7b612721d7e78a4b9bf36bc3c5ad45ef45c22d0060e/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43c446e2ba8df8889e0e16f02211c25b4934898384c1ec1ec04d7889c0333587", size = 2186227, upload-time = "2025-05-17T17:22:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/c8/a9/8862616a85cf450d2822dbd4fff1fcaba90877907a6ff5bc2672cafe42f8/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f489c4765093fb60e2edafdf223397bc716491b2b69fe74367b70d6999257a5c", size = 2272578, upload-time = "2025-05-17T17:22:53.676Z" }, + { url = "https://files.pythonhosted.org/packages/46/9f/bda9c49a7c1842820de674ab36c79f4fbeeee03f8ff0e4f3546c3889076b/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdc69d0d3d989a1029df0eed67cc5e8e5d968f3724f4519bd03e0ec68df7543c", size = 2312166, upload-time = "2025-05-17T17:22:56.585Z" }, + { url = "https://files.pythonhosted.org/packages/03/cc/870b9bf8ca92866ca0186534801cf8d20554ad2a76ca959538041b7a7cf4/pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6bbcb1dd0f646484939e142462d9e532482bc74475cecf9c4903d4e1cd21f003", size = 2185467, upload-time = "2025-05-17T17:22:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/96/e3/ce9348236d8e669fea5dd82a90e86be48b9c341210f44e25443162aba187/pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:8a4fcd42ccb04c31268d1efeecfccfd1249612b4de6374205376b8f280321744", size = 2346104, upload-time = "2025-05-17T17:23:02.112Z" }, + { url = "https://files.pythonhosted.org/packages/a5/e9/e869bcee87beb89040263c416a8a50204f7f7a83ac11897646c9e71e0daf/pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:55ccbe27f049743a4caf4f4221b166560d3438d0b1e5ab929e07ae1702a4d6fd", size = 2271038, upload-time = "2025-05-17T17:23:04.872Z" }, + { url = "https://files.pythonhosted.org/packages/8d/67/09ee8500dd22614af5fbaa51a4aee6e342b5fa8aecf0a6cb9cbf52fa6d45/pycryptodomex-3.23.0-cp37-abi3-win32.whl", hash = "sha256:189afbc87f0b9f158386bf051f720e20fa6145975f1e76369303d0f31d1a8d7c", size = 1771969, upload-time = "2025-05-17T17:23:07.115Z" }, + { url = "https://files.pythonhosted.org/packages/69/96/11f36f71a865dd6df03716d33bd07a67e9d20f6b8d39820470b766af323c/pycryptodomex-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:52e5ca58c3a0b0bd5e100a9fbc8015059b05cffc6c66ce9d98b4b45e023443b9", size = 1803124, upload-time = "2025-05-17T17:23:09.267Z" }, + { url = "https://files.pythonhosted.org/packages/f9/93/45c1cdcbeb182ccd2e144c693eaa097763b08b38cded279f0053ed53c553/pycryptodomex-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:02d87b80778c171445d67e23d1caef279bf4b25c3597050ccd2e13970b57fd51", size = 1707161, upload-time = "2025-05-17T17:23:11.414Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pydash" +version = "8.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/75/c1/1c55272f49d761cec38ddb80be9817935b9c91ebd6a8988e10f532868d56/pydash-8.0.6.tar.gz", hash = "sha256:b2821547e9723f69cf3a986be4db64de41730be149b2641947ecd12e1e11025a", size = 164338, upload-time = "2026-01-17T16:42:56.576Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/b7/cc5e7974699db40014d58c7dd7c4ad4ffc244d36930dc9ec7d06ee67d7a9/pydash-8.0.6-py3-none-any.whl", hash = "sha256:ee70a81a5b292c007f28f03a4ee8e75c1f5d7576df5457b836ec7ab2839cc5d0", size = 101561, upload-time = "2026-01-17T16:42:55.448Z" }, +] + +[[package]] +name = "pygithub" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyjwt", extra = ["crypto"] }, + { name = "pynacl" }, + { name = "requests" }, + { name = "typing-extensions" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/a3/cd19cd08ddd34f35040d8ab9577262b296f014856089c821d677061e5bd1/pygithub-2.8.0.tar.gz", hash = "sha256:72f5f2677d86bc3a8843aa720c6ce4c1c42fb7500243b136e3d5e14ddb5c3386", size = 2246155, upload-time = "2025-09-02T09:24:19Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/d0/136d47b7a523f93175c0608da7437dda57a98169070aa5d0b632c1fef5d8/pygithub-2.8.0-py3-none-any.whl", hash = "sha256:11a3473c1c2f1c39c525d0ee8c559f369c6d46c272cb7321c9b0cabc7aa1ce7d", size = 432572, upload-time = "2025-09-02T09:24:17.314Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pylint" +version = "3.2.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "astroid" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "dill" }, + { name = "isort" }, + { name = "mccabe" }, + { name = "platformdirs" }, + { name = "tomlkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/f3/e552877a02574b7919855a8d1f372591e67d276ea880c079968e7b3ba353/pylint-3.2.5.tar.gz", hash = "sha256:e9b7171e242dcc6ebd0aaa7540481d1a72860748a0a7816b8fe6cf6c80a6fe7e", size = 1508793, upload-time = "2024-06-28T13:10:26.915Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/ff/7f52c1461d8ceaefa989d2700a027f84427879bb7571145bbffdec5d5f4a/pylint-3.2.5-py3-none-any.whl", hash = "sha256:32cd6c042b5004b8e857d727708720c54a676d1e22917cf1a2df9b4d4868abd6", size = 519603, upload-time = "2024-06-28T13:10:23.526Z" }, +] + +[[package]] +name = "pymsalruntime" +version = "0.18.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/f9/9691cd100cf311f6e526d6e88bd20144dded21f5206dbb7d1b12dbe1bb67/pymsalruntime-0.18.1-cp311-cp311-win32.whl", hash = "sha256:74416947b1071054f3258cac3448a7adf708888727bf283267df2bb27f0998f1", size = 1094568, upload-time = "2025-03-13T18:12:28.926Z" }, + { url = "https://files.pythonhosted.org/packages/0f/d1/eacd266809a276c8d4da33268c751cb407ac90e6aee8185f1cccb05ec794/pymsalruntime-0.18.1-cp311-cp311-win_amd64.whl", hash = "sha256:beb926655aae3367b7e4bda2baad86f9271beefee1121f71642da0ed4de37fd2", size = 1246912, upload-time = "2025-03-13T18:12:14.264Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d3/0399d685a7c544dd6792736a8f0dbb66e66a452dd5c53134cdff2ea39477/pymsalruntime-0.18.1-cp312-cp312-win32.whl", hash = "sha256:ace12bf9b7fcbf1bf21a03c227717e09ba99acd9190623fe0821a08832ece4eb", size = 1093430, upload-time = "2025-03-13T18:12:31.525Z" }, + { url = "https://files.pythonhosted.org/packages/f1/cc/22bb46a8c5a17c815a50e82b430808ebe970239d41a3b5851fb9b86b4912/pymsalruntime-0.18.1-cp312-cp312-win_amd64.whl", hash = "sha256:f9fd8ea52395f52f7d62498e47754adf2bfe6530816ff57eff1ba6f524aee51b", size = 1245309, upload-time = "2025-03-13T18:12:16.771Z" }, +] + +[[package]] +name = "pynacl" +version = "1.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/9a/4019b524b03a13438637b11538c82781a5eda427394380381af8f04f467a/pynacl-1.6.2.tar.gz", hash = "sha256:018494d6d696ae03c7e656e5e74cdfd8ea1326962cc401bcf018f1ed8436811c", size = 3511692, upload-time = "2026-01-01T17:48:10.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/7b/4845bbf88e94586ec47a432da4e9107e3fc3ce37eb412b1398630a37f7dd/pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:c949ea47e4206af7c8f604b8278093b674f7c79ed0d4719cc836902bf4517465", size = 388458, upload-time = "2026-01-01T17:32:16.829Z" }, + { url = "https://files.pythonhosted.org/packages/1e/b4/e927e0653ba63b02a4ca5b4d852a8d1d678afbf69b3dbf9c4d0785ac905c/pynacl-1.6.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8845c0631c0be43abdd865511c41eab235e0be69c81dc66a50911594198679b0", size = 800020, upload-time = "2026-01-01T17:32:18.34Z" }, + { url = "https://files.pythonhosted.org/packages/7f/81/d60984052df5c97b1d24365bc1e30024379b42c4edcd79d2436b1b9806f2/pynacl-1.6.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22de65bb9010a725b0dac248f353bb072969c94fa8d6b1f34b87d7953cf7bbe4", size = 1399174, upload-time = "2026-01-01T17:32:20.239Z" }, + { url = "https://files.pythonhosted.org/packages/68/f7/322f2f9915c4ef27d140101dd0ed26b479f7e6f5f183590fd32dfc48c4d3/pynacl-1.6.2-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46065496ab748469cdd999246d17e301b2c24ae2fdf739132e580a0e94c94a87", size = 835085, upload-time = "2026-01-01T17:32:22.24Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d0/f301f83ac8dbe53442c5a43f6a39016f94f754d7a9815a875b65e218a307/pynacl-1.6.2-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a66d6fb6ae7661c58995f9c6435bda2b1e68b54b598a6a10247bfcdadac996c", size = 1437614, upload-time = "2026-01-01T17:32:23.766Z" }, + { url = "https://files.pythonhosted.org/packages/c4/58/fc6e649762b029315325ace1a8c6be66125e42f67416d3dbd47b69563d61/pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130", size = 818251, upload-time = "2026-01-01T17:32:25.69Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a8/b917096b1accc9acd878819a49d3d84875731a41eb665f6ebc826b1af99e/pynacl-1.6.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c8a231e36ec2cab018c4ad4358c386e36eede0319a0c41fed24f840b1dac59f6", size = 1402859, upload-time = "2026-01-01T17:32:27.215Z" }, + { url = "https://files.pythonhosted.org/packages/85/42/fe60b5f4473e12c72f977548e4028156f4d340b884c635ec6b063fe7e9a5/pynacl-1.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:68be3a09455743ff9505491220b64440ced8973fe930f270c8e07ccfa25b1f9e", size = 791926, upload-time = "2026-01-01T17:32:29.314Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f9/e40e318c604259301cc091a2a63f237d9e7b424c4851cafaea4ea7c4834e/pynacl-1.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b097553b380236d51ed11356c953bf8ce36a29a3e596e934ecabe76c985a577", size = 1363101, upload-time = "2026-01-01T17:32:31.263Z" }, + { url = "https://files.pythonhosted.org/packages/48/47/e761c254f410c023a469284a9bc210933e18588ca87706ae93002c05114c/pynacl-1.6.2-cp38-abi3-win32.whl", hash = "sha256:5811c72b473b2f38f7e2a3dc4f8642e3a3e9b5e7317266e4ced1fba85cae41aa", size = 227421, upload-time = "2026-01-01T17:32:33.076Z" }, + { url = "https://files.pythonhosted.org/packages/41/ad/334600e8cacc7d86587fe5f565480fde569dfb487389c8e1be56ac21d8ac/pynacl-1.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:62985f233210dee6548c223301b6c25440852e13d59a8b81490203c3227c5ba0", size = 239754, upload-time = "2026-01-01T17:32:34.557Z" }, + { url = "https://files.pythonhosted.org/packages/29/7d/5945b5af29534641820d3bd7b00962abbbdfee84ec7e19f0d5b3175f9a31/pynacl-1.6.2-cp38-abi3-win_arm64.whl", hash = "sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c", size = 184801, upload-time = "2026-01-01T17:32:36.309Z" }, +] + +[[package]] +name = "pyopenssl" +version = "26.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/11/a62e1d33b373da2b2c2cd9eb508147871c80f12b1cacde3c5d314922afdd/pyopenssl-26.0.0.tar.gz", hash = "sha256:f293934e52936f2e3413b89c6ce36df66a0b34ae1ea3a053b8c5020ff2f513fc", size = 185534, upload-time = "2026-03-15T14:28:26.353Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/7d/d4f7d908fa8415571771b30669251d57c3cf313b36a856e6d7548ae01619/pyopenssl-26.0.0-py3-none-any.whl", hash = "sha256:df94d28498848b98cc1c0ffb8ef1e71e40210d3b0a8064c9d29571ed2904bf81", size = 57969, upload-time = "2026-03-15T14:28:24.864Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "pyreadline3" +version = "3.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/49/4cea918a08f02817aabae639e3d0ac046fef9f9180518a3ad394e22da148/pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7", size = 99839, upload-time = "2024-09-19T02:40:10.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178, upload-time = "2024-09-19T02:40:08.598Z" }, +] + +[[package]] +name = "pysocks" +version = "1.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/11/293dd436aea955d45fc4e8a35b6ae7270f5b8e00b53cf6c024c83b657a11/PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0", size = 284429, upload-time = "2019-09-20T02:07:35.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/59/b4572118e098ac8e46e399a1dd0f2d85403ce8bbaad9ec79373ed6badaf9/PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", size = 16725, upload-time = "2019-09-20T02:06:22.938Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "pytest-celery" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "celery" }, + { name = "debugpy" }, + { name = "docker" }, + { name = "kombu" }, + { name = "psutil" }, + { name = "pytest-docker-tools" }, + { name = "tenacity" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/14/0b/775fdabc659b21b6d03162141d9f2c1485eb9f8b0de725017a5c85f2364c/pytest_celery-1.3.0.tar.gz", hash = "sha256:bd9e5b0f594ec5de9ab97cf27e3a11c644718a761bab6b997d01800fd7394f64", size = 28149, upload-time = "2026-03-02T19:58:44.977Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/52/929eac8a21b91850165c7340d06df20f9ee7cbea332b71c95a0e15e8b9ab/pytest_celery-1.3.0-py3-none-any.whl", hash = "sha256:f02201d7770584a0c412a1ded329a142170c24012467c7046f2c72cc8205ad5d", size = 49312, upload-time = "2026-03-02T19:58:46.45Z" }, +] + +[package.optional-dependencies] +redis = [ + { name = "redis" }, +] + +[[package]] +name = "pytest-cov" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/67/00efc8d11b630c56f15f4ad9c7f9223f1e5ec275aaae3fa9118c6a223ad2/pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857", size = 63042, upload-time = "2024-03-24T20:16:34.856Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", size = 21990, upload-time = "2024-03-24T20:16:32.444Z" }, +] + +[[package]] +name = "pytest-django" +version = "4.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/cf/44510ac5479f281d6663a08dff0d93f56b21f4ee091980ea4d4b64491ad6/pytest-django-4.8.0.tar.gz", hash = "sha256:5d054fe011c56f3b10f978f41a8efb2e5adfc7e680ef36fb571ada1f24779d90", size = 83291, upload-time = "2024-01-30T13:14:19.511Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/5b/29555191e903881d05e1f7184205ec534c7021e0ee077d1e6a1ee8f1b1eb/pytest_django-4.8.0-py3-none-any.whl", hash = "sha256:ca1ddd1e0e4c227cf9e3e40a6afc6d106b3e70868fd2ac5798a22501271cd0c7", size = 23432, upload-time = "2024-01-30T13:14:17.953Z" }, +] + +[[package]] +name = "pytest-docker-tools" +version = "3.1.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docker" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/2a/4d68472c2bac257c4e7389b5ca3a6b6cd7da88bce012bed321fdd31372ae/pytest_docker_tools-3.1.9.tar.gz", hash = "sha256:1b6a0cb633c20145731313335ef15bcf5571839c06726764e60cbe495324782b", size = 42824, upload-time = "2025-03-16T13:48:23.888Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/79/9dae84c244dabebca6a952e098d6ac9d13719b701fc5323ba6d00abc675a/pytest_docker_tools-3.1.9-py2.py3-none-any.whl", hash = "sha256:36f8e88d56d84ea177df68a175673681243dd991d2807fbf551d90f60341bfdb", size = 29268, upload-time = "2025-03-16T13:48:22.184Z" }, +] + +[[package]] +name = "pytest-env" +version = "1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/cc/df6940b2527bfa634c00940dfb6e3ec873bdfb7507b55894c93283fa3178/pytest_env-1.1.3.tar.gz", hash = "sha256:fcd7dc23bb71efd3d35632bde1bbe5ee8c8dc4489d6617fb010674880d96216b", size = 8627, upload-time = "2023-11-28T04:11:27.607Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/b2/bdc663a5647ce2034f7e8420122af340df87c01ba97745fc753b8c917acb/pytest_env-1.1.3-py3-none-any.whl", hash = "sha256:aada77e6d09fcfb04540a6e462c58533c37df35fa853da78707b17ec04d17dfc", size = 6154, upload-time = "2023-11-28T04:11:25.923Z" }, +] + +[[package]] +name = "pytest-randomly" +version = "3.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/d4/6e924a0b2855736d942703dec88dfc98b4fe0881c8fa849b6b0fbb9182fa/pytest_randomly-3.15.0.tar.gz", hash = "sha256:b908529648667ba5e54723088edd6f82252f540cc340d748d1fa985539687047", size = 21743, upload-time = "2023-08-15T18:04:59.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/d3/00e575657422055c4ea220b2f80e8cc6026ab7130372b7067444d1b0ac10/pytest_randomly-3.15.0-py3-none-any.whl", hash = "sha256:0516f4344b29f4e9cdae8bce31c4aeebf59d0b9ef05927c33354ff3859eeeca6", size = 8685, upload-time = "2023-08-15T18:04:57.913Z" }, +] + +[[package]] +name = "pytest-xdist" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/c4/3c310a19bc1f1e9ef50075582652673ef2bfc8cd62afef9585683821902f/pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d", size = 84060, upload-time = "2024-04-28T19:29:54.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/82/1d96bf03ee4c0fdc3c0cbe61470070e659ca78dc0086fb88b66c185e2449/pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7", size = 46108, upload-time = "2024-04-28T19:29:52.813Z" }, +] + +[[package]] +name = "python-crontab" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/99/7f/c54fb7e70b59844526aa4ae321e927a167678660ab51dda979955eafb89a/python_crontab-3.3.0.tar.gz", hash = "sha256:007c8aee68dddf3e04ec4dce0fac124b93bd68be7470fc95d2a9617a15de291b", size = 57626, upload-time = "2025-07-13T20:05:35.535Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/42/bb4afa5b088f64092036221843fc989b7db9d9d302494c1f8b024ee78a46/python_crontab-3.3.0-py3-none-any.whl", hash = "sha256:739a778b1a771379b75654e53fd4df58e5c63a9279a63b5dfe44c0fcc3ee7884", size = 27533, upload-time = "2025-07-13T20:05:34.266Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-digitalocean" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpickle" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/f7/43cb73fb393c4c0da36294b6040c7424bc904042d55c1b37c73ecc9e7714/python-digitalocean-1.17.0.tar.gz", hash = "sha256:107854fde1aafa21774e8053cf253b04173613c94531f75d5a039ad770562b24", size = 33586, upload-time = "2021-10-02T21:05:21.803Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/4f/87c17d4e23a62273c192656300e92a26bf6d5d8b2908cf2fc4a5a96da4b2/python_digitalocean-1.17.0-py3-none-any.whl", hash = "sha256:0032168e022e85fca314eb3f8dfaabf82087f2ed40839eb28f1eeeeca5afb1fa", size = 40298, upload-time = "2021-10-02T21:05:20.806Z" }, +] + +[[package]] +name = "python3-saml" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "isodate" }, + { name = "lxml" }, + { name = "xmlsec" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/98/6e0268c3a9893af3d4c5cf670183e0314cd6b5cb034a612d6a7cc5060df8/python3-saml-1.16.0.tar.gz", hash = "sha256:97c9669aecabc283c6e5fb4eb264f446b6e006f5267d01c9734f9d8bffdac133", size = 83468, upload-time = "2023-10-09T10:37:43.128Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/14/49d9828443b58bd5cc80a454c91b0f867fbf36a24975d501945e6cb9e32f/python3_saml-1.16.0-py3-none-any.whl", hash = "sha256:20b97d11b04f01ee22e98f4a38242e2fea2e28fbc7fbc9bdd57cab5ac7fc2d0d", size = 76155, upload-time = "2023-10-09T10:40:34.001Z" }, +] + +[[package]] +name = "pytz" +version = "2025.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/57/df1c9157c8d5a05117e455d66fd7cf6dbc46974f832b1058ed4856785d8a/pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e", size = 319617, upload-time = "2025-01-31T01:54:48.615Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/38/ac33370d784287baa1c3d538978b5e2ea064d4c1b93ffbd12826c190dd10/pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57", size = 507930, upload-time = "2025-01-31T01:54:45.634Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, +] + +[[package]] +name = "redis" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/c8/983d5c6579a411d8a99bc5823cc5712768859b5ce2c8afe1a65b37832c81/redis-7.1.0.tar.gz", hash = "sha256:b1cc3cfa5a2cb9c2ab3ba700864fb0ad75617b41f01352ce5779dabf6d5f9c3c", size = 4796669, upload-time = "2025-11-19T15:54:39.961Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b", size = 354159, upload-time = "2025-11-19T15:54:38.064Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "reportlab" +version = "4.4.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "charset-normalizer" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/57/28bfbf0a775b618b6e4d854ef8dd3f5c8988e5d614d8898703502a35f61c/reportlab-4.4.10.tar.gz", hash = "sha256:5cbbb34ac3546039d0086deb2938cdec06b12da3cdb836e813258eb33cd28487", size = 3714962, upload-time = "2026-02-12T10:45:21.325Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/2e/e1798b8b248e1517e74c6cdf10dd6edd485044e7edf46b5f11ffcc5a0add/reportlab-4.4.10-py3-none-any.whl", hash = "sha256:5abc815746ae2bc44e7ff25db96814f921349ca814c992c7eac3c26029bf7c24", size = 1955400, upload-time = "2026-02-12T10:45:18.828Z" }, +] + +[[package]] +name = "requests" +version = "2.33.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, +] + +[package.optional-dependencies] +socks = [ + { name = "pysocks" }, +] + +[[package]] +name = "requests-file" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/f8/5dc70102e4d337063452c82e1f0d95e39abfe67aa222ed8a5ddeb9df8de8/requests_file-3.0.1.tar.gz", hash = "sha256:f14243d7796c588f3521bd423c5dea2ee4cc730e54a3cac9574d78aca1272576", size = 6967, upload-time = "2025-10-20T18:56:42.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/d5/de8f089119205a09da657ed4784c584ede8381a0ce6821212a6d4ca47054/requests_file-3.0.1-py2.py3-none-any.whl", hash = "sha256:d0f5eb94353986d998f80ac63c7f146a307728be051d4d1cd390dbdb59c10fa2", size = 4514, upload-time = "2025-10-20T18:56:41.184Z" }, +] + +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "oauthlib" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, +] + +[[package]] +name = "requestsexceptions" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/61b9652d3256503c99b0b8f145d9c8aa24c514caff6efc229989505937c1/requestsexceptions-1.4.0.tar.gz", hash = "sha256:b095cbc77618f066d459a02b137b020c37da9f46d9b057704019c9f77dba3065", size = 6880, upload-time = "2018-02-01T17:04:45.294Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/8c/49ca60ea8c907260da4662582c434bec98716177674e88df3fd340acf06d/requestsexceptions-1.4.0-py2.py3-none-any.whl", hash = "sha256:3083d872b6e07dc5c323563ef37671d992214ad9a32b0ca4a3d7f5500bf38ce3", size = 3802, upload-time = "2018-02-01T17:04:39.07Z" }, +] + +[[package]] +name = "retrying" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/5a/b17e1e257d3e6f2e7758930e1256832c9ddd576f8631781e6a072914befa/retrying-1.4.2.tar.gz", hash = "sha256:d102e75d53d8d30b88562d45361d6c6c934da06fab31bd81c0420acb97a8ba39", size = 11411, upload-time = "2025-08-03T03:35:25.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/f3/6cd296376653270ac1b423bb30bd70942d9916b6978c6f40472d6ac038e7/retrying-1.4.2-py3-none-any.whl", hash = "sha256:bbc004aeb542a74f3569aeddf42a2516efefcdaff90df0eb38fbfbf19f179f59", size = 10859, upload-time = "2025-08-03T03:35:23.829Z" }, +] + +[[package]] +name = "rich" +version = "14.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/99/a4cab2acbb884f80e558b0771e97e21e939c5dfb460f488d19df485e8298/rich-14.3.2.tar.gz", hash = "sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8", size = 230143, upload-time = "2026-02-01T16:20:47.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/45/615f5babd880b4bd7d405cc0dc348234c5ffb6ed1ea33e152ede08b2072d/rich-14.3.2-py3-none-any.whl", hash = "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69", size = 309963, upload-time = "2026-02-01T16:20:46.078Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "rsa" +version = "4.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, +] + +[[package]] +name = "ruff" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/28/9a/dde343d95ecd0747207e4e8d143c373ef961cbd6b78c61a659f67582dbd2/ruff-0.5.0.tar.gz", hash = "sha256:eb641b5873492cf9bd45bc9c5ae5320648218e04386a5f0c264ad6ccce8226a1", size = 2587996, upload-time = "2024-06-27T15:42:16.137Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/5d/0d9510720d61df753df39bf24a96d6c141080c94fe6025568747fbea856a/ruff-0.5.0-py3-none-linux_armv6l.whl", hash = "sha256:ee770ea8ab38918f34e7560a597cc0a8c9a193aaa01bfbd879ef43cb06bd9c4c", size = 9434156, upload-time = "2024-06-27T15:40:40.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/5a/7f466f5449dce168c2d956ad4a207d62dc7b76836d46f1c04249a4daaf34/ruff-0.5.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:38f3b8327b3cb43474559d435f5fa65dacf723351c159ed0dc567f7ab735d1b6", size = 8536948, upload-time = "2024-06-27T15:40:47.907Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a2/afc6952d5a0199e7e6c0a2051d6f4780fb70376f5bd07f27838f8bc0cf47/ruff-0.5.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7594f8df5404a5c5c8f64b8311169879f6cf42142da644c7e0ba3c3f14130370", size = 8107163, upload-time = "2024-06-27T15:41:06.887Z" }, + { url = "https://files.pythonhosted.org/packages/34/54/ea77237405b7573298f5cc00045d1aceab609841d3cc88de3d7c3d2a6163/ruff-0.5.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:adc7012d6ec85032bc4e9065110df205752d64010bed5f958d25dbee9ce35de3", size = 9877009, upload-time = "2024-06-27T15:41:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/56/db/3f74873bc0ca915f79d26575e549eb5e633022d56315d314e6f9c0fa596a/ruff-0.5.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d505fb93b0fabef974b168d9b27c3960714d2ecda24b6ffa6a87ac432905ea38", size = 9219926, upload-time = "2024-06-27T15:41:15.032Z" }, + { url = "https://files.pythonhosted.org/packages/57/08/1052c80f3f44321631a8c1337e55883dd7a7b02b4efe5c9282258db42358/ruff-0.5.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dc5cfd3558f14513ed0d5b70ce531e28ea81a8a3b1b07f0f48421a3d9e7d80a", size = 10031146, upload-time = "2024-06-27T15:41:20.601Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a2/f7c01c4a02b87998c9e1379ec8d7345d6a45f8b34e326e8700c13da391c3/ruff-0.5.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:db3ca35265de239a1176d56a464b51557fce41095c37d6c406e658cf80bbb362", size = 10770796, upload-time = "2024-06-27T15:41:24.665Z" }, + { url = "https://files.pythonhosted.org/packages/12/a1/5f45ab0948a202da7fe13c6e0678f907bd88caacc7e4f4909603d3774051/ruff-0.5.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b1a321c4f68809fddd9b282fab6a8d8db796b270fff44722589a8b946925a2a8", size = 10364804, upload-time = "2024-06-27T15:41:29.153Z" }, + { url = "https://files.pythonhosted.org/packages/7e/40/83f88d5bda41496a90871ec82dd82545def4c4683e1c2f4a42f5a168ae3e/ruff-0.5.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c4dfcd8d34b143916994b3876b63d53f56724c03f8c1a33a253b7b1e6bf2a7d", size = 11241308, upload-time = "2024-06-27T15:41:33.569Z" }, + { url = "https://files.pythonhosted.org/packages/af/79/8a57016a761d11491b913460a3d1545cdbe96dca6acb1279102814c9147b/ruff-0.5.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81e5facfc9f4a674c6a78c64d38becfbd5e4f739c31fcd9ce44c849f1fad9e4c", size = 10064506, upload-time = "2024-06-27T15:41:38.21Z" }, + { url = "https://files.pythonhosted.org/packages/67/34/fd7cd8be0d8cd4bcce0dbef807933f6c9685d5dc2549b729da7ee7a7a5cc/ruff-0.5.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e589e27971c2a3efff3fadafb16e5aef7ff93250f0134ec4b52052b673cf988d", size = 9866155, upload-time = "2024-06-27T15:41:42.551Z" }, + { url = "https://files.pythonhosted.org/packages/7b/54/8a654417265fe91de3ff303274a9d4d64774496eaa2eadd7da8e88a48b82/ruff-0.5.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2ffbc3715a52b037bcb0f6ff524a9367f642cdc5817944f6af5479bbb2eb50e", size = 9285874, upload-time = "2024-06-27T15:41:46.991Z" }, + { url = "https://files.pythonhosted.org/packages/86/39/564161e306b12ab40d2b6be0a0bc843c692a8295cc7101fa930db89e1e7e/ruff-0.5.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cd096e23c6a4f9c819525a437fa0a99d1c67a1b6bb30948d46f33afbc53596cf", size = 9645133, upload-time = "2024-06-27T15:41:52.535Z" }, + { url = "https://files.pythonhosted.org/packages/3b/67/3203d56ee41d3dee8d94c7926b298b13a150f105a55fef38b75ccf5e0901/ruff-0.5.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:46e193b36f2255729ad34a49c9a997d506e58f08555366b2108783b3064a0e1e", size = 10143022, upload-time = "2024-06-27T15:41:56.879Z" }, + { url = "https://files.pythonhosted.org/packages/71/2e/1bab3c5a3929f348cdc086a3f3013ea0b8823ec3d273f3334ef621f4f83f/ruff-0.5.0-py3-none-win32.whl", hash = "sha256:49141d267100f5ceff541b4e06552e98527870eafa1acc9dec9139c9ec5af64c", size = 7735210, upload-time = "2024-06-27T15:42:02.173Z" }, + { url = "https://files.pythonhosted.org/packages/48/05/04bf25784ba73abf0e639065fd7a785c005c895c4bf64aa2729d26a1984f/ruff-0.5.0-py3-none-win_amd64.whl", hash = "sha256:e9118f60091047444c1b90952736ee7b1792910cab56e9b9a9ac20af94cd0440", size = 8536440, upload-time = "2024-06-27T15:42:08.275Z" }, + { url = "https://files.pythonhosted.org/packages/63/ab/a10ab4a751514d4f954079fbd2f645cc0c5982a18f510ab411048a2a5409/ruff-0.5.0-py3-none-win_arm64.whl", hash = "sha256:ed5c4df5c1fb4518abcb57725b576659542bdbe93366f4f329e8f398c4b71178", size = 7949476, upload-time = "2024-06-27T15:42:12.464Z" }, +] + +[[package]] +name = "s3transfer" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/74/8d69dcb7a9efe8baa2046891735e5dfe433ad558ae23d9e3c14c633d1d58/s3transfer-0.14.0.tar.gz", hash = "sha256:eff12264e7c8b4985074ccce27a3b38a485bb7f7422cc8046fee9be4983e4125", size = 151547, upload-time = "2025-09-09T19:23:31.089Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/f0/ae7ca09223a81a1d890b2557186ea015f6e0502e9b8cb8e1813f1d8cfa4e/s3transfer-0.14.0-py3-none-any.whl", hash = "sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456", size = 85712, upload-time = "2025-09-09T19:23:30.041Z" }, +] + +[[package]] +name = "scaleway" +version = "2.10.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "scaleway-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/ce/774eea10c35fb0f028d0ae2e45d404bc16d83ac5cdd12f410d348e6dd6a4/scaleway-2.10.3.tar.gz", hash = "sha256:b1f9dd1b1450767205234c6f5a345e5e25dc039c780253d698893b5c344ce594", size = 717421, upload-time = "2025-11-04T10:03:13.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/d6/2b41dfb4cac8bf51cc100050f70129aa5284d04344ceec1391176467b1cb/scaleway-2.10.3-py3-none-any.whl", hash = "sha256:dbf381440d6caf37c878cf16445a63f4969a4aac2257c9b72c744d10ff223a0c", size = 877950, upload-time = "2025-11-04T10:03:12.128Z" }, +] + +[[package]] +name = "scaleway-core" +version = "2.10.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "pyyaml" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/6a/9f53cc92ad2332f96b48bd4975e53e15b8099a148a4e7386d64d393538d5/scaleway_core-2.10.3.tar.gz", hash = "sha256:56432f755d694669429de51d51c1d0b3361b28dc2f939b28e4cb954610ee76be", size = 27282, upload-time = "2025-11-04T10:01:41.021Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/d0/b6b133a27a94fc272d316f7bc8722be701130b93ee12c60c0c2413c16199/scaleway_core-2.10.3-py3-none-any.whl", hash = "sha256:fd4112144554d6adae22ff737555eeb0e38cb1063250b3e88c9aebc1b957793b", size = 33523, upload-time = "2025-11-04T10:01:40.106Z" }, +] + +[[package]] +name = "schema" +version = "0.7.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contextlib2" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/e8/01e1b46d9e04cdaee91c9c736d9117304df53361a191144c8eccda7f0ee9/schema-0.7.5.tar.gz", hash = "sha256:f06717112c61895cabc4707752b88716e8420a8819d71404501e114f91043197", size = 48173, upload-time = "2021-12-01T20:49:24.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/93/ca8aa5a772efd69043d0a745172d92bee027caa7565c7f774a2f44b91207/schema-0.7.5-py2.py3-none-any.whl", hash = "sha256:f3ffdeeada09ec34bf40d7d79996d9f7175db93b7a5065de0faa7f41083c1e6c", size = 17603, upload-time = "2021-12-01T20:49:21.252Z" }, +] + +[[package]] +name = "sentry-sdk" +version = "2.56.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/df/5008954f5466085966468612a7d1638487596ee6d2fd7fb51783a85351bf/sentry_sdk-2.56.0.tar.gz", hash = "sha256:fdab72030b69625665b2eeb9738bdde748ad254e8073085a0ce95382678e8168", size = 426820, upload-time = "2026-03-24T09:56:36.575Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/1a/b3a3e9f6520493fed7997af4d2de7965d71549c62f994a8fd15f2ecd519e/sentry_sdk-2.56.0-py2.py3-none-any.whl", hash = "sha256:5afafb744ceb91d22f4cc650c6bd048ac6af5f7412dcc6c59305a2e36f4dbc02", size = 451568, upload-time = "2026-03-24T09:56:34.807Z" }, +] + +[package.optional-dependencies] +django = [ + { name = "django" }, +] + +[[package]] +name = "setuptools" +version = "80.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/95/faf61eb8363f26aa7e1d762267a8d602a1b26d4f3a1e758e92cb3cb8b054/setuptools-80.10.2.tar.gz", hash = "sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70", size = 1200343, upload-time = "2026-01-25T22:38:17.252Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/b8/f1f62a5e3c0ad2ff1d189590bfa4c46b4f3b6e49cef6f26c6ee4e575394d/setuptools-80.10.2-py3-none-any.whl", hash = "sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173", size = 1064234, upload-time = "2026-01-25T22:38:15.216Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "shodan" +version = "1.31.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "click-plugins" }, + { name = "colorama" }, + { name = "requests" }, + { name = "tldextract" }, + { name = "xlsxwriter" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/06/c6dcc975a1e7d89bc764fd271da8138b318e18080b48e7f1acd2ab63df28/shodan-1.31.0.tar.gz", hash = "sha256:c73275386ea02390e196c35c660706a28dd4d537c5a21eb387ab6236fac251f6", size = 57939, upload-time = "2023-12-17T01:42:02.426Z" } + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "slack-sdk" +version = "3.39.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/dd/645f3eb93fce38eadbb649e85684730b1fc3906c2674ca59bddc2ca2bd2e/slack_sdk-3.39.0.tar.gz", hash = "sha256:6a56be10dc155c436ff658c6b776e1c082e29eae6a771fccf8b0a235822bbcb1", size = 247207, upload-time = "2025-11-20T15:27:57.556Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/1f/32bcf088e535c1870b1a1f2e3b916129c66fdfe565a793316317241d41e5/slack_sdk-3.39.0-py2.py3-none-any.whl", hash = "sha256:b1556b2f5b8b12b94e5ea3f56c4f2c7f04462e4e1013d325c5764ff118044fa8", size = 309850, upload-time = "2025-11-20T15:27:55.729Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sqlparse" +version = "0.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/76/437d71068094df0726366574cf3432a4ed754217b436eb7429415cf2d480/sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e", size = 120815, upload-time = "2025-12-19T07:17:45.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" }, +] + +[[package]] +name = "statsd" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/29/05e9f50946f4cf2ed182726c60d9c0ae523bb3f180588c574dd9746de557/statsd-4.0.1.tar.gz", hash = "sha256:99763da81bfea8daf6b3d22d11aaccb01a8d0f52ea521daab37e758a4ca7d128", size = 27814, upload-time = "2022-11-06T14:17:36.194Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/d0/c9543b52c067a390ae6ae632d7fd1b97a35cdc8d69d40c0b7d334b326410/statsd-4.0.1-py2.py3-none-any.whl", hash = "sha256:c2676519927f7afade3723aca9ca8ea986ef5b059556a980a867721ca69df093", size = 13118, upload-time = "2022-11-06T14:17:34.258Z" }, +] + +[[package]] +name = "std-uritemplate" +version = "2.0.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/93/62/61866776cd32df3f984ff2f79b1428e10700e0a33ca7a7536e3fcba3cf2a/std_uritemplate-2.0.8.tar.gz", hash = "sha256:138ceff2c5bfef18a650372a5e8c82fe7f780c87235513de6c342fb5f7e18347", size = 6018, upload-time = "2025-10-16T15:51:29.774Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/97/b4f2f442fee92a1406f08b4fbc990bd7d02dc84b3b5e6315a59fa9b2a9f4/std_uritemplate-2.0.8-py3-none-any.whl", hash = "sha256:839807a7f9d07f0bad1a88977c3428bd97b9ff0d229412a0bf36123d8c724257", size = 6512, upload-time = "2025-10-16T15:51:28.713Z" }, +] + +[[package]] +name = "stevedore" +version = "5.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/5b/496f8abebd10c3301129abba7ddafd46c71d799a70c44ab080323987c4c9/stevedore-5.6.0.tar.gz", hash = "sha256:f22d15c6ead40c5bbfa9ca54aa7e7b4a07d59b36ae03ed12ced1a54cf0b51945", size = 516074, upload-time = "2025-11-20T10:06:07.264Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/40/8561ce06dc46fd17242c7724ab25b257a2ac1b35f4ebf551b40ce6105cfa/stevedore-5.6.0-py3-none-any.whl", hash = "sha256:4a36dccefd7aeea0c70135526cecb7766c4c84c473b1af68db23d541b6dc1820", size = 54428, upload-time = "2025-11-20T10:06:05.946Z" }, +] + +[[package]] +name = "tabulate" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" }, +] + +[[package]] +name = "tenacity" +version = "9.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload-time = "2025-04-02T08:25:09.966Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, +] + +[[package]] +name = "tldextract" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "idna" }, + { name = "requests" }, + { name = "requests-file" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/7b/644fbbb49564a6cb124a8582013315a41148dba2f72209bba14a84242bf0/tldextract-5.3.1.tar.gz", hash = "sha256:a72756ca170b2510315076383ea2993478f7da6f897eef1f4a5400735d5057fb", size = 126105, upload-time = "2025-12-28T23:58:05.532Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/42/0e49d6d0aac449ca71952ec5bae764af009754fcb2e76a5cc097543747b3/tldextract-5.3.1-py3-none-any.whl", hash = "sha256:6bfe36d518de569c572062b788e16a659ccaceffc486d243af0484e8ecf432d9", size = 105886, upload-time = "2025-12-28T23:58:04.071Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "tomlkit" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/af/14b24e41977adb296d6bd1fb59402cf7d60ce364f90c890bd2ec65c43b5a/tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064", size = 187167, upload-time = "2026-01-13T01:14:53.304Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", size = 39310, upload-time = "2026-01-13T01:14:51.965Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + +[[package]] +name = "typer" +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/bf/8825b5929afd84d0dabd606c67cd57b8388cb3ec385f7ef19c5cc2202069/typer-0.21.1.tar.gz", hash = "sha256:ea835607cd752343b6b2b7ce676893e5a0324082268b48f27aa058bdb7d2145d", size = 110371, upload-time = "2026-01-06T11:21:10.989Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01", size = 47381, upload-time = "2026-01-06T11:21:09.824Z" }, +] + +[[package]] +name = "types-aiobotocore-ecr" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/27/fc82f9d7522d554cbf7df02a037aa9c2f4a7e978efb82f2f60e06494f8b1/types_aiobotocore_ecr-3.1.1.tar.gz", hash = "sha256:155edc63c612e1a7861fa746376a5143cc4f3ca05b60c27d68ced23e8567a344", size = 37025, upload-time = "2026-01-21T02:07:06.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/34/8cf816860041845bcff546cefaa9f5f129391dae8c67580090bad47994b2/types_aiobotocore_ecr-3.1.1-py3-none-any.whl", hash = "sha256:e5c02e06ff057bbe7821fb40ac7de67d2335fdc7987ea31392051efe81ceb69c", size = 43575, upload-time = "2026-01-21T02:07:05.422Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, +] + +[[package]] +name = "tzlocal" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" }, +] + +[[package]] +name = "uritemplate" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/60/f174043244c5306c9988380d2cb10009f91563fc4b31293d27e17201af56/uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e", size = 33267, upload-time = "2025-06-02T15:12:06.318Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/99/3ae339466c9183ea5b8ae87b34c0b897eda475d2aec2307cae60e5cd4f29/uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686", size = 11488, upload-time = "2025-06-02T15:12:03.405Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "uuid6" +version = "2024.7.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/56/2560a9f1ccab9e12b1b3478a3c870796cf4d8ee5652bb19b61751cced14a/uuid6-2024.7.10.tar.gz", hash = "sha256:2d29d7f63f593caaeea0e0d0dd0ad8129c9c663b29e19bdf882e864bedf18fb0", size = 8705, upload-time = "2024-07-10T16:39:37.592Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/3e/4ae6af487ce5781ed71d5fe10aca72e7cbc4d4f45afc31b120287082a8dd/uuid6-2024.7.10-py3-none-any.whl", hash = "sha256:93432c00ba403751f722829ad21759ff9db051dea140bf81493271e8e4dd18b7", size = 6376, upload-time = "2024-07-10T16:39:36.148Z" }, +] + +[[package]] +name = "vine" +version = "5.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/e4/d07b5f29d283596b9727dd5275ccbceb63c44a1a82aa9e4bfd20426762ac/vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0", size = 48980, upload-time = "2023-11-05T08:46:53.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", size = 9636, upload-time = "2023-11-05T08:46:51.205Z" }, +] + +[[package]] +name = "vulture" +version = "2.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/25/925f35db758a0f9199113aaf61d703de891676b082bd7cf73ea01d6000f7/vulture-2.14.tar.gz", hash = "sha256:cb8277902a1138deeab796ec5bef7076a6e0248ca3607a3f3dee0b6d9e9b8415", size = 58823, upload-time = "2024-12-08T17:39:43.319Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/56/0cc15b8ff2613c1d5c3dc1f3f576ede1c43868c1bc2e5ccaa2d4bcd7974d/vulture-2.14-py2.py3-none-any.whl", hash = "sha256:d9a90dba89607489548a49d557f8bac8112bd25d3cbc8aeef23e860811bd5ed9", size = 28915, upload-time = "2024-12-08T17:39:40.573Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/62/a7c072fbfefb2980a00f99ca994279cb9ecf310cb2e6b2a4d2a28fe192b3/wcwidth-0.5.3.tar.gz", hash = "sha256:53123b7af053c74e9fe2e92ac810301f6139e64379031f7124574212fb3b4091", size = 157587, upload-time = "2026-01-31T03:52:10.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/c1/d73f12f8cdb1891334a2ccf7389eed244d3941e74d80dd220badb937f3fb/wcwidth-0.5.3-py3-none-any.whl", hash = "sha256:d584eff31cd4753e1e5ff6c12e1edfdb324c995713f75d26c29807bb84bf649e", size = 92981, upload-time = "2026-01-31T03:52:09.14Z" }, +] + +[[package]] +name = "websocket-client" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/43/76ded108b296a49f52de6bac5192ca1c4be84e886f9b5c9ba8427d9694fd/werkzeug-3.1.7.tar.gz", hash = "sha256:fb8c01fe6ab13b9b7cdb46892b99b1d66754e1d7ab8e542e865ec13f526b5351", size = 875700, upload-time = "2026-03-24T01:08:07.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/b2/0bba9bbb4596d2d2f285a16c2ab04118f6b957d8441566e1abb892e6a6b2/werkzeug-3.1.7-py3-none-any.whl", hash = "sha256:4b314d81163a3e1a169b6a0be2a000a0e204e8873c5de6586f453c55688d422f", size = 226295, upload-time = "2026-03-24T01:08:06.133Z" }, +] + +[[package]] +name = "workos" +version = "6.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "httpx" }, + { name = "pyjwt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/2f/99fb8718274116c5c146c745755620fd5c5943f78ca52ca9b17e94348286/workos-6.0.4.tar.gz", hash = "sha256:b0bfe8fd212b8567422c4ea3732eb33608794033eb3a69900c6b04db183c32d6", size = 172217, upload-time = "2026-04-16T03:09:28.583Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/f1/d2ab661e6dc2828a4c73e38f12630c3b109cfe2bc664ab70631c04f0db4b/workos-6.0.4-py3-none-any.whl", hash = "sha256:548668b3702673536f853ba72a7b5bbbc269e467aaf9ac4f477b6e0177df5e21", size = 511418, upload-time = "2026-04-16T03:09:27.098Z" }, +] + +[[package]] +name = "wrapt" +version = "1.17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload-time = "2025-08-12T05:51:45.79Z" }, + { url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload-time = "2025-08-12T05:51:34.629Z" }, + { url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload-time = "2025-08-12T05:51:56.074Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376, upload-time = "2025-08-12T05:52:32.134Z" }, + { url = "https://files.pythonhosted.org/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604, upload-time = "2025-08-12T05:52:11.663Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782, upload-time = "2025-08-12T05:52:12.626Z" }, + { url = "https://files.pythonhosted.org/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076, upload-time = "2025-08-12T05:52:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", size = 36457, upload-time = "2025-08-12T05:53:03.936Z" }, + { url = "https://files.pythonhosted.org/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", size = 38745, upload-time = "2025-08-12T05:53:02.885Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", size = 36806, upload-time = "2025-08-12T05:52:53.368Z" }, + { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, + { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, + { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, + { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, + { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, + { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, +] + +[[package]] +name = "xlsxwriter" +version = "3.2.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/2c/c06ef49dc36e7954e55b802a8b231770d286a9758b3d936bd1e04ce5ba88/xlsxwriter-3.2.9.tar.gz", hash = "sha256:254b1c37a368c444eac6e2f867405cc9e461b0ed97a3233b2ac1e574efb4140c", size = 215940, upload-time = "2025-09-16T00:16:21.63Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/0c/3662f4a66880196a590b202f0db82d919dd2f89e99a27fadef91c4a33d41/xlsxwriter-3.2.9-py3-none-any.whl", hash = "sha256:9a5db42bc5dff014806c58a20b9eae7322a134abb6fce3c92c181bfb275ec5b3", size = 175315, upload-time = "2025-09-16T00:16:20.108Z" }, +] + +[[package]] +name = "xmlsec" +version = "1.3.17" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lxml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/14/538b75379e6ab8f688f14d8663e2ab138d9c778bac4999d155b5f33c71c1/xmlsec-1.3.17.tar.gz", hash = "sha256:f3fac9ae679f66585925cc00c5f6839ae36c1d03157619571dee18acc05b9c01", size = 115637, upload-time = "2025-11-11T16:20:46.019Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/e4/970614d892749da00df253c370230fd24143028268923a1c35651fb3f962/xmlsec-1.3.17-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4a7ee007c6b55f7621330aee8330ef2dafa4225fce554064571ca826beafe7e", size = 3450577, upload-time = "2025-11-11T16:19:34.159Z" }, + { url = "https://files.pythonhosted.org/packages/50/4a/2f48ad48fecbd49dbbc6f2a5b540cd65277089fd5b8b5d8c7e816c3625c2/xmlsec-1.3.17-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1ef656421d01851618d0fe5518e57469159c14a48e05125f7bd3225631952f9", size = 3846698, upload-time = "2025-11-11T16:19:35.408Z" }, + { url = "https://files.pythonhosted.org/packages/a9/07/0130e0b711f7443d0abdec403ea5128392cd5b241bb53f4ec41d144d94db/xmlsec-1.3.17-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80fff2251d0e73714435b5860ce200990dffe85466dd91d08d75c4d64ee9967d", size = 4423233, upload-time = "2025-11-11T16:19:37.129Z" }, + { url = "https://files.pythonhosted.org/packages/00/f7/a4e588d61f602f25a51b6004be9a162e36e746fa1cbeb12248794a96766b/xmlsec-1.3.17-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f2bf6bbf04f8a912483d268b4c2727d400d1806d054624da13bee4b9f6fa28a", size = 4163716, upload-time = "2025-11-11T16:19:38.365Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a2/f8c019445134dfc59afb5874d1fc4fe212ec2dc45a8c33806a15b5c0c119/xmlsec-1.3.17-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a603584ceee175036e1bccdbe65d551c0fff67343fd506bfa6cec52bc64d9a75", size = 3875404, upload-time = "2025-11-11T16:19:40.008Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c3/90c0e26bb9f95799c64874ebee0b43eaf7e5b5ba912bcd87ed4cc46ea514/xmlsec-1.3.17-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:26cc3d81437b51839946d2e93d09371dfd73ed2831dc7e37eff0fb52fc33747c", size = 4460640, upload-time = "2025-11-11T16:19:41.372Z" }, + { url = "https://files.pythonhosted.org/packages/ba/be/7b85b0ff4281779293d93a8bbef70a6b72ba60d8a80d15653bd4967d0c07/xmlsec-1.3.17-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d862f023f56a49c06576be41dfaf213c9ac77e7a344e7f204278c365bb36d00e", size = 4209625, upload-time = "2025-11-11T16:19:43.289Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6d/028472e523c2f667a4634881b65acfa939bc4902ed37e1e9fe1d55d45ec0/xmlsec-1.3.17-cp311-cp311-win_amd64.whl", hash = "sha256:9877303e8c72d7aa2467d1af12e56d67b8fb50d324eda5848e0ec5ee2176aac5", size = 2445935, upload-time = "2025-11-11T16:19:44.605Z" }, + { url = "https://files.pythonhosted.org/packages/f0/01/d36fd82b837167546951e7e088dbd2f0dacf553157d256b2a25802d28a95/xmlsec-1.3.17-cp311-cp311-win_arm64.whl", hash = "sha256:b3f306f5aef47336b8299d8dbee31fa0b2eba4579f9f41396070f7a97d0dcd49", size = 2261485, upload-time = "2025-11-11T16:19:46.212Z" }, + { url = "https://files.pythonhosted.org/packages/cd/a5/d91216f7dbb85cb65cb7249fcc894f5389a8a4843857aff678646cab77fa/xmlsec-1.3.17-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:df4a8d7fef3ffe90e572400d47392ea480120e339c292f802830ed09d449e622", size = 3450960, upload-time = "2025-11-11T16:19:47.794Z" }, + { url = "https://files.pythonhosted.org/packages/b7/38/c37bd4e164259e0b271fe4d17d054f31c7287a1e4c47d24ef77d723b3493/xmlsec-1.3.17-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed63cbd87dd69ebcf3a9f82d87b67818c9a7d656325dd4fb34d6c4dfbaa84017", size = 3846774, upload-time = "2025-11-11T16:19:49.636Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ff/83430c5df33c6ad402728a681998c5b2872c090b556a558d02f8cf1d2f24/xmlsec-1.3.17-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5c3008b32a15d24b6c9da39bf6ede8dc3122570a640a73795d763aea55a2193e", size = 4425910, upload-time = "2025-11-11T16:19:50.95Z" }, + { url = "https://files.pythonhosted.org/packages/02/41/bb94c7a97ea613b3860f6152bb7efcf5be524d135592e094ecc64ff79228/xmlsec-1.3.17-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a0b9a1dcda547e0340eefa6f4a04b87dbd9e40cd514487f347934f94fd559ab", size = 4169038, upload-time = "2025-11-11T16:19:52.217Z" }, + { url = "https://files.pythonhosted.org/packages/3b/4c/852ba0805df27b7bd1e88e9524d9573b076c3a126e936b1f18c6f22fb968/xmlsec-1.3.17-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3a53c14d4bc40b0f0fcc6d7908b88f3cbbcf36e25c392f796d88aee7dee5beea", size = 3876430, upload-time = "2025-11-11T16:19:53.388Z" }, + { url = "https://files.pythonhosted.org/packages/b0/f0/08fec6adc65f6911b49b4fa71e920c8f6434f44fdc427c71360e6dd9e9ce/xmlsec-1.3.17-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5346616e1fe1015f7800698c15225c7902f45db199e217af2039a21989aff7e9", size = 4464419, upload-time = "2025-11-11T16:19:54.777Z" }, + { url = "https://files.pythonhosted.org/packages/25/ce/84789ba3929715806deae88f10bc31e1ff904aa735059ee3855c104a142d/xmlsec-1.3.17-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:64c1184d51c8a67e3d1eb3ac477e307a07e2b40fd03cd0c8084b147ea0f342db", size = 4215080, upload-time = "2025-11-11T16:19:56.293Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6e/57b5054187cd2b42e5310dc1f6d209fced456f93dae25345a422b3a290ef/xmlsec-1.3.17-cp312-cp312-win_amd64.whl", hash = "sha256:d360d4adfb53d3adeca398c225cb7e2a73a2246414455937082a1fa19bd8572b", size = 2445872, upload-time = "2025-11-11T16:19:57.713Z" }, + { url = "https://files.pythonhosted.org/packages/04/7b/f64c95df054dd793ae1925f04248abd359b1c26cc2320d67407e7fd26e4d/xmlsec-1.3.17-cp312-cp312-win_arm64.whl", hash = "sha256:eee89c268a35f8a08a8e9abef6f466b97577e94f5cac8bf32c25e97cd5020097", size = 2261464, upload-time = "2025-11-11T16:19:58.937Z" }, +] + +[[package]] +name = "xmltodict" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/aa/917ceeed4dbb80d2f04dbd0c784b7ee7bba8ae5a54837ef0e5e062cd3cfb/xmltodict-1.0.2.tar.gz", hash = "sha256:54306780b7c2175a3967cad1db92f218207e5bc1aba697d887807c0fb68b7649", size = 25725, upload-time = "2025-09-17T21:59:26.459Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/20/69a0e6058bc5ea74892d089d64dfc3a62ba78917ec5e2cfa70f7c92ba3a5/xmltodict-1.0.2-py3-none-any.whl", hash = "sha256:62d0fddb0dcbc9f642745d8bbf4d81fd17d6dfaec5a15b5c1876300aad92af0d", size = 13893, upload-time = "2025-09-17T21:59:24.859Z" }, +] + +[[package]] +name = "yarl" +version = "1.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/27/5ab13fc84c76a0250afd3d26d5936349a35be56ce5785447d6c423b26d92/yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", size = 141607, upload-time = "2025-10-06T14:09:16.298Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", size = 94027, upload-time = "2025-10-06T14:09:17.786Z" }, + { url = "https://files.pythonhosted.org/packages/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", size = 94963, upload-time = "2025-10-06T14:09:19.662Z" }, + { url = "https://files.pythonhosted.org/packages/68/fe/2c1f674960c376e29cb0bec1249b117d11738db92a6ccc4a530b972648db/yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", size = 368406, upload-time = "2025-10-06T14:09:21.402Z" }, + { url = "https://files.pythonhosted.org/packages/95/26/812a540e1c3c6418fec60e9bbd38e871eaba9545e94fa5eff8f4a8e28e1e/yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", size = 336581, upload-time = "2025-10-06T14:09:22.98Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f5/5777b19e26fdf98563985e481f8be3d8a39f8734147a6ebf459d0dab5a6b/yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", size = 388924, upload-time = "2025-10-06T14:09:24.655Z" }, + { url = "https://files.pythonhosted.org/packages/86/08/24bd2477bd59c0bbd994fe1d93b126e0472e4e3df5a96a277b0a55309e89/yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", size = 392890, upload-time = "2025-10-06T14:09:26.617Z" }, + { url = "https://files.pythonhosted.org/packages/46/00/71b90ed48e895667ecfb1eaab27c1523ee2fa217433ed77a73b13205ca4b/yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", size = 365819, upload-time = "2025-10-06T14:09:28.544Z" }, + { url = "https://files.pythonhosted.org/packages/30/2d/f715501cae832651d3282387c6a9236cd26bd00d0ff1e404b3dc52447884/yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", size = 363601, upload-time = "2025-10-06T14:09:30.568Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f9/a678c992d78e394e7126ee0b0e4e71bd2775e4334d00a9278c06a6cce96a/yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", size = 358072, upload-time = "2025-10-06T14:09:32.528Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d1/b49454411a60edb6fefdcad4f8e6dbba7d8019e3a508a1c5836cba6d0781/yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", size = 385311, upload-time = "2025-10-06T14:09:34.634Z" }, + { url = "https://files.pythonhosted.org/packages/87/e5/40d7a94debb8448c7771a916d1861d6609dddf7958dc381117e7ba36d9e8/yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", size = 381094, upload-time = "2025-10-06T14:09:36.268Z" }, + { url = "https://files.pythonhosted.org/packages/35/d8/611cc282502381ad855448643e1ad0538957fc82ae83dfe7762c14069e14/yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", size = 370944, upload-time = "2025-10-06T14:09:37.872Z" }, + { url = "https://files.pythonhosted.org/packages/2d/df/fadd00fb1c90e1a5a8bd731fa3d3de2e165e5a3666a095b04e31b04d9cb6/yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca", size = 81804, upload-time = "2025-10-06T14:09:39.359Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f7/149bb6f45f267cb5c074ac40c01c6b3ea6d8a620d34b337f6321928a1b4d/yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b", size = 86858, upload-time = "2025-10-06T14:09:41.068Z" }, + { url = "https://files.pythonhosted.org/packages/2b/13/88b78b93ad3f2f0b78e13bfaaa24d11cbc746e93fe76d8c06bf139615646/yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376", size = 81637, upload-time = "2025-10-06T14:09:42.712Z" }, + { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" }, + { url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" }, + { url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" }, + { url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" }, + { url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" }, + { url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" }, + { url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" }, + { url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" }, + { url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" }, + { url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" }, + { url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" }, + { url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" }, + { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +] + +[[package]] +name = "zope-event" +version = "6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/33/d3eeac228fc14de76615612ee208be2d8a5b5b0fada36bf9b62d6b40600c/zope_event-6.1.tar.gz", hash = "sha256:6052a3e0cb8565d3d4ef1a3a7809336ac519bc4fe38398cb8d466db09adef4f0", size = 18739, upload-time = "2025-11-07T08:05:49.934Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/b0/956902e5e1302f8c5d124e219c6bf214e2649f92ad5fce85b05c039a04c9/zope_event-6.1-py3-none-any.whl", hash = "sha256:0ca78b6391b694272b23ec1335c0294cc471065ed10f7f606858fc54566c25a0", size = 6414, upload-time = "2025-11-07T08:05:48.874Z" }, +] + +[[package]] +name = "zope-interface" +version = "8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/a4/77daa5ba398996d16bb43fc721599d27d03eae68fe3c799de1963c72e228/zope_interface-8.2.tar.gz", hash = "sha256:afb20c371a601d261b4f6edb53c3c418c249db1a9717b0baafc9a9bb39ba1224", size = 254019, upload-time = "2026-01-09T07:51:07.253Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/97/9c2aa8caae79915ed64eb114e18816f178984c917aa9adf2a18345e4f2e5/zope_interface-8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c65ade7ea85516e428651048489f5e689e695c79188761de8c622594d1e13322", size = 208081, upload-time = "2026-01-09T08:05:06.623Z" }, + { url = "https://files.pythonhosted.org/packages/34/86/4e2fcb01a8f6780ac84923748e450af0805531f47c0956b83065c99ab543/zope_interface-8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1ef4b43659e1348f35f38e7d1a6bbc1682efde239761f335ffc7e31e798b65b", size = 208522, upload-time = "2026-01-09T08:05:07.986Z" }, + { url = "https://files.pythonhosted.org/packages/f6/eb/08e277da32ddcd4014922854096cf6dcb7081fad415892c2da1bedefbf02/zope_interface-8.2-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:dfc4f44e8de2ff4eba20af4f0a3ca42d3c43ab24a08e49ccd8558b7a4185b466", size = 255198, upload-time = "2026-01-09T08:05:09.532Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a1/b32484f3281a5dc83bc713ad61eca52c543735cdf204543172087a074a74/zope_interface-8.2-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8f094bfb49179ec5dc9981cb769af1275702bd64720ef94874d9e34da1390d4c", size = 259970, upload-time = "2026-01-09T08:05:11.477Z" }, + { url = "https://files.pythonhosted.org/packages/f6/81/bca0e8ae1e487d4093a8a7cfed2118aa2d4758c8cfd66e59d2af09d71f1c/zope_interface-8.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d2bb8e7364e18f083bf6744ccf30433b2a5f236c39c95df8514e3c13007098ce", size = 261153, upload-time = "2026-01-09T08:05:13.402Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/e3ff2a708011e56b10b271b038d4cb650a8ad5b7d24352fe2edf6d6b187a/zope_interface-8.2-cp311-cp311-win_amd64.whl", hash = "sha256:6f4b4dfcfdfaa9177a600bb31cebf711fdb8c8e9ed84f14c61c420c6aa398489", size = 212330, upload-time = "2026-01-09T08:05:15.267Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a0/1e1fabbd2e9c53ef92b69df6d14f4adc94ec25583b1380336905dc37e9a0/zope_interface-8.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:624b6787fc7c3e45fa401984f6add2c736b70a7506518c3b537ffaacc4b29d4c", size = 208785, upload-time = "2026-01-09T08:05:17.348Z" }, + { url = "https://files.pythonhosted.org/packages/c3/2a/88d098a06975c722a192ef1fb7d623d1b57c6a6997cf01a7aabb45ab1970/zope_interface-8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bc9ded9e97a0ed17731d479596ed1071e53b18e6fdb2fc33af1e43f5fd2d3aaa", size = 208976, upload-time = "2026-01-09T08:05:18.792Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e8/757398549fdfd2f8c89f32c82ae4d2f0537ae2a5d2f21f4a2f711f5a059f/zope_interface-8.2-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:532367553e4420c80c0fc0cabcc2c74080d495573706f66723edee6eae53361d", size = 259411, upload-time = "2026-01-09T08:05:20.567Z" }, + { url = "https://files.pythonhosted.org/packages/91/af/502601f0395ce84dff622f63cab47488657a04d0065547df42bee3a680ff/zope_interface-8.2-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2bf9cf275468bafa3c72688aad8cfcbe3d28ee792baf0b228a1b2d93bd1d541a", size = 264859, upload-time = "2026-01-09T08:05:22.234Z" }, + { url = "https://files.pythonhosted.org/packages/89/0c/d2f765b9b4814a368a7c1b0ac23b68823c6789a732112668072fe596945d/zope_interface-8.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0009d2d3c02ea783045d7804da4fd016245e5c5de31a86cebba66dd6914d59a2", size = 264398, upload-time = "2026-01-09T08:05:23.853Z" }, + { url = "https://files.pythonhosted.org/packages/4a/81/2f171fbc4222066957e6b9220c4fb9146792540102c37e6d94e5d14aad97/zope_interface-8.2-cp312-cp312-win_amd64.whl", hash = "sha256:845d14e580220ae4544bd4d7eb800f0b6034fe5585fc2536806e0a26c2ee6640", size = 212444, upload-time = "2026-01-09T08:05:25.148Z" }, +] + +[[package]] +name = "zstd" +version = "1.5.7.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/62/b9c075ad664e7c4cbb3d8d2be7c246506abe1bc7f778eb58d260ef9538c8/zstd-1.5.7.3.tar.gz", hash = "sha256:403e5205f4ac04b92e6b0cda654be2f51de268228a0db0067bc087faacf2f495", size = 672559, upload-time = "2026-01-08T16:24:43.361Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/0d/8c89c0d010b58c21a7865a239790bb1c6822029c053b1ded858d6b573e3a/zstd-1.5.7.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a3c1781a24e2ced2c0ddee11d45b1f04018b03615eeb622a62eca4d56d3358a", size = 267641, upload-time = "2026-01-08T16:30:50.812Z" }, + { url = "https://files.pythonhosted.org/packages/a3/6d/155d8c344d96eca2a5a003a5ddd63373a5f13591fd5cf2b9490250d6805a/zstd-1.5.7.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a6c7c81056362b60a04baa34632e713d596662a860ec34efd8e9b109c10e6ec7", size = 230962, upload-time = "2026-01-08T16:30:49.155Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c7/ab93916a26eb58cd501ad701974c31b4bc67a7f6abd6c24bef8fe4d7649b/zstd-1.5.7.3-cp311-cp311-manylinux_2_14_x86_64.whl", hash = "sha256:e564f34a55effc7d654eb293468edc80b64d476b0f899f82760ecd8323223ff5", size = 304166, upload-time = "2026-01-10T11:17:45.697Z" }, + { url = "https://files.pythonhosted.org/packages/c2/54/27a7040a360019a4602343e3c98c0c0a140f382186002c01e1992fd21837/zstd-1.5.7.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:fbc49a57188184931d5e3c9f1133cad7eea5a370a9e9418fb8122d58c14340a5", size = 1540288, upload-time = "2026-01-08T17:50:26.913Z" }, + { url = "https://files.pythonhosted.org/packages/96/93/4a4d4edd1b2e809e0ebbb16000404bdcc9a09743c04ee1661442c9581b75/zstd-1.5.7.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:d121d3e63722819e1fe5effbcd9628d8a7cfea0cddabcc5bb37ea861a6a83424", size = 1619134, upload-time = "2026-01-08T17:50:32.324Z" }, + { url = "https://files.pythonhosted.org/packages/31/6b/cd6f0a7f4f0d98e4110aa77763cf3e85f594d983ea9ca3d64cc0cee10684/zstd-1.5.7.3-cp311-cp311-manylinux_2_4_i686.whl", hash = "sha256:621f2e7ca8e9eb52a83eb9c91ec3cd283d87591bf75cc658de486b65f44742c7", size = 300166, upload-time = "2026-01-10T11:12:27.938Z" }, + { url = "https://files.pythonhosted.org/packages/05/3f/c717e0d15127d04b7fa58ba9b4c56e8b88b803048b9766cd9d158dbb22ea/zstd-1.5.7.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:c1950fcae690ba32d0f31702b335c548fb42547821565925e48576afdad774a5", size = 1525776, upload-time = "2026-01-08T17:50:35.518Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a2/1813cd787d1a2f9ab8e8a90d28dcbc8e8098997dd04de38897ea8e75dd08/zstd-1.5.7.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bac4f0d03da69115878bedbfa03c4a3f64364e8396b432028c4ce0f05141a0fb", size = 2096057, upload-time = "2026-01-08T17:50:33.984Z" }, + { url = "https://files.pythonhosted.org/packages/36/ce/f5a3c7c12de458dd9ce15c484d627fe5412b60c155da23dacb5fcf08d9d5/zstd-1.5.7.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:da0ab134b7fd28023dedf013751ca850de300a090eb11f689d2a1c178c87d9dc", size = 2132659, upload-time = "2026-01-08T17:50:29.534Z" }, + { url = "https://files.pythonhosted.org/packages/f1/66/151f9546498bfd8971a0b6ad67d87c26d7a0df17d57f724da674f3778666/zstd-1.5.7.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b9923175842ee8f7602ec9cc578f5fc396896f0e8460d3ac9a5adc3cea77244e", size = 2124811, upload-time = "2026-01-08T17:50:37.612Z" }, + { url = "https://files.pythonhosted.org/packages/6a/34/4d2dbb36cb2373d3f115c047cb901b64f89de0703d10779da39de9453812/zstd-1.5.7.3-cp311-cp311-win32.whl", hash = "sha256:0612b604948d7b58aecc6788c7ceb53c5f21d94a155bb6ea9bd0f54ffa43725d", size = 150363, upload-time = "2026-01-08T17:11:02.392Z" }, + { url = "https://files.pythonhosted.org/packages/d9/de/f53687e0dd8c0d0ebfaed9ae88f6a96a1a0388ae7424b469e74bb17ac57d/zstd-1.5.7.3-cp311-cp311-win_amd64.whl", hash = "sha256:5b7f8c81b2bd3b62c0345242247d484cafa4b518d59d18619813d9225af5c5c3", size = 167577, upload-time = "2026-01-08T17:11:03.356Z" }, + { url = "https://files.pythonhosted.org/packages/f2/58/d4a6a902e229e953ed273fe9b78587ed31f57567aa68d3e34af6056e42af/zstd-1.5.7.3-cp311-cp311-win_arm64.whl", hash = "sha256:ea112e3acd9e1765adca35df7b54ac75b36194290f64ea03a3a59664209c8527", size = 157238, upload-time = "2026-01-08T16:36:06.25Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ed/5a3bf2e29dc56d4cc7619929bb51f0c758de6d02967cc73c5d8755a862c0/zstd-1.5.7.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:01a39efb0eeab7cc45cb308618233b624b0840d5e16dcf85456b6cca0592f203", size = 268124, upload-time = "2026-01-08T16:29:57.091Z" }, + { url = "https://files.pythonhosted.org/packages/e2/1d/efc2074ac90af938e78f2ed4004639fe24f294d9086c5280f8d9a02b9897/zstd-1.5.7.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7a8e8838cf35fa3987bfe1958584cc22e1797efce8e155a63544b4144fc671f8", size = 230988, upload-time = "2026-01-08T16:29:55.604Z" }, + { url = "https://files.pythonhosted.org/packages/2a/52/178393b8d70e23fba67f42dfce4663e4e8a30867110168beb490a36d4639/zstd-1.5.7.3-cp312-cp312-manylinux_2_14_i686.whl", hash = "sha256:f3920ac1d1cc7e9f252f3e29f217fe3cd36f2191bb3dbcae826c29e189b7ad54", size = 300207, upload-time = "2026-01-10T11:26:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/8dcd86a2efb2ed3f9dae39545a05d3c7ed26c7678330786ce4a44cd8b099/zstd-1.5.7.3-cp312-cp312-manylinux_2_14_x86_64.whl", hash = "sha256:143f9062953fb5590cbd47c1040d357336742c79696bf90b6d5b835279a68304", size = 304154, upload-time = "2026-01-10T11:17:40.91Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ce/0c96905ab01ffe0e53a3cec8132123b82db26bd583a71608029bcc789ebc/zstd-1.5.7.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36d1fd8647e47e1f21b345e192f1a279e925678c23dad8236b547d04456cd699", size = 2162222, upload-time = "2026-01-08T18:02:22.762Z" }, + { url = "https://files.pythonhosted.org/packages/11/c4/db4807d6a68b4628c74fd379de7e3c67ec34f19a2a80ac246b3837cde6cb/zstd-1.5.7.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1538db419afa62773cf534fc7f3009ff59ecf55ecee4e889587ac2ef0010ed8", size = 2201732, upload-time = "2026-01-08T18:02:20.835Z" }, + { url = "https://files.pythonhosted.org/packages/c5/99/c19a3c0f5580ff9c33a74f06d98d6060ed1fa6bd09b55aed9be852ec191f/zstd-1.5.7.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c5efd16adb092e2a547a7d51cfdaf6fd5680528227684c5bafc7669ab4a55f41", size = 2096459, upload-time = "2026-01-08T18:02:25.336Z" }, + { url = "https://files.pythonhosted.org/packages/23/fd/02eac30419475dbe50212c119043a2d0698a0cbc756da85fd3fd9abddf42/zstd-1.5.7.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:39b3438e64637d80a5b1860526903b92020acb9bae9ceb5adffd9838c1441328", size = 2125442, upload-time = "2026-01-08T18:02:17.715Z" }, + { url = "https://files.pythonhosted.org/packages/bb/43/3a16ff0a8c913bb9825379db1bd533c75c57c2d2f31dd9111aa9b53711f4/zstd-1.5.7.3-cp312-cp312-win32.whl", hash = "sha256:cbf48c53461e224ffc2490cfe5120a1ff40d14c84d2b512c6d6d99fc91685cf3", size = 150367, upload-time = "2026-01-08T17:03:40.178Z" }, + { url = "https://files.pythonhosted.org/packages/46/83/b85875d7428e63dfa9247e41d17fac611443c774f7892f8643bd4164a6b2/zstd-1.5.7.3-cp312-cp312-win_amd64.whl", hash = "sha256:943a189910f2fea997462e3e4d7fbf727a06d231ef801ebee557b1c87568981c", size = 167604, upload-time = "2026-01-08T17:03:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/37/42/cf291e26804de2f55500cdac93f5e9fa6267cf315def8aa402529bae3a87/zstd-1.5.7.3-cp312-cp312-win_arm64.whl", hash = "sha256:85c4d508f8109afa7c51c4960626c3325af2cf1e442c6c36ebfea15d04757e3f", size = 157241, upload-time = "2026-01-08T16:47:34.615Z" }, + { url = "https://files.pythonhosted.org/packages/04/b8/d13d584867d5eb1bc607877a870858e02a256d4706a4274e475413a000aa/zstd-1.5.7.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:76c49ea969bc08389ea59155cea7c5dea224522ffc62f443f3c0a915f5fd184d", size = 260025, upload-time = "2026-01-08T16:57:45.739Z" }, + { url = "https://files.pythonhosted.org/packages/16/a1/1e5faf75bedfd2bfccfb83e18736b115bed6e348504bd21800cd8f30dcea/zstd-1.5.7.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:6b1a638ff3dfce8f4cb1203c662fb5606dd99b4a62c5ddc4c406d2d1326bcfdd", size = 221038, upload-time = "2026-01-08T17:16:32.005Z" }, + { url = "https://files.pythonhosted.org/packages/b7/2c/0fe74d8b2029eef8000bc71aac5b3e5b55d00581238711cf627814183ea3/zstd-1.5.7.3-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5e96a5cb100a0edc162935227f2d9784b1031ce4a8a83e96e66eae2673c10143", size = 326792, upload-time = "2026-01-08T16:57:35.631Z" }, + { url = "https://files.pythonhosted.org/packages/96/e0/2c7f081f3524f872128ff31bea2acb6b21cb1dacccef920eb6a1a77a87c6/zstd-1.5.7.3-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bda0bbf3a9553720cd33f1f85940a259656c7ffba4be717ff82b7f062052188", size = 322283, upload-time = "2026-01-08T16:57:36.759Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a7/3bebfcc18d66b90bc7b506a61b2ff4af5ee1b0b16e784ea644afa06241c5/zstd-1.5.7.3-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac36e4022422f6e49b3f07bdbb8a964fd348223d3dc9c82ad5398a4f0432a719", size = 311553, upload-time = "2026-01-08T16:57:38.465Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/8a791cae2c98e5e44a158e15db50d21b7ec0b37aeaffa68d151bc8ffb6d6/zstd-1.5.7.3-pp311-pypy311_pp73-manylinux_2_14_x86_64.whl", hash = "sha256:fa4d760a220541b18ce732a3a2cf7547ea05afc76d05b3b39edebfeb721f6079", size = 317071, upload-time = "2026-01-08T16:36:07.47Z" }, + { url = "https://files.pythonhosted.org/packages/2f/25/b6624e6b08d515242154436c9d06fb20b790d300ac82e84f3c4c133e25e1/zstd-1.5.7.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a69e60146bf8aaa6a0e6c9a94a7c5f3133d68091e2e5c5a3c5ababf71fd5ec7a", size = 167654, upload-time = "2026-01-08T17:00:56.667Z" }, +] diff --git a/claude_plugins/prowler/.claude-plugin/plugin.json b/claude_plugins/prowler/.claude-plugin/plugin.json new file mode 100644 index 0000000000..7bf822e2e7 --- /dev/null +++ b/claude_plugins/prowler/.claude-plugin/plugin.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", + "name": "prowler", + "version": "0.1.0", + "description": "Prowler for Claude Code — cloud security and compliance skills powered by the Prowler MCP server. Bundles compliance triage and remediation; more skills coming.", + "author": { + "name": "Prowler", + "email": "support@prowler.com", + "url": "https://prowler.com" + }, + "homepage": "https://docs.prowler.com", + "repository": "https://github.com/prowler-cloud/prowler", + "license": "Apache-2.0", + "keywords": [ + "prowler", + "security", + "compliance", + "cloud-security", + "mcp" + ], + "userConfig": { + "api_key": { + "type": "string", + "title": "Prowler API key", + "description": "API key token used to authenticate with Prowler Cloud / Prowler App via the Prowler MCP server. Create one at https://cloud.prowler.com.", + "sensitive": true, + "required": true + } + } +} diff --git a/claude_plugins/prowler/.mcp.json b/claude_plugins/prowler/.mcp.json new file mode 100644 index 0000000000..9788b6d982 --- /dev/null +++ b/claude_plugins/prowler/.mcp.json @@ -0,0 +1,9 @@ +{ + "prowler": { + "type": "http", + "url": "https://mcp.prowler.com/mcp", + "headers": { + "Authorization": "Bearer ${user_config.api_key}" + } + } +} diff --git a/claude_plugins/prowler/README.md b/claude_plugins/prowler/README.md new file mode 100644 index 0000000000..6e204fbe44 --- /dev/null +++ b/claude_plugins/prowler/README.md @@ -0,0 +1,80 @@ +# Prowler for Claude Code + +End-to-end cloud security and compliance from inside [Claude Code](https://www.claude.com/product/claude-code), powered by the [Prowler MCP server](https://docs.prowler.com/projects/prowler-mcp/). The plugin lets Claude walk a Prowler Cloud-connected account through a compliance assessment and remediate findings until the chosen security or industry framework is compliant. + +> **Preview**: this plugin is under active development. Report issues at or join the [Slack community](https://goto.prowler.com/slack). + +## Requirements + +- [Claude Code](https://www.claude.com/product/claude-code) installed and signed in. +- A [Prowler Cloud](https://cloud.prowler.com) account (the free tier is enough to start). +- A Prowler API key — create one at . + +## Installation + +Inside a Claude Code session: + +```text +/plugin marketplace add prowler-cloud/prowler +/plugin install prowler@prowler-plugins +``` + +Or, if you already have the repo checked out locally: + +```text +/plugin marketplace add /absolute/path/to/prowler +/plugin install prowler@prowler-plugins +``` + +## Configuration + +On first install, Claude Code prompts for your **Prowler API key**. It is stored securely (macOS keychain or `~/.claude/.credentials.json`) and used to authenticate against Prowler Cloud. + +To rotate the key, uninstall and reinstall the plugin — Claude Code will prompt again. + +## Verify the install + +In a Claude Code session: + +```text +/mcp → "prowler" appears as a connected server +/plugin → "prowler" enabled, skill listed as prowler:framework-compliance-triage +``` + +If `/mcp` reports the `prowler` server as failed, the most common cause is a rejected API key — re-issue one in Prowler Cloud and reinstall the plugin so it re-prompts. + +## Usage + +Open a conversation that mentions the framework you want to comply with. Examples: + +- *"Make my AWS production account compliant with CIS 4.0."* +- *"Make my current Terraform project compliant with the Prowler ThreatScore Compliance Framework based on the latest scan results."* +- *"Help me get to 100% on PCI-DSS for this GCP project."* + +You pick a **primary tool** (Terraform, gh / az / aws CLI, web console, or mixed) and a **mode**: + +- **Claude-assisted** (default). Claude shows each fix — target resource, exact commands, side effects, reversibility — and waits for your go-ahead before applying. +- **Claude autonomous**. Claude presents a single up-front plan grouped by shared fixes, waits for one confirmation, then proceeds. It pauses mid-loop if a fix has wide blast radius or a finding is not applicable. + +Claude tracks progress in a markdown report under `.prowler/` at your project root — one file per framework × account. Open it any time to see exactly where the flow is. When all findings are addressed, Claude proposes a fresh Prowler scan to verify everything end-to-end. + +## Uninstalling + +```text +/plugin uninstall prowler@prowler-plugins +/plugin marketplace remove prowler-plugins +``` + +The stored API key is removed automatically. + +## Troubleshooting + +| Symptom | Likely cause | Fix | +|---|---|---| +| `/mcp` shows `prowler` as failed | Rejected API key | Generate a new one in Prowler Cloud and reinstall the plugin to re-prompt. | +| Skill not invoked when expected | The skill description didn't match the prompt | Mention the framework name plus "compliance" or "compliant" in your prompt. | +| "Framework not supported" | Prowler Hub does not list the framework for that provider | Open an issue or PR at . | + +## License + +Apache 2.0 — see [LICENSE](../../LICENSE). diff --git a/claude_plugins/prowler/skills/framework-compliance-triage/SKILL.md b/claude_plugins/prowler/skills/framework-compliance-triage/SKILL.md new file mode 100644 index 0000000000..1af29f82b9 --- /dev/null +++ b/claude_plugins/prowler/skills/framework-compliance-triage/SKILL.md @@ -0,0 +1,199 @@ +--- +name: framework-compliance-triage +description: Make a cloud account compliant with a security or industry framework using Prowler Cloud. +--- + +# Framework compliance + +Iterative, interactive flow that takes a cloud account through setup, reporting, and remediation until it complies with the chosen security or industry framework. + +## Checkpoints + +This skill uses **checkpoints** to mark moments where you must stop, post a clear question or summary to the user, and wait for the reply before continuing. Each checkpoint is rendered like this: + +> **Checkpoint — ** +> +> What to present, and what to wait for. + +Treat every checkpoint as a hard stop: + +- Do not skip a checkpoint because the user previously said "go ahead", "just do it", or similar. Confirmations are scoped to a single checkpoint and do not transfer to later ones. +- Do not bundle two checkpoints into one message. Post one, wait for the reply, then continue. +- Do not infer the user's answer from context or proceed on silence. Ask explicitly and wait. +- If a checkpoint is conditional (e.g. only fires when multiple accounts exist), evaluate the condition first; if it does not apply, continue without prompting. +- If the user's initial message already answers the question a checkpoint asks (e.g. "make my AWS subscription compliant with CIS using Terraform autonomously"), treat the checkpoint as satisfied for the parts they covered, and only ask for what is still missing. + +## 1. Initial Prowler Cloud setup + +> **Checkpoint — Provider and framework selection** +> +> If the user has not already specified both the provider and the framework, ask explicitly and wait for the answer. If they have specified them in their opening message, skip this checkpoint. + +Confirm both are supported by the Prowler Hub MCP: + +- Enumerate supported providers with `prowler_hub_list_providers`. +- Enumerate frameworks for the chosen provider with `prowler_hub_list_compliances`, passing the provider `id` as the only element of the `provider` input list. + +If the framework is not supported, tell the user, suggest they request it or contribute it themselves, and end the flow. Otherwise continue. + +### 1.1 Connect to Prowler Cloud + +Verify the Prowler MCP connection by calling `prowler_app_search_providers` — a successful response returns the list of providers. If the call fails, walk the user through troubleshooting: internet connectivity, Prowler Cloud credentials, and permissions on the Prowler Cloud account. +For getting accurate information about configurations use `prowler_docs_search` to pull relevant instructions from the Prowler documentation. + +### 1.2 Verify the provider is configured (or configure it) + +Call `prowler_app_search_providers` to check whether the target provider (AWS account, Azure Subscription, GitHub Account...) exists in the user's Prowler Cloud account. Handle the result based on what's found: + +- **Provider not present.** Guide the user through adding and configuring it. Retrieve the relevant connection, credential, and permission instructions with `prowler_docs_search`. +- **Provider present but misconfigured** (missing credentials, insufficient permissions, etc.). Walk the user through fixing the configuration, pulling the relevant guidance with `prowler_docs_search`. +- **Provider present and configured.** Continue. + +> **Checkpoint — Account selection** *(conditional: more than one account of the chosen provider is configured)* +> +> List the accounts with helpful detail (account name, uid, last scan date) and ask which one to use. Wait for the answer. If only one account exists, skip this checkpoint and use it. + +### 1.3 Review compliance report for the provider account + +The flow needs at least one completed scan with a compliance report available. + +Look for a completed scan first: call `prowler_app_list_scans` with the selected `provider_id` and `state: ["completed"]`, then call `prowler_app_get_compliance_overview` with each `scan_id` to find one whose compliance report is available. If one is found, continue to the next section. + +If no completed scan has a report, call `prowler_app_list_scans` again with `state: ["available", "executing"]` to detect a scan in progress. + +> **Checkpoint — Scan-in-progress decision** *(conditional: an in-progress scan was detected)* +> +> Tell the user a scan is already running and ask whether to wait for it to complete or start a fresh one. Wait for the answer. + +If no scan is running (or the user chose to start a fresh one), trigger a new scan with `prowler_app_trigger_scan` and the `provider_id`. The link `https://cloud.prowler.com/scans?filter%5Bprovider_uid__in%5D={provider_id}` lets the user monitor progress. + +When a scan is in progress (either pre-existing and elected to wait, or just triggered), stop the flow and ask the user to return when it's completed — restart this section to re-check the results. + +## 2. Compliance report + +Every iteration of the remediation loop reads and writes a single markdown file per provider account and framework, stored at `${CLAUDE_PROJECT_DIR}/.prowler/compliance--.md`. Sanitize `` to `[a-zA-Z0-9_-]` by replacing anything else with `-`. Create `.prowler/` if missing. + +Across iterations, edit only: status tags on failed requirements and their findings, the per-requirement `Fix plan` / `Fix applied` sub-bullets added during sections 3.3–3.4, the **Global remediation approach** block, and the **Activity log** (append-only, newest on top). Requirement descriptions, finding IDs, and the entire **Manual review requirements** section are read-only after first render. + +Status taxonomy for failed requirements and their findings: + +- `[FAIL]` — failing in the latest scan. +- `[IN PROGRESS]` — picked up by section 3.3. +- `[FIXED-UNVERIFIED]` — remediation applied; not yet confirmed. +- `[PASS]` — passing in the latest scan (set when a rescan in section 3.5 confirms the fix). +- `[SKIPPED]` — user explicitly deferred. + +### Report template + +A fresh report is rendered like this (substituting values from the `prowler_app_get_compliance_framework_state_details` Prowler MCP tool response): + +````markdown +# Compliance report: + +**Provider account**: +**Scan ID**: +**Generated**: +**Last update**: +**Status**: / passing (%) · failing · manual review + +## Global remediation approach + +- **Primary tool**: _Terraform | Azure CLI | AWS CLI | web console | mixed_ +- **Mode**: _Claude autonomous | Claude-assisted_ +- **Notes**: + +## Activity log +- — Report initialized from scan ``. + +## Failed requirements + +### — [FAIL] +**Description**: +**Findings** (): +- [FAIL] `` + +## Manual review requirements +- **** — [PENDING]: +```` + +### 2.1 Generate or refresh the report + +Resolve the report path for the current `compliance_id` and provider account. + +If the file does not exist, call `prowler_app_get_compliance_framework_state_details` for the target scan, render the template above, and write the file with one initialization entry in the activity log. + +If the file exists, read it and compare its `Scan ID` to the target scan from section 1.3. When the scan matches, reuse the file and summarize remaining `[FAIL]` and `[IN PROGRESS]` items in chat. + +> **Checkpoint — Report refresh** *(conditional: the file's `Scan ID` differs from the current target scan)* +> +> Tell the user the report on disk was generated from a different scan and ask whether to refresh it from the new scan. Wait for the answer. + +On confirmation, regenerate the failed-requirements section from the new `prowler_app_get_compliance_framework_state_details` response, carry forward the **Global remediation approach** block and the full activity log, and append an activity-log entry noting the scan change. + +Once the file is current, surface the top failing requirements in chat: sort by finding count descending, show the top 5 with their codes and counts, and point to the file path for the full list. + +## 3. Remediation loop + +### 3.1 Define the global remediation approach + +Two modes are available: + +- **Claude-assisted** (default when the user has not specified): per-requirement confirmation. For each requirement Claude shows the target resource, exact commands, side effects, and reversibility, then waits for explicit go-ahead before applying. +- **Claude autonomous**: no per-requirement gate, but Claude still presents one batch-level fix plan up front (§3.2) and waits for a single confirmation, and pauses if a finding looks not applicable, requires a paid feature, or has wide blast radius (breaks dev workflow, forces collaborator changes, is hard to reverse). + +If the user phrases their request as "just do it" or similar, treat that as autonomous **with** the batch-plan confirmation still required — the confirmation is a property of the skill, not the user's verbosity preference. + +> **Checkpoint — Global remediation approach** +> +> Ask the user which tool to use for fixes (Terraform, gh / az / aws CLI, web console, mixed...) and which mode to operate in. Wait for the answer before continuing. This checkpoint is non-negotiable: never assume a default tool, and never assume autonomous mode. + +Once answered, write the values into the **Global remediation approach** block of the report file. + +> **Checkpoint — Overwriting an existing approach** *(conditional: the block is already populated from a previous session)* +> +> Show the previous values and the new ones, and ask the user to confirm before overwriting. Wait for the answer. + +### 3.2 Present the batch fix plan *(autonomous mode only)* + +In **assisted** mode, skip this section — the per-requirement gate in §3.3 confirms each fix as it comes up. Only run §3.2 in **autonomous** mode, where the loop will otherwise apply fixes without further input. + +Before touching anything, post a single chat summary covering every `[FAIL]` requirement: + +- Group findings that share a fix (e.g. ten branch-protection requirements satisfied by one PUT call → present as one group). +- For each group: target resource, exact tool calls, side effects, reversibility. +- Call out findings that look **not applicable** to this target (e.g. an Organization-only check evaluated against a User account, a feature gated by a paid plan, a resource type the user doesn't have) and propose `[SKIPPED]` with the reason. +- Call out findings that require manual user action Claude cannot perform. + +> **Checkpoint — Batch fix plan approval** *(conditional: autonomous mode)* +> +> Post the grouped plan and wait for explicit confirmation. Do not start any fix before the user replies. + +Once approved, the loop proceeds through the batch without further prompts unless something deviates from the approved plan. + +### 3.3 Pick the first FAIL requirement and inspect its findings + +Pick the first `[FAIL]` requirement at the top of the failed-requirements section. Move its status and every finding under it to `[IN PROGRESS]`, and add a `**Fix plan**:` sub-bullet describing what will be done. + +Call `prowler_app_get_finding_details` for each `finding_id` to retrieve the failing resource and the Prowler Hub's remediation guidance for that check using the tool `prowler_hub_get_check_details` with the `check_id` from the finding details. Summarize the guidance in chat, and append it to the `**Fix plan**` note for each finding. + +If a finding does not apply to the target resource (Organization-only check on a User account, paid-tier feature, missing resource type, etc.), set the requirement status to `[SKIPPED]` with the reason, log it in the activity log, and move on without attempting the fix — even if it was missed during §3.2. + +> **Checkpoint — Per-requirement approval** *(conditional: assisted mode)* +> +> Post the per-requirement plan in chat — resource, command, side effects, reversibility — and wait for confirmation before moving to §3.4. In **autonomous** mode, post the plan for transparency but proceed unless it deviates from the batch plan agreed in §3.2. + +### 3.4 Diagnose, fix, verify + +Read the remediation guidance returned in §3.3, identify the root cause, and apply the fix using the tool defined in the **Global remediation approach** block. After applying, verify via the same tool that applied the fix or via a provider API call when applicable. If the re-read shows the change did not land, leave the status at `[IN PROGRESS]`, surface the error to the user, and stop the loop for this requirement. + +When the change is in place, append a `**Fix applied**: ` sub-bullet to the requirement, move each fixed finding to `[FIXED-UNVERIFIED]`, and add one activity-log entry describing the change. If no programmatic verification was possible (e.g. web console action), note in the activity log that confirmation depends on the rescan in §3.5. + +### 3.5 Loop + +Move to the next `[FAIL]` requirement and repeat from section 3.3. + +> **Checkpoint — Rescan trigger** *(conditional: no `[FAIL]` requirements remain; all are `[FIXED-UNVERIFIED]` or `[SKIPPED]`)* +> +> Summarize what was applied, list any `[SKIPPED]` items with reasons, and ask whether to trigger a fresh scan with `prowler_app_trigger_scan` to verify the fixes end-to-end. Wait for the answer. + +On confirmation, trigger the rescan. When it completes, restart section 2.1 with the carry-forward path — requirements no longer in the new FAIL list move to `[PASS]`, anything still failing reverts to `[FAIL]` with the previous fix attempt visible in the activity log. diff --git a/contrib/PowerBI/Multicloud CIS Benchmarks/Prowler Multicloud CIS Benchmarks.pbit b/contrib/PowerBI/Multicloud CIS Benchmarks/Prowler Multicloud CIS Benchmarks.pbit index b1dc68338e..2349ea33bd 100644 Binary files a/contrib/PowerBI/Multicloud CIS Benchmarks/Prowler Multicloud CIS Benchmarks.pbit and b/contrib/PowerBI/Multicloud CIS Benchmarks/Prowler Multicloud CIS Benchmarks.pbit differ diff --git a/contrib/aws/multi-account-securityhub/run-prowler-securityhub.sh b/contrib/aws/multi-account-securityhub/run-prowler-securityhub.sh index c101da8613..3a60faa1f8 100755 --- a/contrib/aws/multi-account-securityhub/run-prowler-securityhub.sh +++ b/contrib/aws/multi-account-securityhub/run-prowler-securityhub.sh @@ -1,8 +1,9 @@ #!/bin/bash # Run Prowler against All AWS Accounts in an AWS Organization -# Activate Poetry Environment -eval "$(poetry env activate)" +# Activate uv-managed virtualenv +# shellcheck disable=SC1091 +source .venv/bin/activate # Show Prowler Version prowler -v diff --git a/contrib/inventory-graph/README.md b/contrib/inventory-graph/README.md new file mode 100644 index 0000000000..37e6292b1b --- /dev/null +++ b/contrib/inventory-graph/README.md @@ -0,0 +1,335 @@ +# AWS Inventory Connectivity Graph + +A community-contributed tool that generates interactive connectivity graphs from Prowler AWS scans, visualizing relationships between AWS resources with zero additional API calls. + +## Overview + +This tool extends Prowler by producing two artifacts after a scan completes: + +- **`.inventory.json`** – Machine-readable graph (nodes + edges) +- **`.inventory.html`** – Interactive D3.js force-directed visualization + +### Why? + +Prowler's existing outputs (CSV, ASFF, OCSF, HTML) report individual check findings but provide no cross-service topology view. Security engineers need to understand **how** resources are connected—which Lambda functions sit inside which VPC, which IAM roles can be assumed by which services, which event sources trigger which functions—before they can reason about attack paths, blast-radius, or lateral-movement risk. + +This tool fills that gap by building a connectivity graph from the service clients that are already loaded during a Prowler scan. + +## Features + +### Supported AWS Services + +The tool currently extracts connectivity information from: + +- **Lambda** – Functions, VPC/subnet/SG edges, event source mappings, layers, DLQ, KMS +- **EC2** – Instances, security groups, subnet/VPC edges +- **VPC** – VPCs, subnets, peering connections +- **RDS** – DB instances, VPC/SG/cluster/KMS edges +- **ELBv2** – ALB/NLB load balancers, SG and VPC edges +- **S3** – Buckets, replication targets, logging buckets, KMS keys +- **IAM** – Roles, trust-relationship edges (who can assume what) + +### Edge Semantic Types + +Edges are typed for downstream filtering and attack-path analysis: + +- `network` – Resources share a network path (VPC/subnet/SG) +- `iam` – IAM trust or permission relationship +- `triggers` – One resource can invoke another (event source → Lambda) +- `data_flow` – Data is written/read (Lambda → SQS dead-letter queue) +- `depends_on` – Soft dependency (Lambda layer, subnet belongs to VPC) +- `routes_to` – Traffic routing (LB → target) +- `replicates_to` – S3 replication +- `encrypts` – KMS key encrypts the resource +- `logs_to` – Logging relationship + +### Interactive HTML Graph Features + +- Force-directed layout with drag-and-drop node pinning +- Zoom / pan (mouse wheel + click-drag on background) +- Per-service color-coded nodes with a legend +- Hover tooltips showing ARN + all metadata properties +- Service filter dropdown (show only Lambda, EC2, RDS, etc.) +- Adjustable link-distance and charge-strength physics sliders +- Edge labels on every arrow + +## Installation + +### Prerequisites + +- Python 3.9.1 or higher +- Prowler installed and configured (see [Prowler documentation](https://docs.prowler.com/)) + +### Setup + +1. Clone or download this directory to your local machine +2. Ensure Prowler is installed and working +3. No additional dependencies required beyond Prowler's existing requirements + +## Usage + +### Basic Usage + +Run Prowler with your desired checks, then use the inventory graph script: + +```bash +# Run Prowler scan (example) +prowler aws --output-formats csv + +# Generate inventory graph from the scan +python contrib/inventory-graph/inventory_graph.py --output-directory ./output +``` + +### Command-Line Options + +```bash +python contrib/inventory-graph/inventory_graph.py [OPTIONS] + +Options: + --output-directory DIR Directory to save output files (default: ./output) + --output-filename NAME Base filename without extension (default: prowler-inventory-) + --help Show this help message and exit +``` + +### Example Workflow + +```bash +# 1. Run a Prowler scan on your AWS account +prowler aws --profile my-aws-profile --output-formats csv html + +# 2. Generate the inventory graph +python contrib/inventory-graph/inventory_graph.py \ + --output-directory ./output \ + --output-filename my-aws-inventory + +# 3. Open the HTML file in your browser +open output/my-aws-inventory.inventory.html +``` + +### Integration with Prowler Scans + +The tool reads from already-loaded AWS service clients in memory (via `sys.modules`). This means: + +- **Zero extra AWS API calls** – Uses data already collected during the Prowler scan +- **Graceful degradation** – Services not scanned are silently skipped +- **Flexible** – Works with any subset of Prowler checks + +## Output Files + +### JSON Output (`*.inventory.json`) + +Machine-readable graph structure: + +```json +{ + "generated_at": "2026-03-19T12:34:56Z", + "nodes": [ + { + "id": "arn:aws:lambda:us-east-1:123456789012:function:my-function", + "type": "lambda_function", + "name": "my-function", + "service": "lambda", + "region": "us-east-1", + "account_id": "123456789012", + "properties": { + "runtime": "python3.9", + "vpc_id": "vpc-abc123" + } + } + ], + "edges": [ + { + "source_id": "arn:aws:lambda:...", + "target_id": "arn:aws:ec2:...:vpc/vpc-abc123", + "edge_type": "network", + "label": "in-vpc" + } + ], + "stats": { + "node_count": 42, + "edge_count": 87 + } +} +``` + +### HTML Output (`*.inventory.html`) + +Self-contained interactive visualization that opens in any modern browser. No server or build step required. + +## Architecture + +### Design Decisions + +| Decision | Rationale | +|----------|-----------| +| **Read from sys.modules** | Zero extra AWS API calls; services not scanned are silently skipped | +| **Self-contained HTML** | D3.js v7 via CDN; no server, no build step; opens in any browser | +| **One extractor per service** | Each extractor is independently testable; adding a new service = one new file + one line in the registry | +| **Typed edges** | Semantic types allow downstream consumers (attack-path tools, Neo4j import) to filter by relationship class | + +### Project Structure + +``` +contrib/inventory-graph/ +├── README.md # This file +├── inventory_graph.py # Main entry point script +├── lib/ +│ ├── __init__.py +│ ├── models.py # ResourceNode, ResourceEdge, ConnectivityGraph dataclasses +│ ├── graph_builder.py # Reads loaded service clients from sys.modules +│ ├── inventory_output.py # write_json(), write_html() +│ └── extractors/ +│ ├── __init__.py +│ ├── lambda_extractor.py # Lambda functions → VPC/subnet/SG/event-sources/layers/DLQ/KMS +│ ├── ec2_extractor.py # EC2 instances + security groups → subnet/VPC +│ ├── vpc_extractor.py # VPCs, subnets, peering connections +│ ├── rds_extractor.py # RDS instances → VPC/SG/cluster/KMS +│ ├── elbv2_extractor.py # ALB/NLB load balancers → SG/VPC +│ ├── s3_extractor.py # S3 buckets → replication targets/logging buckets/KMS keys +│ └── iam_extractor.py # IAM roles + trust-relationship edges +└── examples/ + └── sample_output.html # Example output (optional) +``` + +## Testing + +### Smoke Test (No AWS Credentials Needed) + +```python +import sys +from unittest.mock import MagicMock + +# Wire a fake Lambda client +mock_module = MagicMock() +mock_fn = MagicMock() +mock_fn.arn = "arn:aws:lambda:us-east-1:123:function:test" +mock_fn.name = "test" +mock_fn.region = "us-east-1" +mock_fn.vpc_id = "vpc-abc" +mock_fn.security_groups = ["sg-111"] +mock_fn.subnet_ids = {"subnet-aaa"} +mock_fn.environment = None +mock_fn.kms_key_arn = None +mock_fn.layers = [] +mock_fn.dead_letter_config = None +mock_fn.event_source_mappings = [] +mock_module.awslambda_client.functions = {mock_fn.arn: mock_fn} +mock_module.awslambda_client.audited_account = "123" +sys.modules["prowler.providers.aws.services.awslambda.awslambda_client"] = mock_module + +from contrib.inventory_graph.lib.graph_builder import build_graph +from contrib.inventory_graph.lib.inventory_output import write_json, write_html + +graph = build_graph() +write_json(graph, "/tmp/test.inventory.json") +write_html(graph, "/tmp/test.inventory.html") +# Open /tmp/test.inventory.html in a browser +``` + +## Extending + +### Adding a New Service + +1. Create a new extractor file in `lib/extractors/` (e.g., `dynamodb_extractor.py`) +2. Implement the `extract(client)` function that returns `(nodes, edges)` +3. Register it in `lib/graph_builder.py` in the `_SERVICE_REGISTRY` tuple + +Example extractor template: + +```python +from typing import List, Tuple +from prowler.lib.outputs.inventory.models import ResourceNode, ResourceEdge + +def extract(client) -> Tuple[List[ResourceNode], List[ResourceEdge]]: + """Extract DynamoDB tables and their relationships.""" + nodes = [] + edges = [] + + for table in client.tables: + nodes.append( + ResourceNode( + id=table.arn, + type="dynamodb_table", + name=table.name, + service="dynamodb", + region=table.region, + account_id=client.audited_account, + properties={"billing_mode": table.billing_mode} + ) + ) + + # Add edges for KMS encryption, streams, etc. + if table.kms_key_arn: + edges.append( + ResourceEdge( + source_id=table.kms_key_arn, + target_id=table.arn, + edge_type="encrypts", + label="encrypts" + ) + ) + + return nodes, edges +``` + +## Troubleshooting + +### No nodes discovered + +**Problem:** The tool reports "no nodes discovered" after running. + +**Solution:** Ensure you've run a Prowler scan first. The tool reads from in-memory service clients loaded during the scan. If no services were scanned, no nodes will be discovered. + +### Missing services in the graph + +**Problem:** Some AWS services are not appearing in the graph. + +**Solution:** The tool only includes services that have been scanned by Prowler. Run Prowler with the services you want to include, or run without service filters to scan all available services. + +### HTML file doesn't display properly + +**Problem:** The HTML visualization doesn't load or shows errors. + +**Solution:** +- Ensure you're opening the file in a modern browser (Chrome, Firefox, Safari, Edge) +- Check your browser's console for JavaScript errors +- Verify the file was generated completely (check file size > 0) +- The HTML requires internet access to load D3.js from CDN + +## Roadmap + +Potential future enhancements: + +- [ ] Support for additional AWS services (DynamoDB, SQS, SNS, etc.) +- [ ] Export to Neo4j / Cartography format +- [ ] Attack path analysis integration +- [ ] Multi-account/multi-region aggregation +- [ ] Custom edge type filtering in HTML UI +- [ ] Graph diff between two scans + +## Contributing + +This is a community contribution. If you'd like to enhance it: + +1. Fork the Prowler repository +2. Make your changes in `contrib/inventory-graph/` +3. Test thoroughly +4. Submit a pull request with a clear description + +## License + +This tool is part of the Prowler project and is licensed under the Apache License 2.0. + +## Credits + +- **Author:** [@sandiyochristan](https://github.com/sandiyochristan) +- **Related PR:** [#10382](https://github.com/prowler-cloud/prowler/pull/10382) +- **Prowler Project:** [prowler-cloud/prowler](https://github.com/prowler-cloud/prowler) + +## Support + +For issues or questions: + +- Open an issue in the [Prowler repository](https://github.com/prowler-cloud/prowler/issues) +- Join the [Prowler Community Slack](https://goto.prowler.com/slack) +- Tag your issue with `contrib:inventory-graph` diff --git a/contrib/inventory-graph/examples/example_usage.py b/contrib/inventory-graph/examples/example_usage.py new file mode 100644 index 0000000000..f651103b47 --- /dev/null +++ b/contrib/inventory-graph/examples/example_usage.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +""" +Example: Generate AWS Inventory Graph with Mock Data + +This example demonstrates how to use the inventory graph tool with mock AWS data. +No AWS credentials required. +""" + +import sys +from pathlib import Path +from unittest.mock import MagicMock + +# Add parent directory to path +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from lib.graph_builder import build_graph +from lib.inventory_output import write_json, write_html + + +def create_mock_lambda_client(): + """Create a mock Lambda client with sample data.""" + mock_module = MagicMock() + + # Create a mock Lambda function + mock_fn = MagicMock() + mock_fn.arn = "arn:aws:lambda:us-east-1:123456789012:function:my-test-function" + mock_fn.name = "my-test-function" + mock_fn.region = "us-east-1" + mock_fn.vpc_id = "vpc-abc123" + mock_fn.security_groups = ["sg-111222"] + mock_fn.subnet_ids = {"subnet-aaa111", "subnet-bbb222"} + mock_fn.environment = {"Variables": {"ENV": "production"}} + mock_fn.kms_key_arn = ( + "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012" + ) + mock_fn.layers = [] + mock_fn.dead_letter_config = None + mock_fn.event_source_mappings = [] + + mock_module.awslambda_client.functions = {mock_fn.arn: mock_fn} + mock_module.awslambda_client.audited_account = "123456789012" + + return mock_module + + +def create_mock_ec2_client(): + """Create a mock EC2 client with sample data.""" + mock_module = MagicMock() + + # Create a mock EC2 instance + mock_instance = MagicMock() + mock_instance.arn = ( + "arn:aws:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0" + ) + mock_instance.id = "i-1234567890abcdef0" + mock_instance.region = "us-east-1" + mock_instance.vpc_id = "vpc-abc123" + mock_instance.subnet_id = "subnet-aaa111" + mock_instance.security_groups = [MagicMock(id="sg-111222")] + mock_instance.state = "running" + mock_instance.type = "t3.micro" + mock_instance.tags = [{"Key": "Name", "Value": "test-instance"}] + + # Create a mock security group + mock_sg = MagicMock() + mock_sg.arn = "arn:aws:ec2:us-east-1:123456789012:security-group/sg-111222" + mock_sg.id = "sg-111222" + mock_sg.name = "test-security-group" + mock_sg.region = "us-east-1" + mock_sg.vpc_id = "vpc-abc123" + + mock_module.ec2_client.instances = [mock_instance] + mock_module.ec2_client.security_groups = [mock_sg] + mock_module.ec2_client.audited_account = "123456789012" + + return mock_module + + +def create_mock_vpc_client(): + """Create a mock VPC client with sample data.""" + mock_module = MagicMock() + + # Create a mock VPC + mock_vpc = MagicMock() + mock_vpc.arn = "arn:aws:ec2:us-east-1:123456789012:vpc/vpc-abc123" + mock_vpc.id = "vpc-abc123" + mock_vpc.region = "us-east-1" + mock_vpc.cidr_block = "10.0.0.0/16" + mock_vpc.tags = [{"Key": "Name", "Value": "test-vpc"}] + + # Create mock subnets + mock_subnet1 = MagicMock() + mock_subnet1.arn = "arn:aws:ec2:us-east-1:123456789012:subnet/subnet-aaa111" + mock_subnet1.id = "subnet-aaa111" + mock_subnet1.region = "us-east-1" + mock_subnet1.vpc_id = "vpc-abc123" + mock_subnet1.cidr_block = "10.0.1.0/24" + mock_subnet1.availability_zone = "us-east-1a" + + mock_subnet2 = MagicMock() + mock_subnet2.arn = "arn:aws:ec2:us-east-1:123456789012:subnet/subnet-bbb222" + mock_subnet2.id = "subnet-bbb222" + mock_subnet2.region = "us-east-1" + mock_subnet2.vpc_id = "vpc-abc123" + mock_subnet2.cidr_block = "10.0.2.0/24" + mock_subnet2.availability_zone = "us-east-1b" + + mock_module.vpc_client.vpcs = [mock_vpc] + mock_module.vpc_client.subnets = [mock_subnet1, mock_subnet2] + mock_module.vpc_client.vpc_peering_connections = [] + mock_module.vpc_client.audited_account = "123456789012" + + return mock_module + + +def main(): + """Main function to demonstrate the inventory graph generation.""" + print("=" * 70) + print("AWS Inventory Graph - Mock Data Example") + print("=" * 70) + print() + + # Create mock clients and inject them into sys.modules + print("Creating mock AWS service clients...") + sys.modules["prowler.providers.aws.services.awslambda.awslambda_client"] = ( + create_mock_lambda_client() + ) + sys.modules["prowler.providers.aws.services.ec2.ec2_client"] = ( + create_mock_ec2_client() + ) + sys.modules["prowler.providers.aws.services.vpc.vpc_client"] = ( + create_mock_vpc_client() + ) + print("✓ Mock clients created") + print() + + # Build the graph + print("Building connectivity graph...") + graph = build_graph() + print(f"✓ Graph built: {len(graph.nodes)} nodes, {len(graph.edges)} edges") + print() + + # Display discovered nodes + print("Discovered nodes:") + for node in graph.nodes: + print(f" - {node.type}: {node.name} ({node.region})") + print() + + # Display discovered edges + print("Discovered edges:") + for edge in graph.edges: + source_node = next((n for n in graph.nodes if n.id == edge.source_id), None) + target_node = next((n for n in graph.nodes if n.id == edge.target_id), None) + source_name = source_node.name if source_node else edge.source_id + target_name = target_node.name if target_node else edge.target_id + print(f" - {source_name} --[{edge.edge_type}]--> {target_name}") + print() + + # Write outputs + output_dir = Path(__file__).parent + json_path = output_dir / "example_output.inventory.json" + html_path = output_dir / "example_output.inventory.html" + + print("Writing output files...") + write_json(graph, str(json_path)) + write_html(graph, str(html_path)) + print(f"✓ JSON written to: {json_path}") + print(f"✓ HTML written to: {html_path}") + print() + + print("=" * 70) + print("✓ Example complete!") + print("=" * 70) + print() + print(f"Open the HTML file to view the interactive graph:") + print(f" open {html_path}") + print() + + +if __name__ == "__main__": + main() diff --git a/contrib/inventory-graph/inventory_graph.py b/contrib/inventory-graph/inventory_graph.py new file mode 100755 index 0000000000..d4f9c50582 --- /dev/null +++ b/contrib/inventory-graph/inventory_graph.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +""" +AWS Inventory Connectivity Graph Generator + +A standalone tool that generates interactive connectivity graphs from Prowler AWS scans. +This tool reads from already-loaded AWS service clients in memory and produces: + - JSON graph (nodes + edges) + - Interactive HTML visualization + +Usage: + python inventory_graph.py --output-directory ./output --output-filename my-inventory + +For more information, see README.md +""" + +import argparse +import os +import sys +from datetime import datetime +from pathlib import Path + +# Add the contrib directory to the path so we can import the lib modules +CONTRIB_DIR = Path(__file__).parent +sys.path.insert(0, str(CONTRIB_DIR)) + +from lib.graph_builder import build_graph +from lib.inventory_output import write_json, write_html + + +def parse_arguments(): + """Parse command-line arguments.""" + parser = argparse.ArgumentParser( + description="Generate AWS inventory connectivity graph from Prowler scan data", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Generate graph with default settings + python inventory_graph.py + + # Specify custom output directory and filename + python inventory_graph.py --output-directory ./my-output --output-filename aws-inventory + + # After running a Prowler scan + prowler aws --profile my-profile + python inventory_graph.py --output-directory ./output + +For more information, see README.md + """, + ) + + parser.add_argument( + "--output-directory", + "-o", + default="./output", + help="Directory to save output files (default: ./output)", + ) + + parser.add_argument( + "--output-filename", + "-f", + default=None, + help="Base filename without extension (default: prowler-inventory-)", + ) + + parser.add_argument( + "--verbose", + "-v", + action="store_true", + help="Enable verbose output", + ) + + return parser.parse_args() + + +def main(): + """Main entry point for the inventory graph generator.""" + args = parse_arguments() + + # Set up output paths + output_dir = Path(args.output_directory) + output_dir.mkdir(parents=True, exist_ok=True) + + # Generate filename with timestamp if not provided + if args.output_filename: + base_filename = args.output_filename + else: + timestamp = datetime.now().strftime("%Y%m%d%H%M%S") + base_filename = f"prowler-inventory-{timestamp}" + + json_path = output_dir / f"{base_filename}.inventory.json" + html_path = output_dir / f"{base_filename}.inventory.html" + + print("=" * 70) + print("AWS Inventory Connectivity Graph Generator") + print("=" * 70) + print() + + # Build the graph from loaded service clients + if args.verbose: + print("Building connectivity graph from loaded AWS service clients...") + + graph = build_graph() + + # Check if any nodes were discovered + if not graph.nodes: + print("⚠️ WARNING: No nodes discovered!") + print() + print("This usually means:") + print(" 1. No Prowler scan has been run yet in this Python session") + print(" 2. No AWS service clients are loaded in memory") + print() + print("To fix this:") + print(" 1. Run a Prowler scan first: prowler aws --output-formats csv") + print(" 2. Then run this script in the same session") + print() + print( + "Alternatively, integrate this tool directly into Prowler's output pipeline." + ) + sys.exit(1) + + print(f"✓ Discovered {len(graph.nodes)} nodes and {len(graph.edges)} edges") + print() + + # Write outputs + if args.verbose: + print(f"Writing JSON output to: {json_path}") + write_json(graph, str(json_path)) + + if args.verbose: + print(f"Writing HTML output to: {html_path}") + write_html(graph, str(html_path)) + + print() + print("=" * 70) + print("✓ Graph generation complete!") + print("=" * 70) + print() + print(f"📄 JSON: {json_path}") + print(f"🌐 HTML: {html_path}") + print() + print(f"Open the HTML file in your browser to explore the interactive graph:") + print(f" open {html_path}") + print() + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("\n\nInterrupted by user. Exiting...") + sys.exit(130) + except Exception as e: + print(f"\n❌ Error: {e}", file=sys.stderr) + if "--verbose" in sys.argv or "-v" in sys.argv: + import traceback + + traceback.print_exc() + sys.exit(1) diff --git a/prowler/lib/outputs/compliance/essential_eight/__init__.py b/contrib/inventory-graph/lib/__init__.py similarity index 100% rename from prowler/lib/outputs/compliance/essential_eight/__init__.py rename to contrib/inventory-graph/lib/__init__.py diff --git a/tests/lib/outputs/compliance/essential_eight/__init__.py b/contrib/inventory-graph/lib/extractors/__init__.py similarity index 100% rename from tests/lib/outputs/compliance/essential_eight/__init__.py rename to contrib/inventory-graph/lib/extractors/__init__.py diff --git a/contrib/inventory-graph/lib/extractors/ec2_extractor.py b/contrib/inventory-graph/lib/extractors/ec2_extractor.py new file mode 100644 index 0000000000..c383e6b6d5 --- /dev/null +++ b/contrib/inventory-graph/lib/extractors/ec2_extractor.py @@ -0,0 +1,94 @@ +from typing import List, Tuple + +from lib.models import ResourceEdge, ResourceNode + + +def extract(client) -> Tuple[List[ResourceNode], List[ResourceEdge]]: + """ + Extract EC2 instance and security-group nodes with their edges. + + Edges produced: + - instance → security-group [network] + - instance → subnet [network] + - security-group → VPC [network] + """ + nodes: List[ResourceNode] = [] + edges: List[ResourceEdge] = [] + + # EC2 Instances + for instance in client.instances: + name = instance.id + for tag in instance.tags or []: + if tag.get("Key") == "Name": + name = tag["Value"] + break + + props = { + "instance_type": getattr(instance, "type", None), + "state": getattr(instance, "state", None), + "vpc_id": getattr(instance, "vpc_id", None), + "subnet_id": getattr(instance, "subnet_id", None), + "public_ip": getattr(instance, "public_ip_address", None), + "private_ip": getattr(instance, "private_ip_address", None), + } + + nodes.append( + ResourceNode( + id=instance.arn, + type="ec2_instance", + name=name, + service="ec2", + region=instance.region, + account_id=client.audited_account, + properties={k: v for k, v in props.items() if v is not None}, + ) + ) + + for sg_id in instance.security_groups or []: + edges.append( + ResourceEdge( + source_id=instance.arn, + target_id=sg_id, + edge_type="network", + label="sg", + ) + ) + + if instance.subnet_id: + edges.append( + ResourceEdge( + source_id=instance.arn, + target_id=instance.subnet_id, + edge_type="network", + label="subnet", + ) + ) + + # Security Groups + for sg in client.security_groups.values(): + name = ( + sg.name if hasattr(sg, "name") else sg.id if hasattr(sg, "id") else sg.arn + ) + nodes.append( + ResourceNode( + id=sg.arn, + type="security_group", + name=name, + service="ec2", + region=sg.region, + account_id=client.audited_account, + properties={"vpc_id": sg.vpc_id}, + ) + ) + + if sg.vpc_id: + edges.append( + ResourceEdge( + source_id=sg.arn, + target_id=sg.vpc_id, + edge_type="network", + label="in-vpc", + ) + ) + + return nodes, edges diff --git a/contrib/inventory-graph/lib/extractors/elbv2_extractor.py b/contrib/inventory-graph/lib/extractors/elbv2_extractor.py new file mode 100644 index 0000000000..f06d3295c7 --- /dev/null +++ b/contrib/inventory-graph/lib/extractors/elbv2_extractor.py @@ -0,0 +1,60 @@ +from typing import List, Tuple + +from lib.models import ResourceEdge, ResourceNode + + +def extract(client) -> Tuple[List[ResourceNode], List[ResourceEdge]]: + """ + Extract ELBv2 (ALB/NLB) load balancer nodes and their edges. + + Edges produced: + - load_balancer → security-group [network] + - load_balancer → VPC [network] + """ + nodes: List[ResourceNode] = [] + edges: List[ResourceEdge] = [] + + for lb in client.loadbalancersv2.values(): + props = { + "type": getattr(lb, "type", None), + "scheme": getattr(lb, "scheme", None), + "dns_name": getattr(lb, "dns", None), + "vpc_id": getattr(lb, "vpc_id", None), + } + + name = getattr(lb, "name", lb.arn.split("/")[-2] if "/" in lb.arn else lb.arn) + + nodes.append( + ResourceNode( + id=lb.arn, + type="load_balancer", + name=name, + service="elbv2", + region=lb.region, + account_id=client.audited_account, + properties={k: v for k, v in props.items() if v is not None}, + ) + ) + + for sg_id in lb.security_groups or []: + edges.append( + ResourceEdge( + source_id=lb.arn, + target_id=sg_id, + edge_type="network", + label="sg", + ) + ) + + vpc_id = getattr(lb, "vpc_id", None) + if vpc_id: + edges.append( + ResourceEdge( + source_id=lb.arn, + target_id=vpc_id, + edge_type="network", + label="in-vpc", + ) + ) + + return nodes, edges diff --git a/contrib/inventory-graph/lib/extractors/iam_extractor.py b/contrib/inventory-graph/lib/extractors/iam_extractor.py new file mode 100644 index 0000000000..99d671e4ae --- /dev/null +++ b/contrib/inventory-graph/lib/extractors/iam_extractor.py @@ -0,0 +1,84 @@ +import json +from typing import Any, Dict, List, Tuple + +from prowler.lib.logger import logger +from lib.models import ResourceEdge, ResourceNode + + +def _parse_trust_principals(assume_role_policy: Any) -> List[str]: + """ + Return a flat list of principal strings from an IAM assume-role policy document. + The policy may be a dict already or a JSON string. + """ + if not assume_role_policy: + return [] + + if isinstance(assume_role_policy, str): + try: + assume_role_policy = json.loads(assume_role_policy) + except (json.JSONDecodeError, ValueError): + return [] + + principals = [] + for statement in assume_role_policy.get("Statement", []): + principal = statement.get("Principal", {}) + if isinstance(principal, str): + principals.append(principal) + elif isinstance(principal, dict): + for v in principal.values(): + if isinstance(v, list): + principals.extend(v) + else: + principals.append(v) + elif isinstance(principal, list): + principals.extend(principal) + + return principals + + +def extract(client) -> Tuple[List[ResourceNode], List[ResourceEdge]]: + """ + Extract IAM role nodes and their trust-relationship edges. + + Edges produced: + - trusted-principal → role [iam] (who can assume this role) + """ + nodes: List[ResourceNode] = [] + edges: List[ResourceEdge] = [] + + for role in client.roles: + props: Dict[str, Any] = { + "path": getattr(role, "path", None), + "create_date": str(getattr(role, "create_date", "") or ""), + } + + nodes.append( + ResourceNode( + id=role.arn, + type="iam_role", + name=role.name, + service="iam", + region="global", + account_id=client.audited_account, + properties={k: v for k, v in props.items() if v}, + ) + ) + + # Trust-relationship edges: principal → role (principal CAN assume role) + try: + for principal in _parse_trust_principals(role.assume_role_policy): + if principal and principal != "*": + edges.append( + ResourceEdge( + source_id=principal, + target_id=role.arn, + edge_type="iam", + label="can-assume", + ) + ) + except Exception as e: + logger.debug( + f"inventory iam_extractor: could not parse trust policy for {role.arn}: {e}" + ) + + return nodes, edges diff --git a/contrib/inventory-graph/lib/extractors/lambda_extractor.py b/contrib/inventory-graph/lib/extractors/lambda_extractor.py new file mode 100644 index 0000000000..63b0a0b6d0 --- /dev/null +++ b/contrib/inventory-graph/lib/extractors/lambda_extractor.py @@ -0,0 +1,118 @@ +from typing import List, Tuple + +from lib.models import ResourceEdge, ResourceNode + + +def extract(client) -> Tuple[List[ResourceNode], List[ResourceEdge]]: + """ + Extract Lambda function nodes and their edges from an awslambda_client. + + Edges produced: + - lambda → VPC [network] + - lambda → subnet [network] + - lambda → sg [network] + - lambda → event-source[triggers] (from EventSourceMapping) + - lambda → layer ARN [depends_on] + - lambda → DLQ target [data_flow] + - lambda → KMS key [encrypts] + """ + nodes: List[ResourceNode] = [] + edges: List[ResourceEdge] = [] + + for fn in client.functions.values(): + props = { + "runtime": fn.runtime, + "vpc_id": fn.vpc_id, + } + if fn.environment: + props["has_env_vars"] = True + if fn.kms_key_arn: + props["kms_key_arn"] = fn.kms_key_arn + + nodes.append( + ResourceNode( + id=fn.arn, + type="lambda_function", + name=fn.name, + service="lambda", + region=fn.region, + account_id=client.audited_account, + properties=props, + ) + ) + + # Network edges → VPC, subnets, security groups + if fn.vpc_id: + edges.append( + ResourceEdge( + source_id=fn.arn, + target_id=fn.vpc_id, + edge_type="network", + label="in-vpc", + ) + ) + for sg_id in fn.security_groups or []: + edges.append( + ResourceEdge( + source_id=fn.arn, + target_id=sg_id, + edge_type="network", + label="sg", + ) + ) + for subnet_id in fn.subnet_ids or set(): + edges.append( + ResourceEdge( + source_id=fn.arn, + target_id=subnet_id, + edge_type="network", + label="subnet", + ) + ) + + # Trigger edges from event source mappings + for esm in getattr(fn, "event_source_mappings", []): + edges.append( + ResourceEdge( + source_id=esm.event_source_arn, + target_id=fn.arn, + edge_type="triggers", + label=f"esm:{esm.state}", + ) + ) + + # Layer dependency edges + for layer in getattr(fn, "layers", []): + edges.append( + ResourceEdge( + source_id=fn.arn, + target_id=layer.arn, + edge_type="depends_on", + label="layer", + ) + ) + + # Dead-letter queue data-flow edge + dlq = getattr(fn, "dead_letter_config", None) + if dlq and dlq.target_arn: + edges.append( + ResourceEdge( + source_id=fn.arn, + target_id=dlq.target_arn, + edge_type="data_flow", + label="dlq", + ) + ) + + # KMS encryption edge + if fn.kms_key_arn: + edges.append( + ResourceEdge( + source_id=fn.kms_key_arn, + target_id=fn.arn, + edge_type="encrypts", + label="kms", + ) + ) + + return nodes, edges diff --git a/contrib/inventory-graph/lib/extractors/rds_extractor.py b/contrib/inventory-graph/lib/extractors/rds_extractor.py new file mode 100644 index 0000000000..3772453edc --- /dev/null +++ b/contrib/inventory-graph/lib/extractors/rds_extractor.py @@ -0,0 +1,86 @@ +from typing import List, Tuple + +from lib.models import ResourceEdge, ResourceNode + + +def extract(client) -> Tuple[List[ResourceNode], List[ResourceEdge]]: + """ + Extract RDS DB instance nodes and their edges. + + Edges produced: + - db_instance → security-group [network] + - db_instance → VPC [network] + - db_instance → cluster [depends_on] + - db_instance → KMS key [encrypts] + """ + nodes: List[ResourceNode] = [] + edges: List[ResourceEdge] = [] + + for db in client.db_instances.values(): + props = { + "engine": getattr(db, "engine", None), + "engine_version": getattr(db, "engine_version", None), + "instance_class": getattr(db, "db_instance_class", None), + "vpc_id": getattr(db, "vpc_id", None), + "multi_az": getattr(db, "multi_az", None), + "publicly_accessible": getattr(db, "publicly_accessible", None), + "storage_encrypted": getattr(db, "storage_encrypted", None), + } + + nodes.append( + ResourceNode( + id=db.arn, + type="rds_instance", + name=db.id, + service="rds", + region=db.region, + account_id=client.audited_account, + properties={k: v for k, v in props.items() if v is not None}, + ) + ) + + for sg in getattr(db, "security_groups", []): + sg_id = sg if isinstance(sg, str) else getattr(sg, "id", str(sg)) + edges.append( + ResourceEdge( + source_id=db.arn, + target_id=sg_id, + edge_type="network", + label="sg", + ) + ) + + vpc_id = getattr(db, "vpc_id", None) + if vpc_id: + edges.append( + ResourceEdge( + source_id=db.arn, + target_id=vpc_id, + edge_type="network", + label="in-vpc", + ) + ) + + cluster_arn = getattr(db, "cluster_arn", None) + if cluster_arn: + edges.append( + ResourceEdge( + source_id=db.arn, + target_id=cluster_arn, + edge_type="depends_on", + label="cluster-member", + ) + ) + + kms_key_id = getattr(db, "kms_key_id", None) + if kms_key_id: + edges.append( + ResourceEdge( + source_id=kms_key_id, + target_id=db.arn, + edge_type="encrypts", + label="kms", + ) + ) + + return nodes, edges diff --git a/contrib/inventory-graph/lib/extractors/s3_extractor.py b/contrib/inventory-graph/lib/extractors/s3_extractor.py new file mode 100644 index 0000000000..7be603c52a --- /dev/null +++ b/contrib/inventory-graph/lib/extractors/s3_extractor.py @@ -0,0 +1,92 @@ +from typing import List, Tuple + +from lib.models import ResourceEdge, ResourceNode + + +def extract(client) -> Tuple[List[ResourceNode], List[ResourceEdge]]: + """ + Extract S3 bucket nodes and their edges. + + Edges produced: + - bucket → replication-target bucket [replicates_to] + - bucket → KMS key [encrypts] + - bucket → logging bucket [logs_to] + """ + nodes: List[ResourceNode] = [] + edges: List[ResourceEdge] = [] + + for bucket in client.buckets.values(): + encryption = getattr(bucket, "encryption", None) + versioning = getattr(bucket, "versioning_enabled", None) + logging = getattr(bucket, "logging", None) + public = getattr(bucket, "public_access_block", None) + + props = {} + if versioning is not None: + props["versioning"] = versioning + if encryption: + enc_type = getattr(encryption, "type", str(encryption)) + props["encryption"] = enc_type + + nodes.append( + ResourceNode( + id=bucket.arn, + type="s3_bucket", + name=bucket.name, + service="s3", + region=bucket.region, + account_id=client.audited_account, + properties=props, + ) + ) + + # Replication edges + for rule in getattr(bucket, "replication_rules", None) or []: + dest_bucket = getattr(rule, "destination_bucket", None) + if dest_bucket: + dest_arn = ( + dest_bucket + if dest_bucket.startswith("arn:") + else f"arn:aws:s3:::{dest_bucket}" + ) + edges.append( + ResourceEdge( + source_id=bucket.arn, + target_id=dest_arn, + edge_type="replicates_to", + label="s3-replication", + ) + ) + + # Logging edges + if logging: + target_bucket = getattr(logging, "target_bucket", None) + if target_bucket: + target_arn = ( + target_bucket + if target_bucket.startswith("arn:") + else f"arn:aws:s3:::{target_bucket}" + ) + edges.append( + ResourceEdge( + source_id=bucket.arn, + target_id=target_arn, + edge_type="logs_to", + label="access-logs", + ) + ) + + # KMS encryption edges + if encryption: + kms_arn = getattr(encryption, "kms_master_key_id", None) + if kms_arn: + edges.append( + ResourceEdge( + source_id=kms_arn, + target_id=bucket.arn, + edge_type="encrypts", + label="kms", + ) + ) + + return nodes, edges diff --git a/contrib/inventory-graph/lib/extractors/vpc_extractor.py b/contrib/inventory-graph/lib/extractors/vpc_extractor.py new file mode 100644 index 0000000000..938554d2ae --- /dev/null +++ b/contrib/inventory-graph/lib/extractors/vpc_extractor.py @@ -0,0 +1,92 @@ +from typing import List, Tuple + +from lib.models import ResourceEdge, ResourceNode + + +def extract(client) -> Tuple[List[ResourceNode], List[ResourceEdge]]: + """ + Extract VPC and subnet nodes with their edges. + + Edges produced: + - subnet → VPC [depends_on] + - peering connection between VPCs [network] + """ + nodes: List[ResourceNode] = [] + edges: List[ResourceEdge] = [] + + # VPCs + for vpc in client.vpcs.values(): + name = vpc.id if hasattr(vpc, "id") else vpc.arn + for tag in vpc.tags or []: + if isinstance(tag, dict) and tag.get("Key") == "Name": + name = tag["Value"] + break + + nodes.append( + ResourceNode( + id=vpc.arn, + type="vpc", + name=name, + service="vpc", + region=vpc.region, + account_id=client.audited_account, + properties={ + "cidr_block": getattr(vpc, "cidr_block", None), + "is_default": getattr(vpc, "is_default", None), + }, + ) + ) + + # VPC Subnets + for subnet in client.vpc_subnets.values(): + name = subnet.id if hasattr(subnet, "id") else subnet.arn + for tag in getattr(subnet, "tags", None) or []: + if isinstance(tag, dict) and tag.get("Key") == "Name": + name = tag["Value"] + break + + nodes.append( + ResourceNode( + id=subnet.arn, + type="subnet", + name=name, + service="vpc", + region=subnet.region, + account_id=client.audited_account, + properties={ + "vpc_id": getattr(subnet, "vpc_id", None), + "cidr_block": getattr(subnet, "cidr_block", None), + "availability_zone": getattr(subnet, "availability_zone", None), + "public": getattr(subnet, "public", None), + }, + ) + ) + + vpc_id = getattr(subnet, "vpc_id", None) + if vpc_id: + # Find the VPC ARN for this vpc_id + vpc_arn = next( + (v.arn for v in client.vpcs.values() if v.id == vpc_id), + vpc_id, + ) + edges.append( + ResourceEdge( + source_id=subnet.arn, + target_id=vpc_arn, + edge_type="depends_on", + label="subnet-of", + ) + ) + + # VPC Peering Connections + for peering in getattr(client, "vpc_peering_connections", {}).values(): + edges.append( + ResourceEdge( + source_id=peering.arn, + target_id=getattr(peering, "accepter_vpc_id", peering.arn), + edge_type="network", + label="vpc-peer", + ) + ) + + return nodes, edges diff --git a/contrib/inventory-graph/lib/graph_builder.py b/contrib/inventory-graph/lib/graph_builder.py new file mode 100644 index 0000000000..073f122a9f --- /dev/null +++ b/contrib/inventory-graph/lib/graph_builder.py @@ -0,0 +1,106 @@ +""" +graph_builder.py +---------------- +Builds a ConnectivityGraph by reading already-loaded AWS service clients from +sys.modules. Only services that were actually scanned (i.e. whose client +module is already imported) contribute nodes and edges. Unknown / unloaded +services are silently skipped, so the output degrades gracefully when only a +subset of checks has been run. +""" + +import sys +from typing import Tuple + +from prowler.lib.logger import logger +from lib.models import ConnectivityGraph + +# Registry: (sys.modules key, attribute name inside that module, extractor module path) +_SERVICE_REGISTRY: Tuple[Tuple[str, str, str], ...] = ( + ( + "prowler.providers.aws.services.awslambda.awslambda_client", + "awslambda_client", + "lib.extractors.lambda_extractor", + ), + ( + "prowler.providers.aws.services.ec2.ec2_client", + "ec2_client", + "lib.extractors.ec2_extractor", + ), + ( + "prowler.providers.aws.services.vpc.vpc_client", + "vpc_client", + "lib.extractors.vpc_extractor", + ), + ( + "prowler.providers.aws.services.rds.rds_client", + "rds_client", + "lib.extractors.rds_extractor", + ), + ( + "prowler.providers.aws.services.elbv2.elbv2_client", + "elbv2_client", + "lib.extractors.elbv2_extractor", + ), + ( + "prowler.providers.aws.services.s3.s3_client", + "s3_client", + "lib.extractors.s3_extractor", + ), + ( + "prowler.providers.aws.services.iam.iam_client", + "iam_client", + "lib.extractors.iam_extractor", + ), +) + + +def build_graph() -> ConnectivityGraph: + """ + Iterate over every registered service, check whether its client module is + already loaded, and call the corresponding extractor. + + Returns a ConnectivityGraph with all discovered nodes and edges. + Duplicate node IDs are silently deduplicated (first occurrence wins). + """ + graph = ConnectivityGraph() + seen_node_ids: set = set() + + for client_module_key, client_attr, extractor_module_key in _SERVICE_REGISTRY: + client_module = sys.modules.get(client_module_key) + if client_module is None: + continue + + service_client = getattr(client_module, client_attr, None) + if service_client is None: + continue + + extractor_module = sys.modules.get(extractor_module_key) + if extractor_module is None: + try: + import importlib + + extractor_module = importlib.import_module(extractor_module_key) + except ImportError as e: + logger.debug( + f"inventory graph_builder: cannot import extractor {extractor_module_key}: {e}" + ) + continue + + try: + nodes, edges = extractor_module.extract(service_client) + except Exception as e: + logger.error( + f"inventory graph_builder: extractor {extractor_module_key} failed: " + f"{e.__class__.__name__}[{e.__traceback__.tb_lineno}]: {e}" + ) + continue + + for node in nodes: + if node.id not in seen_node_ids: + graph.add_node(node) + seen_node_ids.add(node.id) + + for edge in edges: + graph.add_edge(edge) + + return graph diff --git a/contrib/inventory-graph/lib/inventory_output.py b/contrib/inventory-graph/lib/inventory_output.py new file mode 100644 index 0000000000..359e7eb91f --- /dev/null +++ b/contrib/inventory-graph/lib/inventory_output.py @@ -0,0 +1,502 @@ +""" +inventory_output.py +------------------- +Writes the ConnectivityGraph produced by graph_builder to two files: + + .inventory.json – machine-readable graph (nodes + edges) + .inventory.html – interactive D3.js force-directed graph +""" + +import json +import os +from dataclasses import asdict +from datetime import datetime +from typing import Optional + +from prowler.lib.logger import logger +from lib.models import ConnectivityGraph + + +# --------------------------------------------------------------------------- +# JSON output +# --------------------------------------------------------------------------- + + +def write_json(graph: ConnectivityGraph, file_path: str) -> None: + """Serialise the graph to a JSON file.""" + try: + os.makedirs(os.path.dirname(file_path), exist_ok=True) + data = { + "generated_at": datetime.utcnow().isoformat() + "Z", + "nodes": [asdict(n) for n in graph.nodes], + "edges": [asdict(e) for e in graph.edges], + "stats": { + "node_count": len(graph.nodes), + "edge_count": len(graph.edges), + }, + } + with open(file_path, "w", encoding="utf-8") as fh: + json.dump(data, fh, indent=2, default=str) + logger.info(f"Inventory graph JSON written to {file_path}") + except Exception as e: + logger.error( + f"inventory_output.write_json: {e.__class__.__name__}[{e.__traceback__.tb_lineno}]: {e}" + ) + + +# --------------------------------------------------------------------------- +# HTML output (self-contained, D3.js CDN) +# --------------------------------------------------------------------------- + +# Colour palette per node type +_NODE_COLOURS = { + "lambda_function": "#f59e0b", + "ec2_instance": "#3b82f6", + "security_group": "#6366f1", + "vpc": "#10b981", + "subnet": "#34d399", + "rds_instance": "#ef4444", + "load_balancer": "#8b5cf6", + "s3_bucket": "#06b6d4", + "iam_role": "#f97316", + "default": "#94a3b8", +} + +# Edge stroke colours per edge type +_EDGE_COLOURS = { + "network": "#64748b", + "iam": "#f97316", + "triggers": "#a855f7", + "data_flow": "#0ea5e9", + "depends_on": "#94a3b8", + "routes_to": "#22c55e", + "replicates_to": "#ec4899", + "encrypts": "#eab308", + "logs_to": "#78716c", +} + +_HTML_TEMPLATE = """\ + + + + + + Prowler – AWS Connectivity Graph + + + + + +
+ + + + +
+
+ +
+
+

Node types

+ {legend_nodes_html} +

Edge types

+ {legend_edges_html} +
+
+ + + + +""" + + +def _build_legend_html(colours: dict, shape: str) -> str: + rows = [] + for key, colour in sorted(colours.items()): + if shape == "dot": + rows.append( + f'
' + f'
' + f"{key}
" + ) + else: + rows.append( + f'
' + f'
' + f"{key}
" + ) + return "\n".join(rows) + + +def write_html(graph: ConnectivityGraph, file_path: str) -> None: + """Render the graph as a self-contained interactive HTML page.""" + try: + os.makedirs(os.path.dirname(file_path), exist_ok=True) + + nodes_json = json.dumps( + [ + { + "id": n.id, + "type": n.type, + "name": n.name, + "service": n.service, + "region": n.region, + "account_id": n.account_id, + "properties": n.properties, + } + for n in graph.nodes + ], + indent=None, + default=str, + ) + edges_json = json.dumps( + [ + { + "source_id": e.source_id, + "target_id": e.target_id, + "edge_type": e.edge_type, + "label": e.label or "", + } + for e in graph.edges + ], + indent=None, + default=str, + ) + + html = _HTML_TEMPLATE.format( + generated_at=datetime.utcnow().strftime("%Y-%m-%d %H:%M UTC"), + nodes_json=nodes_json, + edges_json=edges_json, + node_colours_json=json.dumps(_NODE_COLOURS), + edge_colours_json=json.dumps(_EDGE_COLOURS), + legend_nodes_html=_build_legend_html(_NODE_COLOURS, "dot"), + legend_edges_html=_build_legend_html(_EDGE_COLOURS, "line"), + ) + + with open(file_path, "w", encoding="utf-8") as fh: + fh.write(html) + + logger.info(f"Inventory graph HTML written to {file_path}") + except Exception as e: + logger.error( + f"inventory_output.write_html: {e.__class__.__name__}[{e.__traceback__.tb_lineno}]: {e}" + ) + + +# --------------------------------------------------------------------------- +# Convenience entry-point called from __main__.py +# --------------------------------------------------------------------------- + + +def generate_inventory_outputs(output_path: str) -> None: + """ + Build the connectivity graph from currently-loaded service clients and write + both JSON and HTML outputs. + + Args: + output_path: base file path WITHOUT extension, e.g. + "output/prowler-output-20240101120000". + The function appends .inventory.json and .inventory.html. + """ + from lib.graph_builder import build_graph + + graph = build_graph() + + if not graph.nodes: + logger.warning( + "Inventory graph: no nodes discovered. " + "Make sure at least one AWS service was scanned before generating the inventory." + ) + + write_json(graph, f"{output_path}.inventory.json") + write_html(graph, f"{output_path}.inventory.html") diff --git a/contrib/inventory-graph/lib/models.py b/contrib/inventory-graph/lib/models.py new file mode 100644 index 0000000000..bb2c9a3ce7 --- /dev/null +++ b/contrib/inventory-graph/lib/models.py @@ -0,0 +1,71 @@ +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional + + +@dataclass +class ResourceNode: + """ + Represents a single AWS resource as a node in the connectivity graph. + + id : globally unique identifier — always the resource ARN + type : coarse resource type used for grouping/colour, e.g. "lambda_function" + name : human-readable label shown on the graph + service : AWS service name, e.g. "lambda", "ec2", "rds" + region : AWS region the resource lives in + account_id: AWS account ID + properties: additional resource-specific metadata (runtime, vpc_id, etc.) + """ + + id: str + type: str + name: str + service: str + region: str + account_id: str + properties: Dict[str, Any] = field(default_factory=dict) + + +@dataclass +class ResourceEdge: + """ + Represents a directional relationship between two resource nodes. + + source_id : ARN of the source node + target_id : ARN of the target node + edge_type : semantic type of the relationship, e.g.: + "network" – resources share a network path (VPC/subnet/SG) + "iam" – IAM trust or permission relationship + "triggers" – one resource can invoke another (event source → Lambda) + "data_flow" – data is written/read (Lambda → SQS dead-letter queue) + "depends_on" – soft dependency (Lambda layer, subnet belongs to VPC) + "routes_to" – traffic routing (LB → target) + "encrypts" – KMS key encrypts the resource + label : optional short label rendered on the edge in the HTML graph + """ + + source_id: str + target_id: str + edge_type: str + label: Optional[str] = None + + +@dataclass +class ConnectivityGraph: + """ + Container for the full inventory connectivity graph. + + nodes: all discovered resource nodes + edges: all discovered edges between nodes + """ + + nodes: List[ResourceNode] = field(default_factory=list) + edges: List[ResourceEdge] = field(default_factory=list) + + def add_node(self, node: ResourceNode) -> None: + self.nodes.append(node) + + def add_edge(self, edge: ResourceEdge) -> None: + self.edges.append(edge) + + def node_ids(self) -> set: + return {n.id for n in self.nodes} diff --git a/contrib/k8s/helm/prowler-api/values.yaml b/contrib/k8s/helm/prowler-api/values.yaml index 61146e1e1f..81eda5f690 100644 --- a/contrib/k8s/helm/prowler-api/values.yaml +++ b/contrib/k8s/helm/prowler-api/values.yaml @@ -73,7 +73,7 @@ secrets: DJANGO_SECRETS_ENCRYPTION_KEY: DJANGO_BROKER_VISIBILITY_TIMEOUT: 86400 -releaseConfigRoot: /home/prowler/.cache/pypoetry/virtualenvs/prowler-api-NnJNioq7-py3.12/lib/python3.12/site-packages/ +releaseConfigRoot: /home/prowler/.venv/lib/python3.12/site-packages/ releaseConfigPath: prowler/config/config.yaml mainConfig: @@ -590,13 +590,16 @@ resources: {} # memory: 128Mi # This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ +# /health/live succeeds while the process answers; /health/ready also +# checks PostgreSQL, Valkey and Neo4j connectivity and returns 503 when +# any of them is unreachable. livenessProbe: httpGet: - path: / + path: /health/live port: http readinessProbe: httpGet: - path: / + path: /health/ready port: http #This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/ diff --git a/contrib/k8s/helm/prowler-app/values.yaml b/contrib/k8s/helm/prowler-app/values.yaml index 9a162bd67e..9acca09f2a 100644 --- a/contrib/k8s/helm/prowler-app/values.yaml +++ b/contrib/k8s/helm/prowler-app/values.yaml @@ -270,20 +270,23 @@ api: # 3m30s to setup DB # startupProbe: # httpGet: - # path: /api/v1/docs + # path: /health/live # port: http # This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ + # /health/live succeeds while the process answers; /health/ready also + # checks PostgreSQL, Valkey and Neo4j connectivity and returns 503 when + # any of them is unreachable. livenessProbe: failureThreshold: 10 httpGet: - path: /api/v1/docs + path: /health/live port: http periodSeconds: 20 readinessProbe: failureThreshold: 10 httpGet: - path: /api/v1/docs + path: /health/ready port: http periodSeconds: 20 diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index b95945e7cb..2020c7d21a 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -37,7 +37,7 @@ services: neo4j: condition: service_healthy healthcheck: - test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:${DJANGO_PORT:-8080}/api/v1/ || exit 1"] + test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:${DJANGO_PORT:-8080}/health/live || exit 1"] interval: 10s timeout: 5s retries: 12 @@ -211,6 +211,7 @@ services: interval: 10s timeout: 5s retries: 3 + start_period: 60s volumes: outputs: diff --git a/docker-compose.yml b/docker-compose.yml index 5a4514546a..a9d2e03c3d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -33,7 +33,7 @@ services: neo4j: condition: service_healthy healthcheck: - test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:${DJANGO_PORT:-8080}/api/v1/ || exit 1"] + test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:${DJANGO_PORT:-8080}/health/live || exit 1"] interval: 10s timeout: 5s retries: 12 @@ -48,10 +48,16 @@ services: - path: .env required: false ports: - - ${UI_PORT:-3000}:${UI_PORT:-3000} + - ${UI_PORT:-3000}:3000 depends_on: mcp-server: condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:3000/api/health || exit 1"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 60s postgres: image: postgres:16.3-alpine3.20 @@ -170,6 +176,7 @@ services: interval: 10s timeout: 5s retries: 3 + start_period: 60s volumes: output: diff --git a/docs/AGENTS.md b/docs/AGENTS.md index 2f9efd405d..8278a7f88a 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -134,7 +134,7 @@ Example 1 is vague and even potentially ambiguous. Verbs state your purpose and Explicit use of second-person pronouns (you) and possessives (your) should be minimized whenever possible. Those constructions are best reserved for cases when instructions are directly given in an imperative form: -**Example of Improvement Through Avoiding Second Person Pronouns** +### Example of Improvement Through Avoiding Second Person Pronouns **Original:** Prowler App can be installed in different ways, depending on your environment: @@ -236,7 +236,7 @@ The use of bullet points is highly recommended when: * Information can be logically divided into multiple categories, each sharing characteristics, features, or other relevant classifications. * Items are significant enough as standalone concepts to deserve their own bullet point. -**Example of Improvement Through Bullet Points** +#### Example of Improvement Through Bullet Points **Original:** It contains hundreds of controls covering CIS, NIST 800, NIST CSF, CISA, RBI, FedRAMS, PCI-DSS, GDPR, HIPAA, FFIEC, SOC2, GXP, AWS Well-Architected Framework Security Pillar, AWS Foundational Technical Review (FTR), ENS (Spanish National Security Scheme), and your custom security frameworks. @@ -467,7 +467,7 @@ Effective headers and section titles enhance document readability and structure, * **Example:** * How to Clone and Install Prowler from GitHub (header: Title case) - * How to install poetry dependencies (subheading: Sentence case) + * How to install uv dependencies (subheading: Sentence case) 5. **Using Keywords in Headers** Headers should include relevant keywords to improve document searchability: * **Good:** Scanning AWS Accounts in Parallel diff --git a/docs/README.md b/docs/README.md index 79917aeed1..595f79bc5a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,10 +10,10 @@ This repository contains the Prowler Open Source documentation powered by [Mintl ## Local Development -Install the [Mintlify CLI](https://www.npmjs.com/package/mint) to preview documentation changes locally: +Install a reviewed version of the [Mintlify CLI](https://www.npmjs.com/package/mint) to preview documentation changes locally: ```bash -npm i -g mint +npm install --global mint@4.2.560 ``` Run the following command at the root of your documentation (where `mint.json` is located): diff --git a/docs/developer-guide/aws-details.mdx b/docs/developer-guide/aws-details.mdx index 70f94f4006..e6f561ea3d 100644 --- a/docs/developer-guide/aws-details.mdx +++ b/docs/developer-guide/aws-details.mdx @@ -73,6 +73,58 @@ The best reference to understand how to implement a new service is following the - AWS API calls are wrapped in try/except blocks, with specific handling for `ClientError` and generic exceptions, always logging errors. - If ARN is not present for some resource, it can be constructed using string interpolation, always including partition, service, region, account, and resource ID. - Tags and additional attributes that cannot be retrieved from the default call, should be collected and stored for each resource using dedicated methods and threading using the resource object list as iterator. +- When accessing dictionary values from AWS API responses, always use `.get()` with a default value instead of direct dictionary access (e.g., `response.get("Policies", {})` instead of `response["Policies"]`). AWS API responses may not always include all keys, and direct access can cause `KeyError` exceptions that break the entire scan for that service. + +### Extending an Existing Service with New Attributes + +When adding a new check that requires data not yet collected by an existing service, you need to extend the service by adding new attributes to its resource models and updating the data collection methods. This is a common contributor task that follows a consistent pattern: + +1. **Identify the missing data**: Determine which AWS API call provides the data you need and whether it's already being called by the service. + +2. **Add new attributes to the resource model**: Extend the Pydantic `BaseModel` class for the resource with the new fields. Use `Optional` types with `None` as the default value to maintain backward compatibility with existing checks. + +3. **Update the data collection method**: Modify the existing method that fetches resource details to also extract and store the new attributes. If no existing method fetches the data, add a new method and call it in the constructor using `self.__threading_call__` if possible. + +4. **Use safe dictionary access**: When extracting values from API responses, always use `.get()` with appropriate defaults to prevent `KeyError` exceptions when the API doesn't return certain fields. + +#### Example: Adding DKIM Status to SES Identities + +```python +# Step 1 & 2: Add new fields to the resource model +class Identity(BaseModel): + name: str + arn: str + region: str + type: Optional[str] + policy: Optional[dict] = None + tags: Optional[list] = [] + # New attributes for DKIM check + dkim_status: Optional[str] = None + dkim_signing_attributes_origin: Optional[str] = None + +# Step 3: Update the data collection method +def _get_email_identities(self, identity): + try: + regional_client = self.regional_clients[identity.region] + identity_attributes = regional_client.get_email_identity( + EmailIdentity=identity.name + ) + # Step 4: Use .get() for safe dictionary access + for content_key, content_value in identity_attributes.get("Policies", {}).items(): + identity.policy = loads(content) + identity.tags = identity_attributes.get("Tags", []) + # Extract new DKIM attributes + identity.dkim_status = identity_attributes.get("DkimStatus") + identity.dkim_signing_attributes_origin = ( + identity_attributes.get("DkimSigningAttributesOrigin") + ) + except Exception as error: + logger.error( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) +``` + +5. **Update the service tests**: Add the new attributes to the test mock data and assertions to verify correct data extraction. ## Specific Patterns in AWS Checks diff --git a/docs/developer-guide/checks.mdx b/docs/developer-guide/checks.mdx index c5dacc72d9..2f0f45cd96 100644 --- a/docs/developer-guide/checks.mdx +++ b/docs/developer-guide/checks.mdx @@ -20,8 +20,8 @@ The most common high level steps to create a new check are: 3. Create a check-specific folder. The path should follow this pattern: `prowler/providers//services//`. Adhere to the [Naming Format for Checks](#naming-format-for-checks). 4. Populate the folder with files as specified in [File Creation](#file-creation). 5. Run the check locally to ensure it works as expected. For checking you can use the CLI in the next way: - - To ensure the check has been detected by Prowler: `poetry run python prowler-cli.py --list-checks | grep `. - - To run the check, to find possible issues: `poetry run python prowler-cli.py --log-level ERROR --verbose --check `. + - To ensure the check has been detected by Prowler: `uv run python prowler-cli.py --list-checks | grep `. + - To run the check, to find possible issues: `uv run python prowler-cli.py --log-level ERROR --verbose --check `. 6. Create comprehensive tests for the check that cover multiple scenarios including both PASS (compliant) and FAIL (non-compliant) cases. For detailed information about test structure and implementation guidelines, refer to the [Testing](/developer-guide/unit-testing) documentation. 7. If the check and its corresponding tests are working as expected, you can submit a PR to Prowler. diff --git a/docs/developer-guide/documentation.mdx b/docs/developer-guide/documentation.mdx index f1fae30d35..d0fb808af2 100644 --- a/docs/developer-guide/documentation.mdx +++ b/docs/developer-guide/documentation.mdx @@ -28,7 +28,7 @@ This includes the [AGENTS.md](https://github.com/prowler-cloud/prowler/blob/mast ```bash - npm i -g mint + npm install --global mint@4.2.560 ``` For detailed instructions, check the [Mintlify documentation](https://www.mintlify.com/docs/installation). diff --git a/docs/developer-guide/introduction.mdx b/docs/developer-guide/introduction.mdx index 03b3bd27bd..117640c669 100644 --- a/docs/developer-guide/introduction.mdx +++ b/docs/developer-guide/introduction.mdx @@ -80,7 +80,7 @@ Before proceeding, ensure the following: - Git is installed. - Python 3.10 or higher is installed. -- `poetry` is installed to manage dependencies. +- `uv` is installed to manage dependencies. ### Forking the Prowler Repository @@ -97,28 +97,21 @@ cd prowler ### Dependency Management and Environment Isolation -To prevent conflicts between environments, we recommend using `poetry`, a Python dependency management solution. Install it by following the [instructions](https://python-poetry.org/docs/#installation). +To prevent conflicts between environments, we recommend using [`uv`](https://docs.astral.sh/uv/), a fast Python package and project manager. Install it by following the [official instructions](https://docs.astral.sh/uv/getting-started/installation/). ### Installing Dependencies To install all required dependencies, including those needed for development, run: ``` -poetry install --with dev -eval $(poetry env activate) +uv sync +source .venv/bin/activate ``` - -Starting from Poetry v2.0.0, `poetry shell` has been deprecated in favor of `poetry env activate`. -If your poetry version is below 2.0.0 you must keep using `poetry shell` to activate your environment. -In case you have any doubts, consult the [Poetry environment activation guide](https://python-poetry.org/docs/managing-environments/#activating-the-environment). - - - ### Pre-Commit Hooks -This repository uses Git pre-commit hooks managed by the [prek](https://prek.j178.dev/) tool, it is installed with `poetry install --with dev`. Next, run the following command in the root of this repository: +This repository uses Git pre-commit hooks managed by the [prek](https://prek.j178.dev/) tool, it is installed with `uv sync`. Next, run the following command in the root of this repository: ```shell prek install @@ -134,16 +127,32 @@ prek installed at `.git/hooks/pre-commit` If pre-commit hooks were previously installed, run `prek install --overwrite` to replace the existing hook. Otherwise, both tools will run on each commit. +#### Enable TruffleHog as a Pre-Push Hook + +By default, only `pre-commit` hooks are installed. To enable [`TruffleHog`](https://github.com/trufflesecurity/trufflehog) secret scanning on every push, install the `pre-push` hook type explicitly: + +```shell +prek install --hook-type pre-push +``` + +Successful installation should produce the following output: + +```shell +prek installed at `.git/hooks/pre-push` +``` + +Once installed, TruffleHog runs before each push and blocks the operation when verified secrets are detected. + ### Code Quality and Security Checks Before merging pull requests, several automated checks and utilities ensure code security and updated dependencies: -These should have been already installed if `poetry install --with dev` was already run. +These should have been already installed if `uv sync` was already run. - [`bandit`](https://pypi.org/project/bandit/) for code security review. -- [`safety`](https://pypi.org/project/safety/) and [`dependabot`](https://github.com/features/security) for dependencies. +- [`osv-scanner`](https://github.com/google/osv-scanner) and [`dependabot`](https://github.com/features/security) for dependencies. - [`hadolint`](https://github.com/hadolint/hadolint) and [`dockle`](https://github.com/goodwithtech/dockle) for container security. - [`Snyk`](https://docs.snyk.io/integrations/snyk-container-integrations/container-security-with-docker-hub-integration) for container security in Docker Hub. - [`clair`](https://github.com/quay/clair) for container security in Amazon ECR. @@ -167,7 +176,7 @@ These resources help ensure that AI-assisted contributions maintain consistency All dependencies are listed in the `pyproject.toml` file. -The SDK keeps direct dependencies pinned to exact versions, while `poetry.lock` records the full resolved dependency tree and the artifact hashes for every package. Use `poetry install` from the lock file instead of ad-hoc `pip` installs when you need a reproducible environment. +The SDK keeps direct dependencies pinned to exact versions, while `uv.lock` records the full resolved dependency tree and the artifact hashes for every package. Use `uv sync` from the lock file instead of ad-hoc `pip` installs when you need a reproducible environment. For proper code documentation, refer to the following and follow the code documentation practices presented there: [Google Python Style Guide - Comments and Docstrings](https://github.com/google/styleguide/blob/gh-pages/pyguide.md#38-comments-and-docstrings). @@ -193,8 +202,8 @@ prowler/ ├── contrib/ # Community-contributed scripts or modules ├── kubernetes/ # Kubernetes deployment files ├── .github/ # GitHub-related files (workflows, issue templates, etc.) -├── pyproject.toml # Python project configuration (Poetry) -├── poetry.lock # Poetry lock file +├── pyproject.toml # Python project configuration (uv) +├── uv.lock # uv lock file ├── README.md # Project overview and getting started ├── Makefile # Common development commands ├── Dockerfile # SDK Docker container diff --git a/docs/developer-guide/provider.mdx b/docs/developer-guide/provider.mdx index ec3e106150..d3e7d3631f 100644 --- a/docs/developer-guide/provider.mdx +++ b/docs/developer-guide/provider.mdx @@ -1003,7 +1003,7 @@ class ProwlerArgumentParser: formatter_class=RawTextHelpFormatter, usage="prowler [-h] [--version] {aws,azure,gcp,kubernetes,m365,github,nhn,dashboard,iac,your_provider} ...", epilog=""" -Available Cloud Providers: +Available Providers: {aws,azure,gcp,kubernetes,m365,github,iac,nhn,your_provider} aws AWS Provider azure Azure Provider @@ -1277,10 +1277,12 @@ Dependencies ensure that your provider's required libraries are available when P **File:** `pyproject.toml` ```toml -[tool.poetry.dependencies] -python = ">=3.10,<3.13" -# ... other dependencies -your-sdk-library = "^1.0.0" # Add your SDK dependency +[project] +requires-python = ">=3.10,<3.13" +dependencies = [ + # ... other dependencies + "your-sdk-library>=1.0.0,<2.0.0", # Add your SDK dependency +] ``` #### Step 18: Create Tests diff --git a/docs/developer-guide/security-compliance-framework.mdx b/docs/developer-guide/security-compliance-framework.mdx index bb01af8271..431849689f 100644 --- a/docs/developer-guide/security-compliance-framework.mdx +++ b/docs/developer-guide/security-compliance-framework.mdx @@ -2,47 +2,378 @@ title: 'Creating a New Security Compliance Framework in Prowler' --- +This guide explains how to add a new security compliance framework to Prowler, end to end. It covers directory layout, the JSON schema, check mapping conventions, the Pydantic models that validate each framework, the CSV output formatter, local validation, testing, and the pull request process. + ## Introduction -To create or contribute a custom security framework for Prowler—or to integrate a public framework—you must ensure the necessary checks are available. If they are missing, they must be implemented before proceeding. +A compliance framework in Prowler maps a public or custom control catalog (for example CIS, NIST 800-53, PCI DSS, HIPAA, ENS, CCC) to the security checks that Prowler already runs. Each requirement links to zero, one or more Prowler checks. When a scan executes, findings are aggregated per requirement to produce the compliance report rendered by Prowler CLI and Prowler Cloud. -Each framework is defined in a compliance file per provider. The file should follow the structure used in `prowler/compliance//` and be named `__.json`. Follow the format below to create your own. +Prowler ships with 85+ compliance frameworks across All Providers. The catalog lives under `prowler/compliance//` (or `prowler/compliance/` for universal compliance frameworks) -## Compliance Framework + +A compliance framework must represent the **complete state** of the source catalog. Every requirement defined by the framework has to be present in the JSON file, even when none of the existing Prowler checks can automate it. In that case, leave `Checks` as an empty array, but do not omit the requirement. -### Compliance Framework Structure +Requirement coverage feeds the compliance percentage calculations and the metadata surfaces (dashboards, widgets, exports). Missing requirements skew those metrics and break the report as a faithful snapshot of the framework. + -Each compliance framework file consists of structured metadata that identifies the framework and maps security checks to requirements or controls. Please note that a single requirement can be linked to multiple Prowler checks: +### Prerequisites -- `Framework`: string – The distinguished name of the framework (e.g., CIS). -- `Provider`: string – The cloud provider where the framework applies (AWS, Azure, OCI). -- `Version`: string – The framework version (e.g., 1.4 for CIS). -- `Requirements`: array of objects. – Defines security requirements and their mapping to Prowler checks. All requirements or controls are to be included with the mapping to Prowler. -- `Requirements_Id`: string – A unique identifier for each requirement within the framework -- `Requirements_Description`: string – The requirement description as specified in the framework. -- `Requirements_Attributes`: array of objects. – Contains relevant metadata such as security levels, sections, and any additional data needed for reporting with the result of the findings. Attributes should be derived directly from the framework’s own terminology, ensuring consistency with its established definitions. -- `Requirements_Checks`: array. The Prowler checks that are needed to prove this requirement. It can be one or multiple checks. In case automation is not feasible, this can be empty. +Before adding a new framework, complete the following checks: + +- **Verify the framework is not already supported.** Inspect `prowler/compliance//` for an existing JSON file matching the name and version. +- **Confirm the required checks exist.** Every requirement that can be automated must point to one or more existing Prowler checks. For each missing check, implement it first by following the [Prowler Checks](/developer-guide/checks) guide. +- **Review a reference framework.** Use an existing framework with a similar structure as your template. `cis_2.0_aws.json` is the canonical reference for CIS-style frameworks. `ccc_aws.json`, `ens_rd2022_aws.json`, and `nist_800_53_revision_5_aws.json` illustrate other attribute shapes. + +## Four-Layer Architecture + +A compliance framework spans four layers. A complete contribution must touch each layer that applies. + +- **Layer 1 – Schema validation:** The Pydantic models in `prowler/lib/check/compliance_models.py` define the canonical schema for each attribute shape (CIS, ENS, Mitre, CCC, C5, CSA CCM, ISO 27001, KISA ISMS-P, AWS Well-Architected, Prowler ThreatScore, and a generic fallback). +- **Layer 2 – JSON catalog:** The framework JSON file in `prowler/compliance//` lists every requirement and maps it to checks. +- **Layer 3 – Output formatter:** The Python module in `prowler/lib/outputs/compliance//` builds the CSV row model, the per-provider transformer, and the CLI summary table. +- **Layer 4 – Output dispatchers:** The dispatchers in `prowler/lib/outputs/compliance/compliance.py` and `prowler/lib/outputs/compliance/compliance_output.py` route findings to the right formatter based on the framework identifier. + +The rest of this guide walks each layer in order. + +## Directory Structure and File Naming + +Compliance frameworks live at: ``` +prowler/compliance//__.json +``` + +The filename conventions are: + +- All lowercase, words separated with underscores. +- `` is a supported provider identifier: `aws`, `azure`, `gcp`, `kubernetes`, `m365`, `github`, `googleworkspace`, `alibabacloud`, `oraclecloud`, `cloudflare`, `mongodbatlas`, `nhn`, `openstack`, `iac`, `llm`. +- `` is optional. Omit it when the framework has no versioning, as in `ccc_aws.json`. +- The file basename (without `.json`) is the framework key that Prowler CLI accepts via `--compliance`. + +Examples: + +- `prowler/compliance/aws/cis_2.0_aws.json` +- `prowler/compliance/aws/nist_800_53_revision_5_aws.json` +- `prowler/compliance/azure/ens_rd2022_azure.json` +- `prowler/compliance/kubernetes/cis_1.10_kubernetes.json` +- `prowler/compliance/aws/ccc_aws.json` + +The output formatter directory mirrors the framework name: + +``` +prowler/lib/outputs/compliance// +├── .py # CLI summary-table dispatcher +├── _.py # Per-provider transformer class +├── models.py # Pydantic CSV row model +└── __init__.py +``` + +## JSON Schema Reference + +Every compliance file is a JSON document with the following top-level keys. + +| Field | Type | Required | Description | +|---|---|---|---| +| `Framework` | string | Yes | Canonical framework identifier, for example `CIS`, `NIST-800-53-Revision-5`, `ENS`, `CCC`. | +| `Name` | string | Yes | Human-readable framework name displayed by Prowler App. | +| `Version` | string | Yes | Framework version, for example `2.0`. Use an empty string only for frameworks without versioning. See [Version Handling](#version-handling). | +| `Provider` | string | Yes | Upper-cased provider identifier: `AWS`, `AZURE`, `GCP`, `KUBERNETES`, `M365`, `GITHUB`, `GOOGLEWORKSPACE`, and so on. | +| `Description` | string | Yes | Short description of the framework's scope and purpose. | +| `Requirements` | array | Yes | List of [requirement objects](#requirement-object). | + +### Requirement Object + +Each entry in `Requirements` describes one control or requirement. + +| Field | Type | Required | Description | +|---|---|---|---| +| `Id` | string | Yes | Unique identifier within the framework, for example `1.10` or `CCC.Core.CN01.AR01`. | +| `Name` | string | No | Optional human-readable name used by frameworks that distinguish control name from description, such as NIST. | +| `Description` | string | Yes | Verbatim description from the source framework. | +| `Attributes` | array | Yes | List of [attribute objects](#attribute-objects). The shape depends on the framework. | +| `Checks` | array of strings | Yes | Prowler check identifiers that automate the requirement. Leave the list empty when the control cannot be automated. | + +### Attribute Objects + +Attributes carry the metadata that Prowler App and the CSV output display for each requirement. The object shape is framework-specific and is validated by a dedicated Pydantic model in `prowler/lib/check/compliance_models.py`. The most common shapes are summarized below. + +#### CIS_Requirement_Attribute + +Used by every CIS benchmark. + +| Field | Type | Required | Notes | +|---|---|---|---| +| `Section` | string | Yes | Top-level section, for example `1 Identity and Access Management`. | +| `SubSection` | string | No | Optional second-level grouping. | +| `Profile` | enum | Yes | One of `Level 1`, `Level 2`, `E3 Level 1`, `E3 Level 2`, `E5 Level 1`, `E5 Level 2`. | +| `AssessmentStatus` | enum | Yes | `Manual` or `Automated`. | +| `Description` | string | Yes | Control description. | +| `RationaleStatement` | string | Yes | Reason the control exists. | +| `ImpactStatement` | string | Yes | Impact of non-compliance. | +| `RemediationProcedure` | string | Yes | Remediation steps. | +| `AuditProcedure` | string | Yes | Audit steps. | +| `AdditionalInformation` | string | Yes | Free-form notes. | +| `DefaultValue` | string | No | Default configuration value, when relevant. | +| `References` | string | Yes | Colon-separated list of reference URLs. | + +#### ENS_Requirement_Attribute + +Used by the Spanish ENS (Esquema Nacional de Seguridad) frameworks. + +| Field | Type | Required | Notes | +|---|---|---|---| +| `IdGrupoControl` | string | Yes | Control group identifier. | +| `Marco` | string | Yes | Framework block (`operacional`, `organizativo`, `proteccion`). | +| `Categoria` | string | Yes | Control category. | +| `DescripcionControl` | string | Yes | Control description in Spanish. | +| `Tipo` | enum | Yes | `refuerzo`, `requisito`, `recomendacion`, `medida`. | +| `Nivel` | enum | Yes | `opcional`, `bajo`, `medio`, `alto`. | +| `Dimensiones` | array of enum | Yes | Subset of `confidencialidad`, `integridad`, `trazabilidad`, `autenticidad`, `disponibilidad`. | +| `ModoEjecucion` | string | Yes | Execution mode (`manual`, `automático`, `híbrido`). | +| `Dependencias` | array of strings | Yes | Ids of prerequisite controls. Empty list when none. | + +#### CCC_Requirement_Attribute + +Used by the Common Cloud Controls Catalog. + +| Field | Type | Required | Notes | +|---|---|---|---| +| `FamilyName` | string | Yes | Control family, for example `Data`. | +| `FamilyDescription` | string | Yes | Description of the family. | +| `Section` | string | Yes | Section title. | +| `SubSection` | string | Yes | Subsection title, or empty string. | +| `SubSectionObjective` | string | Yes | Stated objective for the subsection. | +| `Applicability` | array of strings | Yes | Applicability tags such as `tlp-green`, `tlp-amber`, `tlp-red`. | +| `Recommendation` | string | Yes | Implementation recommendation. | +| `SectionThreatMappings` | array of objects | Yes | Each entry has `ReferenceId` and `Identifiers`. | +| `SectionGuidelineMappings` | array of objects | Yes | Each entry has `ReferenceId` and `Identifiers`. | + +#### Generic_Compliance_Requirement_Attribute + +The fallback attribute model used when no framework-specific schema applies (for example NIST 800-53, PCI DSS, GDPR, HIPAA). + +| Field | Type | Required | Notes | +|---|---|---|---| +| `ItemId` | string | No | Item identifier. | +| `Section` | string | No | Section name. | +| `SubSection` | string | No | Subsection name. | +| `SubGroup` | string | No | Subgroup name. | +| `Service` | string | No | Affected service, for example `aws`, `iam`. | +| `Type` | string | No | Control type. | +| `Comment` | string | No | Free-form comment. | + +Additional per-framework attribute models exist for `AWS_Well_Architected_Requirement_Attribute`, `ISO27001_2013_Requirement_Attribute`, `Mitre_Requirement_Attribute_`, `KISA_ISMSP_Requirement_Attribute`, `Prowler_ThreatScore_Requirement_Attribute`, `C5Germany_Requirement_Attribute`, and `CSA_CCM_Requirement_Attribute`. Consult `prowler/lib/check/compliance_models.py` for their full field sets. + + +The `Attributes` field is a Pydantic `Union`. The generic attribute model must remain the last element of that Union, otherwise Pydantic v1 silently coerces every framework into the generic shape and your specialized fields are dropped. + + +## Minimal Working Example + +The following snippet is a complete, valid framework file named `my_framework_1.0_aws.json`, saved at `prowler/compliance/aws/my_framework_1.0_aws.json`. It uses the generic attribute shape for simplicity. + +```json title="prowler/compliance/aws/my_framework_1.0_aws.json" { - "Framework": "-", - "Version": "", + "Framework": "My-Framework", + "Name": "My Framework 1.0 for AWS", + "Version": "1.0", + "Provider": "AWS", + "Description": "Internal baseline for AWS accounts.", "Requirements": [ { - "Id": "", - "Description": "Full description of the requirement", - "Checks": [ - "Here is the prowler check or checks that will be executed" - ], + "Id": "MF-1.1", + "Description": "Root account must have multi-factor authentication enabled.", "Attributes": [ { - + "ItemId": "MF-1.1", + "Section": "Identity and Access Management", + "SubSection": "Root Account", + "Service": "iam" } + ], + "Checks": [ + "iam_root_mfa_enabled", + "iam_root_hardware_mfa_enabled" ] }, - ... + { + "Id": "MF-2.1", + "Description": "S3 buckets must block public access at the account level.", + "Attributes": [ + { + "ItemId": "MF-2.1", + "Section": "Data Protection", + "Service": "s3" + } + ], + "Checks": [ + "s3_account_level_public_access_blocks" + ] + } ] } ``` -Finally, to have a proper output file for your reports, your framework data model has to be created in `prowler/lib/outputs/models.py` and also the CLI table output in `prowler/lib/outputs/compliance.py`. Also, you need to add a new conditional in `prowler/lib/outputs/file_descriptors.py` if creating a new CSV model. +## Mapping Checks to Requirements + +Each requirement links to the Prowler checks that, together, produce a PASS or FAIL verdict for that control. + +- **Include every requirement from the source catalog.** The framework file must mirror the full control list, one-to-one. Compliance percentages, dashboards, and exported metadata are computed against the total requirement count, so omitting an unmappable control inflates coverage and misrepresents the framework. +- List every check by its canonical identifier, the value of `CheckID` inside the check's `.metadata.json` file. +- One requirement can reference multiple checks. The requirement is evaluated as FAIL when any referenced check produces a FAIL finding for a resource in scope. +- Leave `Checks` as an empty array when the requirement cannot be automated. The requirement still appears in the report, contributes to the total, and resolves to `MANUAL`. An empty mapping is valid; a missing requirement is not. +- Reuse checks across requirements when the same control applies in multiple places. Do not duplicate check logic to match framework structure. +- Avoid referencing checks from a different provider. A compliance file is bound to one provider, and cross-provider checks will never match findings in the scan. + +To discover available checks, run: + +```bash +uv run python prowler-cli.py --list-checks +``` + +## Supporting Multiple Providers + +Each compliance file targets a single provider. To cover several providers with the same framework (for example CIS across AWS, Azure, and GCP), ship one JSON file per provider: + +``` +prowler/compliance/aws/cis_2.0_aws.json +prowler/compliance/azure/cis_2.0_azure.json +prowler/compliance/gcp/cis_2.0_gcp.json +``` + +Keep the `Framework` and `Version` values identical across the files so the dispatcher matches them, and change only the `Provider`, `Checks`, and provider-specific metadata. + +The CIS output formatter already supports every provider listed above. For a brand-new framework that spans several providers, add one transformer per provider in `prowler/lib/outputs/compliance//` and extend the summary-table dispatcher accordingly. See [Output Formatter](#output-formatter). + +## Output Formatter + +Prowler renders every compliance framework in two forms: a detailed CSV report written to disk, and a summary table printed in the CLI. Both are produced by the output formatter package for the framework. + +For a new framework named `my_framework`, create: + +``` +prowler/lib/outputs/compliance/my_framework/ +├── __init__.py +├── my_framework.py # CLI summary table dispatcher +├── my_framework_aws.py # Per-provider transformer +└── models.py # CSV row Pydantic model +``` + +### Step 1 – Define the CSV Row Model + +In `models.py`, declare a Pydantic v1 model with one field per CSV column. Use existing models such as `AWSCISModel` in `prowler/lib/outputs/compliance/cis/models.py` as the reference. Fields typically include `Provider`, `Description`, `AccountId`, `Region`, `AssessmentDate`, `Requirements_Id`, `Requirements_Description`, one `Requirements_Attributes_*` field per attribute key, plus the finding fields `Status`, `StatusExtended`, `ResourceId`, `ResourceName`, `CheckId`, `Muted`, `Framework`, `Name`. + +### Step 2 – Implement the Transformer Class + +In `my_framework_aws.py`, subclass `ComplianceOutput` from `prowler.lib.outputs.compliance.compliance_output` and implement `transform(findings, compliance, compliance_name)`. Iterate over `findings`, match each finding to the requirements it satisfies through `finding.compliance.get(compliance_name, [])`, and append one row per attribute to `self._data`. + +### Step 3 – Add the Summary-Table Dispatcher + +In `my_framework.py`, implement `get_my_framework_table(findings, bulk_checks_metadata, compliance_framework, output_filename, output_directory, compliance_overview)` following the pattern in `prowler/lib/outputs/compliance/cis/cis.py`. + +### Step 4 – Register the Framework in the Dispatchers + +- Add the dispatcher call in `prowler/lib/outputs/compliance/compliance.py`, inside `display_compliance_table`, with a branch such as `elif "my_framework" in compliance_framework:`. +- Register the CSV model and transformer in `prowler/lib/outputs/compliance/compliance_output.py` so the CSV file is emitted during the scan. + + +For NIST-style catalogs that use `Generic_Compliance_Requirement_Attribute`, no custom formatter is needed. The generic formatter in `prowler/lib/outputs/compliance/generic/` handles them automatically, provided the JSON validates against the generic attribute schema. + + +## Version Handling + +Prowler matches frameworks by concatenating `Framework` and `Version`. A missing or empty `Version` collapses several frameworks to the same key and breaks CLI filtering with `--compliance`. + +- Always set `Version` to a non-empty string, even for frameworks that rename editions rather than version them. Use the edition identifier (for example `RD2022`, `v2025.10`, `4.0`). +- When the source catalog has no version, use the first year of adoption or the release date. +- Make sure the version substring embedded in the filename matches `Version`, because the CLI dispatcher reads `compliance_framework.split("_")[1]` to select the correct version. + +## Validating the Framework Locally + +Follow the steps below before opening a pull request. + +### 1. Run the Compliance Model Validator + +```bash +uv run python prowler-cli.py --list-compliance +``` + +The framework must appear in the output. A validation error indicates a schema mismatch between the JSON file and the attribute model. + +### 2. Run a Scan Filtered by the Framework + +```bash +uv run python prowler-cli.py \ + --compliance __ \ + --log-level ERROR +``` + +Verify that: + +- Prowler produces a CSV file under `output/compliance/` with the expected name. +- The CLI summary table lists every section in the framework. +- Findings roll up under the expected requirements. + +### 3. Inspect the CSV Output + +Open the generated CSV and confirm: + +- All columns defined in `models.py` appear. +- Every requirement has at least one row per scanned resource. +- Values such as `Requirements_Attributes_Section` reflect the JSON content. + +### 4. Verify the Framework in Prowler App + +Launch Prowler App locally (`docker compose up` from the repository root) and run a scan with the new compliance framework. Confirm the compliance page renders the requirements, sections, and status widgets correctly. + +## Testing + +Compliance contributions require two layers of tests. + +- **Schema tests** exercise the Pydantic models. Extend `tests/lib/check/universal_compliance_models_test.py` with a case that loads the new JSON file and asserts the attribute type matches the expected model. +- **Output tests** exercise the transformer. Mirror the structure under `tests/lib/outputs/compliance//` with fixtures that feed synthetic findings through the transformer and assert the resulting CSV rows. + +Run the suite with: + +```bash +uv run pytest -n auto tests/lib/check/universal_compliance_models_test.py \ + tests/lib/outputs/compliance/ +``` + +For guidance on writing Prowler SDK tests, refer to [Unit Testing](/developer-guide/unit-testing). + +## Submitting the Pull Request + +Before opening the pull request: + +1. Run the complete QA pipeline: + ```bash + uv run pre-commit run --all-files + uv run pytest -n auto + ``` +2. Add a changelog entry under the `### 🚀 Added` section of `prowler/CHANGELOG.md`, describing the new framework and the providers it covers. +3. Follow the [Pull Request Template](https://github.com/prowler-cloud/prowler/blob/master/.github/pull_request_template.md) and set the PR title using Conventional Commits, for example `feat(compliance): add My Framework 1.0 for AWS`. +4. Request review from the compliance codeowners listed in `.github/CODEOWNERS`. + +## Troubleshooting + +The following issues are the most common when contributing a compliance framework. + +- **`ValidationError: field required` during scan.** The JSON is missing a required attribute field. Re-check the matching Pydantic model in `prowler/lib/check/compliance_models.py`. +- **All attributes collapse to `Generic_Compliance_Requirement_Attribute` values.** The Pydantic `Union` is ordered incorrectly, or the JSON matches only the generic shape. Move the generic model to the last Union position and ensure every required field is present in the JSON. +- **`--compliance` filter does not find the framework.** The filename does not match the expected pattern `__.json`, the version is empty, or the file lives outside `prowler/compliance//`. +- **CLI summary table is empty but the CSV is populated.** The dispatcher branch in `prowler/lib/outputs/compliance/compliance.py` is missing or its substring match does not catch the framework key. +- **CSV file is missing after the scan.** The transformer class is not registered in `prowler/lib/outputs/compliance/compliance_output.py`, or `transform()` raises silently. Run the scan with `--log-level DEBUG`. +- **Findings do not roll up under a requirement.** A check listed in `Checks` either does not exist for that provider or is spelled incorrectly. Run `--list-checks | grep ` to confirm. + +## Reference Examples + +Use the following files as templates when modeling a new contribution. + +- `prowler/compliance/aws/cis_2.0_aws.json` – CIS attribute shape. +- `prowler/compliance/aws/nist_800_53_revision_5_aws.json` – Generic attribute shape. +- `prowler/compliance/aws/ccc_aws.json` – CCC attribute shape. +- `prowler/compliance/azure/ens_rd2022_azure.json` – ENS attribute shape. +- `prowler/lib/check/compliance_models.py` – Canonical Pydantic schemas. +- `prowler/lib/outputs/compliance/cis/` – Reference implementation of a multi-provider output formatter. +- `prowler/lib/outputs/compliance/generic/` – Reference implementation of a generic output formatter. diff --git a/docs/developer-guide/services.mdx b/docs/developer-guide/services.mdx index ac7088dda0..62700efb73 100644 --- a/docs/developer-guide/services.mdx +++ b/docs/developer-guide/services.mdx @@ -23,7 +23,7 @@ Within this folder the following files are also to be created: - `_service.py` – Contains all the logic and API calls of the service. - `_client_.py` – Contains the initialization of the freshly created service's class so that the checks can use it. -Once the files are create, you can check that the service has been created by running the following command: `poetry run python prowler-cli.py --list-services | grep `. +Once the files are create, you can check that the service has been created by running the following command: `uv run python prowler-cli.py --list-services | grep `. ## Service Structure and Initialisation diff --git a/docs/docs.json b/docs/docs.json index e9c8cc9347..8c3c79e8be 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -73,6 +73,12 @@ "getting-started/products/prowler-lighthouse-ai" ] }, + { + "group": "Prowler for Claude Code", + "pages": [ + "getting-started/products/prowler-claude-code-plugin" + ] + }, { "group": "Prowler MCP Server", "pages": [ @@ -119,6 +125,7 @@ "user-guide/tutorials/prowler-app-multi-tenant", "user-guide/tutorials/prowler-app-api-keys", "user-guide/tutorials/prowler-app-import-findings", + "user-guide/tutorials/prowler-app-alerts", { "group": "Mutelist", "expanded": true, @@ -176,7 +183,6 @@ "pages": [ "user-guide/cli/tutorials/misc", "user-guide/cli/tutorials/reporting", - "user-guide/cli/tutorials/compliance", "user-guide/cli/tutorials/dashboard", "user-guide/cli/tutorials/configuration_file", "user-guide/cli/tutorials/logging", @@ -326,18 +332,33 @@ "user-guide/providers/openstack/authentication" ] }, + { + "group": "Scaleway", + "pages": [ + "user-guide/providers/scaleway/getting-started-scaleway", + "user-guide/providers/scaleway/authentication" + ] + }, { "group": "Vercel", "pages": [ "user-guide/providers/vercel/getting-started-vercel", "user-guide/providers/vercel/authentication" ] + }, + { + "group": "Okta", + "pages": [ + "user-guide/providers/okta/getting-started-okta", + "user-guide/providers/okta/authentication" + ] } ] }, { "group": "Compliance", "pages": [ + "user-guide/compliance/tutorials/compliance", "user-guide/compliance/tutorials/threatscore" ] }, @@ -345,7 +366,8 @@ "group": "Cookbooks", "pages": [ "user-guide/cookbooks/kubernetes-in-cluster", - "user-guide/cookbooks/cicd-pipeline" + "user-guide/cookbooks/cicd-pipeline", + "user-guide/cookbooks/powerbi-cis-benchmarks" ] } ] @@ -503,6 +525,10 @@ } }, "redirects": [ + { + "source": "/user-guide/cli/tutorials/compliance", + "destination": "/user-guide/compliance/tutorials/compliance" + }, { "source": "/projects/prowler-open-source/en/latest/tutorials/prowler-app-lighthouse", "destination": "/user-guide/tutorials/prowler-app-lighthouse" diff --git a/docs/getting-started/basic-usage/prowler-app.mdx b/docs/getting-started/basic-usage/prowler-app.mdx index 26d7d8ee2e..bc39353dcc 100644 --- a/docs/getting-started/basic-usage/prowler-app.mdx +++ b/docs/getting-started/basic-usage/prowler-app.mdx @@ -32,11 +32,11 @@ Access Prowler App by logging in with **email and password**. Log In -## Add Cloud Provider +## Add Provider -Configure a cloud provider for scanning: +Configure a provider for scanning: -1. Navigate to `Settings > Cloud Providers` and click `Add Account`. +1. Navigate to `Settings > Providers` and click `Add Provider`. 2. Select the cloud provider. 3. Enter the provider's identifier (Optional: Add an alias): - **AWS**: Account ID diff --git a/docs/getting-started/basic-usage/prowler-mcp-tools.mdx b/docs/getting-started/basic-usage/prowler-mcp-tools.mdx index d80d8e511d..ec11680dd5 100644 --- a/docs/getting-started/basic-usage/prowler-mcp-tools.mdx +++ b/docs/getting-started/basic-usage/prowler-mcp-tools.mdx @@ -10,7 +10,7 @@ Complete reference guide for all tools available in the Prowler MCP Server. Tool |----------|------------|------------------------| | Prowler Hub | 10 tools | No | | Prowler Documentation | 2 tools | No | -| Prowler Cloud/App | 29 tools | Yes | +| Prowler Cloud/App | 32 tools | Yes | ## Tool Naming Convention @@ -36,6 +36,14 @@ Tools for searching, viewing, and analyzing security findings across all cloud p - **`prowler_app_get_finding_details`** - Get comprehensive details about a specific finding including remediation guidance, check metadata, and resource relationships - **`prowler_app_get_findings_overview`** - Get aggregate statistics and trends about security findings as a markdown report +### Finding Groups Management + +Tools for listing finding groups aggregated by check ID, viewing complete group counters, and drilling down into affected resources. + +- **`prowler_app_list_finding_groups`** - List latest or historical finding groups with filters for provider, region, service, resource, category, check, severity, status, muted state, delta, date range, and sorting +- **`prowler_app_get_finding_group_details`** - Get complete details for a specific finding group including counters, description, timestamps, and impacted providers +- **`prowler_app_list_finding_group_resources`** - List actionable unmuted resources affected by a finding group by default, including nested resource and provider data plus the `finding_id` for remediation details. Set `include_muted` to include suppressed resources + ### Provider Management Tools for managing cloud provider connections in Prowler. diff --git a/docs/getting-started/basic-usage/prowler-mcp.mdx b/docs/getting-started/basic-usage/prowler-mcp.mdx index a9357dcdeb..2c32dbdbfc 100644 --- a/docs/getting-started/basic-usage/prowler-mcp.mdx +++ b/docs/getting-started/basic-usage/prowler-mcp.mdx @@ -44,13 +44,21 @@ Choose the configuration based on your deployment: **Configuration:** + + Avoid configuring MCP clients to run `npx mcp-remote` directly. `npx` can download and execute a new package version on each run. Install a reviewed version of `mcp-remote` in a dedicated local workspace, then point the MCP client to the installed binary. + + ```bash + mkdir -p ~/.local/share/prowler-mcp-bridge + cd ~/.local/share/prowler-mcp-bridge + npm init -y + npm install --save-exact mcp-remote@0.1.38 + ``` ```json { "mcpServers": { "prowler": { - "command": "npx", + "command": "/absolute/path/to/.local/share/prowler-mcp-bridge/node_modules/.bin/mcp-remote", "args": [ - "mcp-remote", "https://mcp.prowler.com/mcp", // or your self-hosted Prowler MCP Server URL "--header", "Authorization: Bearer ${PROWLER_APP_API_KEY}" @@ -72,14 +80,20 @@ Choose the configuration based on your deployment: 2. Go to "Developer" tab 3. Click in "Edit Config" button 4. Edit the `claude_desktop_config.json` file with your favorite editor - 5. Add the following configuration: + 5. Install a reviewed version of `mcp-remote` in a dedicated local workspace: + ```bash + mkdir -p ~/.local/share/prowler-mcp-bridge + cd ~/.local/share/prowler-mcp-bridge + npm init -y + npm install --save-exact mcp-remote@0.1.38 + ``` + 6. Add the following configuration: ```json { "mcpServers": { "prowler": { - "command": "npx", + "command": "/absolute/path/to/.local/share/prowler-mcp-bridge/node_modules/.bin/mcp-remote", "args": [ - "mcp-remote", "https://mcp.prowler.com/mcp", "--header", "Authorization: Bearer ${PROWLER_APP_API_KEY}" diff --git a/docs/getting-started/installation/prowler-app.mdx b/docs/getting-started/installation/prowler-app.mdx index 0b9d15852b..52d095d581 100644 --- a/docs/getting-started/installation/prowler-app.mdx +++ b/docs/getting-started/installation/prowler-app.mdx @@ -37,8 +37,8 @@ Refer to the [Prowler App Tutorial](/user-guide/tutorials/prowler-app) for detai _Requirements_: - `git` installed. - - `poetry` installed: [poetry installation](https://python-poetry.org/docs/#installation). - - `npm` installed: [npm installation](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). + - `uv` installed: [uv installation](https://docs.astral.sh/uv/getting-started/installation/). + - `pnpm` installed through [Corepack](https://pnpm.io/installation#using-corepack) or the standalone [pnpm installation](https://pnpm.io/installation). - `Docker Compose` installed: https://docs.docker.com/compose/install/. @@ -49,8 +49,8 @@ Refer to the [Prowler App Tutorial](/user-guide/tutorials/prowler-app) for detai ```bash git clone https://github.com/prowler-cloud/prowler \ cd prowler/api \ - poetry install \ - eval $(poetry env activate) \ + uv sync \ + source .venv/bin/activate \ set -a \ source .env \ docker compose up postgres valkey -d \ @@ -59,11 +59,6 @@ Refer to the [Prowler App Tutorial](/user-guide/tutorials/prowler-app) for detai gunicorn -c config/guniconf.py config.wsgi:application ``` - - Starting from Poetry v2.0.0, `poetry shell` has been deprecated in favor of `poetry env activate`. - - If your poetry version is below 2.0.0 you must keep using `poetry shell` to activate your environment. In case you have any doubts, consult the Poetry environment activation guide: https://python-poetry.org/docs/managing-environments/#activating-the-environment - > Now, you can access the API documentation at http://localhost:8080/api/v1/docs. _Commands to run the API Worker_: @@ -71,8 +66,8 @@ Refer to the [Prowler App Tutorial](/user-guide/tutorials/prowler-app) for detai ```bash git clone https://github.com/prowler-cloud/prowler \ cd prowler/api \ - poetry install \ - eval $(poetry env activate) \ + uv sync \ + source .venv/bin/activate \ set -a \ source .env \ cd src/backend \ @@ -84,8 +79,8 @@ Refer to the [Prowler App Tutorial](/user-guide/tutorials/prowler-app) for detai ```bash git clone https://github.com/prowler-cloud/prowler \ cd prowler/api \ - poetry install \ - eval $(poetry env activate) \ + uv sync \ + source .venv/bin/activate \ set -a \ source .env \ cd src/backend \ @@ -97,9 +92,11 @@ Refer to the [Prowler App Tutorial](/user-guide/tutorials/prowler-app) for detai ```bash git clone https://github.com/prowler-cloud/prowler \ cd prowler/ui \ - npm install \ - npm run build \ - npm start + corepack enable \ + corepack install \ + pnpm install --frozen-lockfile \ + pnpm run build \ + pnpm start ``` > Enjoy Prowler App at http://localhost:3000 by signing up with your email and password. @@ -121,8 +118,8 @@ To update the environment file: Edit the `.env` file and change version values: ```env -PROWLER_UI_VERSION="5.25.1" -PROWLER_API_VERSION="5.25.1" +PROWLER_UI_VERSION="5.28.0" +PROWLER_API_VERSION="5.28.0" ``` diff --git a/docs/getting-started/installation/prowler-cli.mdx b/docs/getting-started/installation/prowler-cli.mdx index 2f82d53166..2c79eda37c 100644 --- a/docs/getting-started/installation/prowler-cli.mdx +++ b/docs/getting-started/installation/prowler-cli.mdx @@ -68,7 +68,7 @@ To install Prowler as a Python package, use `Python >= 3.10, <= 3.12`. Prowler i _Requirements for Developers_: * `git` - * `poetry` installed: [poetry installation](https://python-poetry.org/docs/#installation). + * `uv` installed: [uv installation](https://docs.astral.sh/uv/getting-started/installation/). * AWS, GCP, Azure and/or Kubernetes credentials _Commands_: @@ -76,8 +76,8 @@ To install Prowler as a Python package, use `Python >= 3.10, <= 3.12`. Prowler i ```bash git clone https://github.com/prowler-cloud/prowler cd prowler - poetry install - poetry run python prowler-cli.py -v + uv sync + uv run python prowler-cli.py -v ``` diff --git a/docs/getting-started/products/prowler-claude-code-plugin.mdx b/docs/getting-started/products/prowler-claude-code-plugin.mdx new file mode 100644 index 0000000000..e3c11ec810 --- /dev/null +++ b/docs/getting-started/products/prowler-claude-code-plugin.mdx @@ -0,0 +1,101 @@ +--- +title: 'Prowler for Claude Code' +--- + +End-to-end cloud security and compliance from inside [Claude Code](https://www.claude.com/product/claude-code), powered by the [Prowler MCP server](/getting-started/products/prowler-mcp). The plugin lets Claude walk a Prowler Cloud-connected account through a compliance assessment and remediate findings until the chosen security or industry framework is compliant. + + +**Preview**: this plugin is under active development. Please report issues on [GitHub](https://github.com/prowler-cloud/prowler/issues) or join the [Slack community](https://goto.prowler.com/slack) for feedback. + + +## Requirements + + + + Installed and signed in. See the [official install guide](https://www.claude.com/product/claude-code). + + + The free tier is enough to start. Sign up at [cloud.prowler.com](https://cloud.prowler.com). + + + Create one at [cloud.prowler.com/profile](https://cloud.prowler.com/profile). + + + +## Installation + + + + Inside a Claude Code session: + + ```text + /plugin marketplace add prowler-cloud/prowler + /plugin install prowler@prowler-plugins + ``` + + + If you already have the repository checked out: + + ```text + /plugin marketplace add /absolute/path/to/prowler + /plugin install prowler@prowler-plugins + ``` + + + +## Configuration + +On first install, Claude Code prompts for your **Prowler API key**. The value is stored securely (macOS keychain or `~/.claude/.credentials.json`) and used to authenticate against Prowler Cloud. + + +To rotate the key, uninstall and reinstall the plugin — Claude Code will prompt again. + + +## Verify the installation + +In a Claude Code session: + +```text +/mcp → "prowler" appears as a connected server +/plugin → "prowler" enabled, skill listed as prowler:framework-compliance-triage +``` + +If `/mcp` reports the `prowler` server as failed, the most common cause is a rejected API key — re-issue one in Prowler Cloud and reinstall the plugin so it re-prompts. + +## Usage + +Open a conversation that mentions the framework you want to comply with. Examples: + +- *"Make my AWS production account compliant with CIS 4.0."* +- *"Make my current Terraform project compliant with Prowler ThreatScore Compliance Framework based on the latest scan results."* +- *"Help me get to 100% on PCI-DSS for this GCP project."* + +You pick a **primary tool** (Terraform, gh / az / aws CLI, web console, or mixed) and a **mode**: + + + + Claude shows each fix — target resource, exact commands, side effects, reversibility — and waits for your go-ahead before applying. + + + Claude presents a single up-front plan grouped by shared fixes, waits for one confirmation, then proceeds. It pauses mid-loop if a fix has wide blast radius or a finding is not applicable. + + + +Claude tracks progress in a markdown report under `.prowler/` at your project root — one file per framework × account. Open it any time to see exactly where the flow is. When all findings are addressed, Claude proposes a fresh Prowler scan to verify everything end-to-end. + +## Uninstalling + +```text +/plugin uninstall prowler@prowler-plugins +/plugin marketplace remove prowler-plugins +``` + +The stored API key is removed automatically. + +## Troubleshooting + +| Symptom | Likely cause | Fix | +| --- | --- | --- | +| `/mcp` shows `prowler` as failed | Rejected API key | Generate a new one in Prowler Cloud and reinstall the plugin to re-prompt. | +| Skill not invoked when expected | The skill description didn't match the prompt | Mention the framework name plus "compliance" or "compliant" in your prompt. | +| "Framework not supported" | Prowler Hub does not list the framework for that provider | Open an issue or PR at [github.com/prowler-cloud/prowler](https://github.com/prowler-cloud/prowler). | diff --git a/docs/images/compliance/prowler-app-compliance-card-download.png b/docs/images/compliance/prowler-app-compliance-card-download.png new file mode 100644 index 0000000000..ec652f8774 Binary files /dev/null and b/docs/images/compliance/prowler-app-compliance-card-download.png differ diff --git a/docs/images/compliance/prowler-app-compliance-detail-download.png b/docs/images/compliance/prowler-app-compliance-detail-download.png new file mode 100644 index 0000000000..96c4cfc980 Binary files /dev/null and b/docs/images/compliance/prowler-app-compliance-detail-download.png differ diff --git a/docs/images/compliance/prowler-app-compliance-detail-header.png b/docs/images/compliance/prowler-app-compliance-detail-header.png new file mode 100644 index 0000000000..03065cc7a5 Binary files /dev/null and b/docs/images/compliance/prowler-app-compliance-detail-header.png differ diff --git a/docs/images/compliance/prowler-app-compliance-overview.png b/docs/images/compliance/prowler-app-compliance-overview.png new file mode 100644 index 0000000000..03302d1902 Binary files /dev/null and b/docs/images/compliance/prowler-app-compliance-overview.png differ diff --git a/docs/images/compliance/prowler-app-compliance-requirements-accordion.png b/docs/images/compliance/prowler-app-compliance-requirements-accordion.png new file mode 100644 index 0000000000..47ceba8f23 Binary files /dev/null and b/docs/images/compliance/prowler-app-compliance-requirements-accordion.png differ diff --git a/docs/images/compliance/prowler-app-compliance-threatscore-card.png b/docs/images/compliance/prowler-app-compliance-threatscore-card.png new file mode 100644 index 0000000000..3bcf349c79 Binary files /dev/null and b/docs/images/compliance/prowler-app-compliance-threatscore-card.png differ diff --git a/docs/images/compliance/prowler-app-compliance-threatscore-detail.png b/docs/images/compliance/prowler-app-compliance-threatscore-detail.png new file mode 100644 index 0000000000..57e909a94c Binary files /dev/null and b/docs/images/compliance/prowler-app-compliance-threatscore-detail.png differ diff --git a/docs/images/powerbi/benchmark-page.png b/docs/images/powerbi/benchmark-page.png new file mode 100644 index 0000000000..0e03a2b91c Binary files /dev/null and b/docs/images/powerbi/benchmark-page.png differ diff --git a/docs/images/powerbi/download-compliance-scan.png b/docs/images/powerbi/download-compliance-scan.png new file mode 100644 index 0000000000..4626d90edd Binary files /dev/null and b/docs/images/powerbi/download-compliance-scan.png differ diff --git a/docs/images/powerbi/overview-page.png b/docs/images/powerbi/overview-page.png new file mode 100644 index 0000000000..c3afde727a Binary files /dev/null and b/docs/images/powerbi/overview-page.png differ diff --git a/docs/images/powerbi/report-cover.png b/docs/images/powerbi/report-cover.png new file mode 100644 index 0000000000..051b913933 Binary files /dev/null and b/docs/images/powerbi/report-cover.png differ diff --git a/docs/images/powerbi/requirement-page.png b/docs/images/powerbi/requirement-page.png new file mode 100644 index 0000000000..a8b30be722 Binary files /dev/null and b/docs/images/powerbi/requirement-page.png differ diff --git a/docs/images/powerbi/validation.png b/docs/images/powerbi/validation.png new file mode 100644 index 0000000000..f227c4daa8 Binary files /dev/null and b/docs/images/powerbi/validation.png differ diff --git a/docs/images/powerbi/walkthrough-video-thumb.png b/docs/images/powerbi/walkthrough-video-thumb.png new file mode 100644 index 0000000000..2f76b8f1cf Binary files /dev/null and b/docs/images/powerbi/walkthrough-video-thumb.png differ diff --git a/docs/images/prowler-app/alerts/alert-email-example.png b/docs/images/prowler-app/alerts/alert-email-example.png new file mode 100644 index 0000000000..3c13f6b556 Binary files /dev/null and b/docs/images/prowler-app/alerts/alert-email-example.png differ diff --git a/docs/images/prowler-app/alerts/alerts-list.png b/docs/images/prowler-app/alerts/alerts-list.png new file mode 100644 index 0000000000..7bb03415fa Binary files /dev/null and b/docs/images/prowler-app/alerts/alerts-list.png differ diff --git a/docs/images/prowler-app/alerts/create-alert-from-findings.png b/docs/images/prowler-app/alerts/create-alert-from-findings.png new file mode 100644 index 0000000000..5524389877 Binary files /dev/null and b/docs/images/prowler-app/alerts/create-alert-from-findings.png differ diff --git a/docs/images/prowler-app/alerts/create-alert-modal.png b/docs/images/prowler-app/alerts/create-alert-modal.png new file mode 100644 index 0000000000..54924638af Binary files /dev/null and b/docs/images/prowler-app/alerts/create-alert-modal.png differ diff --git a/docs/images/prowler-app/alerts/edit-alert-test.png b/docs/images/prowler-app/alerts/edit-alert-test.png new file mode 100644 index 0000000000..252a0cf67c Binary files /dev/null and b/docs/images/prowler-app/alerts/edit-alert-test.png differ diff --git a/docs/introduction.mdx b/docs/introduction.mdx index 5747eeadb1..dd05e3dbdb 100644 --- a/docs/introduction.mdx +++ b/docs/introduction.mdx @@ -32,9 +32,10 @@ Prowler supports a wide range of providers organized by category: | [Azure](/user-guide/providers/azure/getting-started-azure) | Official | Subscriptions | UI, API, CLI | | [Cloudflare](/user-guide/providers/cloudflare/getting-started-cloudflare) | Official | Accounts | UI, API, CLI | | [Google Cloud](/user-guide/providers/gcp/getting-started-gcp) | Official | Projects | UI, API, CLI | -| **NHN** | Unofficial | Tenants | CLI | +| **NHN** | [Contact us](https://prowler.com/contact) | Tenants | CLI | | [OpenStack](/user-guide/providers/openstack/getting-started-openstack) | Official | Projects | UI, API, CLI | | [Oracle Cloud](/user-guide/providers/oci/getting-started-oci) | Official | Tenancies / Compartments | UI, API, CLI | +| [Scaleway](/user-guide/providers/scaleway/getting-started-scaleway) | [Contact us](https://prowler.com/contact) | Organizations | CLI | ### Infrastructure as Code Providers @@ -47,11 +48,12 @@ Prowler supports a wide range of providers organized by category: | Provider | Support | Audit Scope/Entities | Interface | | ----------------------------------------------------------------------------------------- | -------- | ---------------------------- | ------------ | | [GitHub](/user-guide/providers/github/getting-started-github) | Official | Organizations / Repositories | UI, API, CLI | -| [Google Workspace](/user-guide/providers/googleworkspace/getting-started-googleworkspace) | Official | Domains | CLI | +| [Google Workspace](/user-guide/providers/googleworkspace/getting-started-googleworkspace) | Official | Domains | UI, API, CLI | | [LLM](/user-guide/providers/llm/getting-started-llm) | Official | Models | CLI | | [M365](/user-guide/providers/microsoft365/getting-started-m365) | Official | Tenants | UI, API, CLI | | [MongoDB Atlas](/user-guide/providers/mongodbatlas/getting-started-mongodbatlas) | Official | Organizations | UI, API, CLI | -| [Vercel](/user-guide/providers/vercel/getting-started-vercel) | Official | Teams / Projects | CLI | +| [Okta](/user-guide/providers/okta/getting-started-okta) | Official | Organizations | CLI | +| [Vercel](/user-guide/providers/vercel/getting-started-vercel) | Official | Teams / Projects | UI, API, CLI | ### Kubernetes diff --git a/docs/security/data-regions.mdx b/docs/security/data-regions.mdx index 0602caf7b9..14c13ab98b 100644 --- a/docs/security/data-regions.mdx +++ b/docs/security/data-regions.mdx @@ -9,6 +9,8 @@ Prowler Cloud runs on AWS with high availability built in. | Region | URL | Location | |--------|-----|----------| | **EU** | [cloud.prowler.com](https://cloud.prowler.com) | Ireland (`eu-west-1`) | +| **US** | On-Demand | On-Demand | + ## Business Continuity diff --git a/docs/security/index.mdx b/docs/security/index.mdx index a9034c4cea..30364d5b82 100644 --- a/docs/security/index.mdx +++ b/docs/security/index.mdx @@ -14,7 +14,7 @@ All Prowler code goes through the same security pipeline, whether running on Pro Security tools and practices applied to all Prowler code. -## Prowler Cloud vs Self-Managed +## Prowler Cloud vs Prowler OSS (Self-Managed) | | Prowler Cloud | Self-Managed | |--|---------------|--------------| diff --git a/docs/security/software-security.mdx b/docs/security/software-security.mdx index 4c690e988b..3fee8d1251 100644 --- a/docs/security/software-security.mdx +++ b/docs/security/software-security.mdx @@ -2,96 +2,205 @@ title: 'Software Security' --- -Prowler follows a **security-by-design approach** throughout the software development lifecycle. All changes go through automated checks at every stage, from local development to production deployment. +Prowler applies security-by-design across the development lifecycle. Every change passes automated checks at each stage: pre-commit hooks locally, multiple CI gates on pull requests, branch protection before merge, container scanning before publish, and registry monitoring after release. -[Pre-commit](https://github.com/prowler-cloud/prowler/blob/master/.pre-commit-config.yaml) validations catch issues early, and [CI/CD pipelines](https://github.com/prowler-cloud/prowler/tree/master/.github) include multiple security gates ensuring code quality, secure configurations, and compliance with internal standards. +All security tooling and configuration lives in the [Prowler GitHub repository](https://github.com/prowler-cloud/prowler): [pre-commit hooks](https://github.com/prowler-cloud/prowler/blob/master/.pre-commit-config.yaml), [CI/CD workflows](https://github.com/prowler-cloud/prowler/tree/master/.github/workflows), and [Dependabot configuration](https://github.com/prowler-cloud/prowler/blob/master/.github/dependabot.yml). -Container registries are continuously scanned for vulnerabilities, with findings automatically reported to the security team for assessment and remediation. This process evolves alongside the stack as new languages, frameworks, and technologies are adopted, ensuring security practices remain comprehensive, proactive, and adaptable. +## Coverage + +Security controls cover six domains, each detailed below: + +| Domain | What It Protects | +|--------|------------------| +| [**CI/CD**](#cicd-security) | GitHub Actions workflows, runners, third-party actions | +| [**SAST**](#static-application-security-testing-sast) | Application source code | +| [**SCA**](#software-composition-analysis-sca) | Third-party dependencies and their known vulnerabilities | +| [**Supply-Chain Pinning**](#supply-chain-pinning) | Reproducible installs across Python, npm, GitHub Actions, container base images | +| [**Containers**](#container-security) | Runtime images for UI, API, SDK, Model Context Protocol (MCP) Server | +| [**Secrets**](#secrets-detection) | Credentials, tokens, API keys in code and git history | + +## CI/CD Security + +Every GitHub Actions workflow uses runner hardening, pinned action versions, and audited permissions. + +### Runner Hardening With StepSecurity + +- [**`step-security/harden-runner`**](https://github.com/step-security/harden-runner) runs as the first step in every workflow, pinned by commit SHA. +- Workflows are being migrated to explicit egress controls: some already declare an egress allow-list with `egress-policy: block`, while others still run in `egress-policy: audit` until their allowed endpoints are fully defined. +- **Global Block Policy** (StepSecurity) blocks known-malicious domains and IP addresses across every workflow run. This protection applies even in audit mode, so workflows that have not yet moved to `block` still resist known-bad egress. + +### Third-Party Action Pinning + +- Every third-party action reference uses a commit SHA with the version as a comment: `uses: org/action@ # v1.2.3`. +- Dependabot tracks the comment and proposes SHA-pinned upgrades on a monthly cadence. + +### Workflow Permissions + +- Workflows declare `permissions: {}` at the top level and grant the minimum required scopes per job. +- Code review covers permission changes; zizmor enforces the rules (see below). + +### Workflow Security Audit With Zizmor + +- **[zizmor](https://github.com/zizmorcore/zizmor)** audits every workflow file for known security anti-patterns. Runs via [`ci-zizmor.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/ci-zizmor.yml). +- Triggers on every push, every pull request that touches `.github/`, and on a daily schedule. +- Results upload to the GitHub Security tab via Static Analysis Results Interchange Format (SARIF). +- Key [audit rules](https://docs.zizmor.sh/audits/) the build gates on: + - **[PWN Request](https://docs.zizmor.sh/audits/#dangerous-triggers)** (`dangerous-triggers`): unsafe use of `pull_request_target` with checked-out PR code. + - **[Script Injection](https://docs.zizmor.sh/audits/#template-injection)** (`template-injection`): unsanitized `${{ github.event.* }}` expressions in `run:` blocks. + - **[artipacked](https://docs.zizmor.sh/audits/#artipacked)**: credential leakage through artifacts. + - **[Excessive permissions](https://docs.zizmor.sh/audits/#excessive-permissions)** (`excessive-permissions`): workflows with unneeded `write` scopes. + +### Branch Protection + +Pull requests to `master` and the active `v5.*` release branches must pass several required workflows before merge. These gates prevent specific classes of supply-chain and pipeline attacks from reaching the main branch: + +- **Compromised packages:** the **npm Package Compromised Updates** and **PyPI Package Compromised Updates** checks (StepSecurity) fail any PR that introduces a package version present in the compromised-package feed. Layered on top of osv-scanner. +- **Premature releases:** the **npm Package Cooldown** and **PyPI Package Cooldown** checks (StepSecurity) fail any PR that introduces a package version published within the cooldown window. Layered on top of pnpm's `minimumReleaseAge`. +- **Workflow exploitation:** the **PWN Request** and **Script Injection** checks (StepSecurity) reject the corresponding zizmor-detected anti-patterns at PR time. Layered on top of zizmor's audit. +- **Vulnerable code or dependencies:** CodeQL (UI, API, SDK), osv-scanner (SDK, API, UI), Bandit (SDK, API), and Trivy (container images) must all pass. ## Static Application Security Testing (SAST) -Multiple SAST tools are employed across the codebase to identify security vulnerabilities, code quality issues, and potential bugs during development. +Multiple SAST tools run on every push and pull request to catch vulnerabilities and code-quality issues before merge. -### CodeQL Analysis +### Cross-Language -- **Scope:** UI (JavaScript/TypeScript), API (Python), and SDK (Python) -- **Frequency:** On every push and pull request, plus daily scheduled scans -- **Integration:** Results uploaded to GitHub Security tab via SARIF format -- **Purpose:** Identifies security vulnerabilities, coding errors, and potential exploits in source code +- **CodeQL:** semantic code analysis for the UI (JavaScript/TypeScript), API (Python), and SDK (Python). Runs on every push and pull request, plus a daily scheduled scan, via [`sdk-codeql.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/sdk-codeql.yml), [`api-codeql.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/api-codeql.yml), and [`ui-codeql.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/ui-codeql.yml). Results upload to the GitHub Security tab via SARIF. -### Python Security Scanners +### Python (SDK + API) -- **Bandit:** Detects common security issues in Python code (SQL injection, hardcoded passwords, etc.) - - Configured to ignore test files and report only high-severity issues - - Runs on both SDK and API codebases -- **Pylint:** Static code analysis with security-focused checks - - Integrated into pre-commit hooks and CI/CD pipelines +- **Bandit:** detects common Python security issues (SQL injection, hardcoded credentials, insecure deserialization). Runs in pre-commit and on every PR/push in [`sdk-security.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/sdk-security.yml) and [`api-security.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/api-security.yml). +- **Pylint:** analyzes your code without actually running it. It checks for errors, enforces a coding standard, looks for code smells, and can suggest refactors. Runs in pre-commit and on every PR/push in [`sdk-code-quality.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/sdk-code-quality.yml) and [`api-code-quality.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/api-code-quality.yml). +- **Vulture:** dead-code detection at `--min-confidence 100`. Unused code can hide incomplete implementations or stale security paths. Runs in pre-commit and on every PR/push in `sdk-security.yml` and `api-security.yml`. +- **Flake8:** style and correctness checks for the SDK. Runs in pre-commit and on every PR/push in [`sdk-code-quality.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/sdk-code-quality.yml). -### Code Quality & Dead Code Detection +### JavaScript/TypeScript (UI) -- **Vulture:** Identifies unused code that could indicate incomplete implementations or security gaps -- **Flake8:** Style guide enforcement with security-relevant checks -- **Shellcheck:** Security and correctness checks for shell scripts +- **TypeScript (`tsc`):** strict type checking for the UI. Catches whole classes of null/undefined and type-confusion bugs at build time. Runs on every PR/push via `pnpm run healthcheck` in [`ui-tests.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/ui-tests.yml). +- **ESLint:** UI linting with a capped warning budget (`--max-warnings 40`). Runs on every PR/push via `pnpm run healthcheck` in `ui-tests.yml`. +- **Knip:** dead-code and unused-export detection for the UI. The UI analogue to Vulture. + + +Knip runs locally on demand via `pnpm run lint:knip` and is not yet wired into CI. + + +### Shell + +- **Shellcheck:** correctness and security checks for shell scripts in `.github/scripts/` and `scripts/`. Runs in pre-commit on staged files. ## Software Composition Analysis (SCA) -Dependencies are continuously monitored for known vulnerabilities with timely updates ensured. +Dependencies are scanned against public vulnerability databases on every pull request and push, with results posted directly on the PR. -### Dependency Vulnerability Scanning +### Cross-Language -- **Safety:** Scans Python dependencies against known vulnerability databases - - Runs on every commit via pre-commit hooks - - Integrated into CI/CD for SDK and API - - Configured with selective ignores for tracked exceptions -- **Trivy:** Multi-purpose scanner for containers and dependencies - - Scans all container images (UI, API, SDK, MCP Server) - - Checks for vulnerabilities in OS packages and application dependencies - - Reports findings to GitHub Security tab +- **osv-scanner:** scans lockfiles against the [OSV.dev](https://osv.dev) vulnerability database for SDK (`uv.lock`), API (`api/uv.lock`), and UI (`ui/pnpm-lock.yaml`). Runs via [`sdk-security.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/sdk-security.yml), [`api-security.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/api-security.yml), and [`ui-security.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/ui-security.yml). + - The action installs the `osv-scanner` binary and verifies its SHA-256 checksum against the upstream-signed `SHA256SUMS` manifest before running. Any mismatch aborts the scan. + - Gates the build on `HIGH`, `CRITICAL`, and `UNKNOWN` severity findings. + - Posts and updates a per-lockfile report as a pull request comment. + - Per-vulnerability ignores live in [`osv-scanner.toml`](https://github.com/prowler-cloud/prowler/blob/master/osv-scanner.toml) at the repo root, each with a reason and an expiry date. +- **Trivy:** scans container images for OS-package and application-dependency vulnerabilities. Runs in [`sdk-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/sdk-container-checks.yml), [`api-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/api-container-checks.yml), [`ui-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/ui-container-checks.yml), and [`mcp-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/mcp-container-checks.yml). Trivy uploads SARIF to the GitHub Security tab and posts a scan summary on the PR. +- **Dependabot:** [configured](https://github.com/prowler-cloud/prowler/blob/master/.github/dependabot.yml) for monthly updates of the SDK Python dependencies, GitHub Actions, Docker base images, and pre-commit hooks. Dependabot opens pull requests for known security advisories, so critical patches reach the team without delay. A 7-day default cooldown reduces exposure to compromised package releases. +- **Renovate:** [configured](https://github.com/prowler-cloud/prowler/blob/master/.github/renovate.json) dependency update automation is transitioning from Dependabot to **Renovate** to gain finer control over update cadence, grouping, and per-component scope. Both tools currently run in parallel during the migration. -### Automated Dependency Updates +#### Renovate (Primary) -- **Dependabot:** Automated pull requests for dependency updates - - **Python (pip):** Monthly updates for SDK - - **GitHub Actions:** Monthly updates for workflow dependencies - - **Docker:** Monthly updates for base images - - Temporarily paused for API and UI to maintain stability during active development - - **Security-first approach:** Even when paused, Dependabot automatically creates pull requests for security vulnerabilities, ensuring critical security patches are never delayed +Configuration: [`.github/renovate.json`](https://github.com/prowler-cloud/prowler/blob/master/.github/renovate.json) + +- **Coverage:** Python (SDK, API, MCP Server), npm (UI), GitHub Actions, Docker images, and Pre-commit hooks +- **Range Strategy:** Versions are pinned to ensure reproducible builds +- **Vulnerability Alerts:** GitHub Security Advisories generate immediate pull requests that bypass rate limits and scheduled windows, labeled `security` for prioritized triage + +#### Dependabot (Legacy) + +Configuration: [`.github/dependabot.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/dependabot.yml) + +Dependabot remains active for the SDK and shared automation ecosystems until the Renovate migration completes: + +- **Python (pip):** Monthly updates for SDK +- **GitHub Actions:** Monthly updates for workflow dependencies +- **Docker:** Monthly updates for base images +- **Pre-commit:** Monthly updates for hook revisions + +Dependabot is paused for the API and UI; Renovate now handles those components. Even when paused, Dependabot continues to open pull requests for security vulnerabilities, ensuring critical patches are never delayed. + +### JavaScript/TypeScript (UI) + +- **pnpm audit:** runs `pnpm audit --audit-level critical` on every UI pull request and push as part of `pnpm run audit` in [`ui-tests.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/ui-tests.yml). Cross-checks the npm registry's advisory database in addition to the OSV scan and surfaces npm-specific advisories that may not yet have an OSV identifier. + +## Supply-Chain Pinning + +Pinning runs across Python, npm, GitHub Actions, and container base images. Every install resolves to the exact set of versions already vetted in CI, and any drift fails loudly instead of slipping in silently. + +### Python (uv) + +The SDK, API, and MCP Server all use [uv](https://docs.astral.sh/uv/) for dependency management. Each component has its own project manifest and lock file: + +| Component | Manifest | Lock File | +|-----------|----------|-----------| +| SDK | `pyproject.toml` | `uv.lock` | +| API | `api/pyproject.toml` | `api/uv.lock` | +| MCP Server | `mcp_server/pyproject.toml` | `mcp_server/uv.lock` | + +The controls applied across all three: + +- **Direct dependencies pinned to exact versions** (`==`). No version ranges in dependency lists. +- **Transitive dependencies pinned** via `[tool.uv].constraint-dependencies` in the SDK and API manifests. The constraint set mirrors the versions locked in the corresponding `uv.lock`. A future `uv lock` preserves these versions instead of silently picking up newer releases, and the resolver fails when a constraint becomes infeasible, signaling that a deliberate bump is needed. +- **Lock files committed.** CI installs strictly from the lock. +- **uv itself pinned** in the [`setup-python-uv`](https://github.com/prowler-cloud/prowler/tree/master/.github/actions/setup-python-uv) composite action. + + +The MCP Server has a small direct-dependency surface and does not yet declare a separate constraint set. Its lock file is the source of truth. + + +### JavaScript/TypeScript (pnpm) + +The UI uses [pnpm](https://pnpm.io) with supply-chain controls configured in [`ui/pnpm-workspace.yaml`](https://github.com/prowler-cloud/prowler/blob/master/ui/pnpm-workspace.yaml). + +- **Minimum release age** (`minimumReleaseAge: 1440`): packages must publish at least 24 hours before install. This reduces exposure during the window when a compromised release has not yet been detected and yanked. +- **Lifecycle script allow-list** (`strictDepBuilds: true` + `allowBuilds`): only explicitly approved packages may run `install` or `postinstall` scripts (currently `sharp`, `esbuild`, `@sentry/cli`, `@heroui/shared-utils`, `unrs-resolver`, `msw`). Any unlisted package with lifecycle scripts fails the install. +- **Trust policy** (`trustPolicy: no-downgrade`): the install fails when a package's trust evidence drops, for example after a new publisher takes over. +- **Block exotic subdeps** (`blockExoticSubdeps: true`): transitive dependencies cannot ship as git URLs or tarballs. Every package in the tree resolves from the configured registry. +- **Transitive overrides** in [`ui/package.json`](https://github.com/prowler-cloud/prowler/blob/master/ui/package.json) force specific versions for transitive packages (`lodash`, `serialize-javascript`, `qs`, `rollup`, `minimatch`, `ajv`, and others). +- **`pnpm-lock.yaml` committed** and CI installs strictly from the lock. +- **pnpm itself pinned** via the `packageManager` field in `package.json` with an integrity hash. + +### GitHub Actions + +- Every third-party action reference uses a commit SHA, with the version in a trailing comment. +- Dependabot opens monthly PRs to bump pinned SHAs. + +### Container Base Images + +- Every Dockerfile references base images by digest (`image@sha256:...`). +- Dependabot opens monthly PRs to bump digests. ## Container Security -All container images are scanned before deployment. +Container images get scanned twice: once in CI before they push to a registry, and continuously after publish by the registries themselves. -### Trivy Vulnerability Scanning +### Pre-Publish (CI) -- Scans images for vulnerabilities and misconfigurations -- Generates SARIF reports uploaded to GitHub Security tab -- Creates PR comments with scan summaries -- Configurable to fail builds on critical findings -- Reports include CVE counts and remediation guidance +- **Trivy** scans for OS-package and application-dependency vulnerabilities. Runs in [`sdk-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/sdk-container-checks.yml), [`api-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/api-container-checks.yml), [`ui-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/ui-container-checks.yml), and [`mcp-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/mcp-container-checks.yml). Trivy uploads SARIF to the GitHub Security tab and posts a summary on the PR. Builds can fail on critical findings when configured to. +- **Hadolint** validates Dockerfile syntax and structure against secure-build best practices. Runs in pre-commit and in the same `*-container-checks.yml` workflows linked above. -### Hadolint +### Post-Publish (Registries) -- Validates Dockerfile syntax and structure -- Ensures secure image building practices +- **Amazon ECR:** ECR continuously scans published images for vulnerabilities. New advisories disclosed after publish surface on the image without requiring a rebuild. +- **Docker Hub:** Docker Hub continuously scans the same images mirrored from ECR. +- The security team reviews findings from both registries for triage and remediation. ## Secrets Detection -Prowler protects against accidental exposure of sensitive credentials. - -### TruffleHog - -- Scans entire codebase and Git history for secrets -- Runs on every push and pull request -- Pre-commit hook prevents committing secrets -- Detects high-entropy strings, API keys, tokens, and credentials -- Configured to report verified and unknown findings +- **[TruffleHog](https://github.com/trufflesecurity/trufflehog)** scans the codebase and git history on every push and pull request via [`find-secrets.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/find-secrets.yml). Detects high-entropy strings, API keys, tokens, and credentials, and reports verified and unknown findings. +- A pre-commit hook runs the same check locally and blocks secrets before they leave the developer machine. ## Security Monitoring -- **GitHub Security Tab:** Centralized view of all security findings from CodeQL, Trivy, and other SARIF-compatible tools -- **Artifact Retention:** Security scan reports retained for post-deployment analysis -- **PR Comments:** Automated security feedback on pull requests for rapid remediation +- **GitHub Security tab:** centralized view of findings from CodeQL, Trivy, zizmor, and any other SARIF-compatible tool. +- **PR comments:** osv-scanner and Trivy post per-PR summaries so issues surface during review, not after merge. +- **Artifact retention:** the build retains security scan reports for post-deployment analysis. ## Contact -For questions regarding software security, visit the [Support page](/support). +For questions about software security, see the [Support page](/support). To report a vulnerability, follow the [responsible disclosure process](https://prowler.com/.well-known/security.txt). diff --git a/docs/snippets/version-badge.mdx b/docs/snippets/version-badge.mdx index 7541ff823a..62d55c4b9d 100644 --- a/docs/snippets/version-badge.mdx +++ b/docs/snippets/version-badge.mdx @@ -1,12 +1,17 @@ export const VersionBadge = ({ version }) => { return ( - -

- Added in:  - {version} -

-
- - + + + + Added in:  + {version} + + + ); }; diff --git a/docs/style.css b/docs/style.css index 3e9bedbc80..5c626fb06b 100644 --- a/docs/style.css +++ b/docs/style.css @@ -1,4 +1,21 @@ /* Version Badge Styling */ +.version-badge-link, +.version-badge-link:hover, +.version-badge-link:focus, +.version-badge-link:active, +.version-badge-link:visited { + display: inline-block; + text-decoration: none !important; + background-image: none !important; + border-bottom: none !important; + color: inherit; + transition: opacity 0.15s ease-in-out; +} + +.version-badge-link:hover { + opacity: 0.85; +} + .version-badge-container { display: inline-block; margin: 0 0 1rem 0; diff --git a/docs/user-guide/cli/tutorials/compliance.mdx b/docs/user-guide/cli/tutorials/compliance.mdx deleted file mode 100644 index 8754be6237..0000000000 --- a/docs/user-guide/cli/tutorials/compliance.mdx +++ /dev/null @@ -1,80 +0,0 @@ ---- -title: 'Compliance' ---- - -Prowler allows you to execute checks based on requirements defined in compliance frameworks. By default, it will execute and give you an overview of the status of each compliance framework: - - - -You can find CSVs containing detailed compliance results in the compliance folder within Prowler's output folder. - -## Execute Prowler based on Compliance Frameworks - -Prowler can analyze your environment based on a specific compliance framework and get more details, to do it, you can use option `--compliance`: - -```sh -prowler --compliance -``` - -Standard results will be shown and additionally the framework information as the sample below for CIS AWS 2.0. For details a CSV file has been generated as well. - - - - -**If Prowler can't find a resource related with a check from a compliance requirement, this requirement won't appear on the output** - - -## List Available Compliance Frameworks - -To see which compliance frameworks are covered by Prowler, use the `--list-compliance` option: - -```sh -prowler --list-compliance -``` - -Or you can visit [Prowler Hub](https://hub.prowler.com/compliance). - -## List Requirements of Compliance Frameworks -To list requirements for a compliance framework, use the `--list-compliance-requirements` option: - -```sh -prowler --list-compliance-requirements -``` - -Example for the first requirements of CIS 1.5 for AWS: - -``` -Listing CIS 1.5 AWS Compliance Requirements: - -Requirement Id: 1.1 - - Description: Maintain current contact details - - Checks: - account_maintain_current_contact_details - -Requirement Id: 1.2 - - Description: Ensure security contact information is registered - - Checks: - account_security_contact_information_is_registered - -Requirement Id: 1.3 - - Description: Ensure security questions are registered in the AWS account - - Checks: - account_security_questions_are_registered_in_the_aws_account - -Requirement Id: 1.4 - - Description: Ensure no 'root' user account access key exists - - Checks: - iam_no_root_access_key - -Requirement Id: 1.5 - - Description: Ensure MFA is enabled for the 'root' user account - - Checks: - iam_root_mfa_enabled - -[redacted] - -``` - -## Create and contribute adding other Security Frameworks - -This information is part of the Developer Guide and can be found [here](/developer-guide/security-compliance-framework). diff --git a/docs/user-guide/cli/tutorials/configuration_file.mdx b/docs/user-guide/cli/tutorials/configuration_file.mdx index 7f96891cbb..caf8853a1b 100644 --- a/docs/user-guide/cli/tutorials/configuration_file.mdx +++ b/docs/user-guide/cli/tutorials/configuration_file.mdx @@ -56,6 +56,7 @@ The following list includes all the AWS checks with configurable variables that | `elb_is_in_multiple_az` | `elb_min_azs` | Integer | | `elbv2_is_in_multiple_az` | `elbv2_min_azs` | Integer | | `guardduty_is_enabled` | `mute_non_default_regions` | Boolean | +| `iam_user_access_not_stale_to_sagemaker` | `max_unused_sagemaker_access_days` | Integer | | `iam_user_accesskey_unused` | `max_unused_access_keys_days` | Integer | | `iam_user_console_access_unused` | `max_console_access_days` | Integer | | `organizations_delegated_administrators` | `organizations_trusted_delegated_administrators` | List of Strings | @@ -90,6 +91,7 @@ The following list includes all the Azure checks with configurable variables tha | `sqlserver_recommended_minimal_tls_version` | `recommended_minimal_tls_versions` | List of Strings | | `vm_sufficient_daily_backup_retention_period` | `vm_backup_min_daily_retention_days` | Integer | | `vm_desired_sku_size` | `desired_vm_sku_sizes` | List of Strings | +| `storage_smb_channel_encryption_with_secure_algorithm` | `recommended_smb_channel_encryption_algorithms` | List of Strings | | `defender_attack_path_notifications_properly_configured` | `defender_attack_path_minimal_risk_level` | String | | `apim_threat_detection_llm_jacking` | `apim_threat_detection_llm_jacking_threshold` | Float | | `apim_threat_detection_llm_jacking` | `apim_threat_detection_llm_jacking_minutes` | Integer | @@ -157,6 +159,16 @@ The following list includes all the Vercel checks with configurable variables th | `team_member_role_least_privilege` | `max_owners` | Integer | | `team_no_stale_invitations` | `stale_invitation_threshold_days` | Integer | +## Okta + +### Configurable Checks +The following list includes all the Okta checks with configurable variables that can be changed in the configuration YAML file: + +| Check Name | Value | Type | +|---------------------------------------------------------------|------------------------------------|---------| +| `application_admin_console_session_idle_timeout_15min` | `okta_admin_console_idle_timeout_max_minutes` | Integer | +| `signon_global_session_idle_timeout_15min` | `okta_max_session_idle_minutes` | Integer | + ## Config YAML File Structure @@ -186,6 +198,8 @@ aws: max_unused_access_keys_days: 45 # aws.iam_user_console_access_unused --> CIS recommends 45 days max_console_access_days: 45 + # aws.iam_user_access_not_stale_to_sagemaker --> default 90 days + max_unused_sagemaker_access_days: 90 # AWS EC2 Configuration # aws.ec2_elastic_ip_shodan @@ -522,6 +536,18 @@ azure: "1.3" ] + # Azure Storage + # azure.storage_smb_channel_encryption_with_secure_algorithm + # List of SMB channel encryption algorithms allowed on file shares. A storage + # account passes only if every enabled algorithm is in this list. Defaults to + # the value required by CIS (AES-256-GCM only, excluding weaker AES-128 ciphers). + recommended_smb_channel_encryption_algorithms: + [ + "AES-256-GCM", + # "AES-128-CCM", + # "AES-128-GCM", + ] + # Azure Virtual Machines # azure.vm_desired_sku_size # List of desired VM SKU sizes that are allowed in the organization diff --git a/docs/user-guide/cli/tutorials/scan-unused-services.mdx b/docs/user-guide/cli/tutorials/scan-unused-services.mdx index 6d675e159f..ae0311cb97 100644 --- a/docs/user-guide/cli/tutorials/scan-unused-services.mdx +++ b/docs/user-guide/cli/tutorials/scan-unused-services.mdx @@ -18,9 +18,11 @@ prowler --scan-unused-services #### ACM (AWS Certificate Manager) -Certificates stored in ACM without active usage in AWS resources are excluded. By default, Prowler only scans actively used certificates. Unused certificates will not be checked if they are expired, if their expiring date is near or if they are good. +Certificates stored in ACM without active usage in AWS resources are excluded. By default, Prowler only scans actively used certificates. Unused certificates are not evaluated for expiration, transparency logging, or weak key algorithms. - `acm_certificates_expiration_check` +- `acm_certificates_transparency_logs_enabled` +- `acm_certificates_with_secure_key_algorithms` #### Athena @@ -28,6 +30,13 @@ Upon AWS account creation, Athena provisions a default primary workgroup for the - `athena_workgroup_encryption` - `athena_workgroup_enforce_configuration` +- `athena_workgroup_logging_enabled` + +#### Amazon Bedrock + +Generative AI workloads benefit from private VPC endpoint connectivity to keep prompt and model traffic off the public internet. Prowler only evaluates this configuration for VPCs in use (with active ENIs). + +- `bedrock_vpc_endpoints_configured` #### AWS CloudTrail @@ -38,15 +47,23 @@ AWS CloudTrail should have at least one trail with a data event to record all S3 #### AWS Elastic Compute Cloud (EC2) -If Amazon Elastic Block Store (EBS) default encyption is not enabled, sensitive data at rest will remain unprotected in EC2. However, Prowler will only generate a finding if EBS volumes exist where default encryption could be enforced. +If Amazon Elastic Block Store (EBS) default encryption is not enabled, sensitive data at rest remains unprotected in EC2. Prowler only generates a finding if EBS volumes exist where default encryption could be enforced. - `ec2_ebs_default_encryption` +**EBS Snapshot Public Access**: Public EBS snapshots can leak data. Prowler only evaluates the account-level block setting if EBS snapshots exist in the account. + +- `ec2_ebs_snapshot_account_block_public_access` + +**EC2 Instance Metadata Service (IMDS)**: Enforcing IMDSv2 at the account level mitigates SSRF-based credential theft. Prowler only evaluates the account-level setting if EC2 instances exist in the account. + +- `ec2_instance_account_imdsv2_enabled` + **Security Groups**: Misconfigured security groups increase the attack surface. Prowler scans only attached security groups to report vulnerabilities in actively used configurations. Applies to: -- 15 security group-related checks, including open ports and ingress/egress traffic rules. +- 20 security group-related checks, including open ports and ingress/egress traffic rules. - `ec2_securitygroup_allow_ingress_from_internet_to_port_X` - `ec2_securitygroup_default_restrict_traffic` @@ -56,6 +73,18 @@ Prowler scans only attached security groups to report vulnerabilities in activel - `ec2_networkacl_allow_ingress_X_port` +#### AWS Identity and Access Management (IAM) + +Customer-managed IAM policies that are not attached to any user, group, or role grant no effective permissions until a principal is bound to them. Prowler treats such policies as dormant by default and skips the content-evaluation checks below when `--scan-unused-services` is not set. Enable the flag to surface findings on unattached policies as well. + +- `iam_policy_allows_privilege_escalation` +- `iam_policy_no_full_access_to_cloudtrail` +- `iam_policy_no_full_access_to_kms` +- `iam_policy_no_wildcard_marketplace_subscribe` +- `iam_no_custom_policy_permissive_role_assumption` + +The dedicated `iam_customer_unattached_policy_no_administrative_privileges` check still inspects unattached policies regardless of the flag, since its purpose is to highlight dormant administrator privileges. + #### AWS Glue AWS Glue best practices recommend encrypting metadata and connection passwords in Data Catalogs. @@ -71,6 +100,12 @@ Amazon Inspector is a vulnerability discovery service that automates continuous - `inspector2_is_enabled` +#### AWS Key Management Service (KMS) + +Customer managed Customer Master Keys (CMKs) in the `Disabled` state cannot be used for cryptographic operations, so Prowler skips the unintentional-deletion check on them by default. Enable the flag to evaluate disabled CMKs as well. + +- `kms_cmk_not_deleted_unintentionally` + #### Amazon Macie Amazon Macie leverages machine learning to automatically discover, classify, and protect sensitive data in S3 buckets. Prowler only generates findings if Macie is disabled and there are S3 buckets in the AWS account. @@ -83,6 +118,15 @@ A network firewall is essential for monitoring and controlling traffic within a - `networkfirewall_in_all_vpc` +#### Amazon Relational Database Service (RDS) + +RDS event subscriptions notify operators of critical database events. Prowler only evaluates these subscription checks when RDS clusters or instances exist in the account. + +- `rds_cluster_critical_event_subscription` +- `rds_instance_critical_event_subscription` +- `rds_instance_event_subscription_parameter_groups` +- `rds_instance_event_subscription_security_groups` + #### Amazon S3 To prevent unintended data exposure: @@ -99,6 +143,10 @@ VPC settings directly impact network security and availability. - `vpc_flow_logs_enabled` +- VPC Endpoint for EC2: Routes EC2 API calls through a private VPC endpoint to keep traffic off the public internet. Prowler only evaluates this configuration for VPCs in use, i.e., those with active ENIs. + + - `vpc_endpoint_for_ec2_enabled` + - VPC Subnet Public IP Restrictions: Prevent unintended exposure of resources to the internet. Prowler only checks this configuration for VPCs in use, i.e., those with active ENIs. - `vpc_subnet_no_public_ip_by_default` diff --git a/docs/user-guide/compliance/tutorials/compliance.mdx b/docs/user-guide/compliance/tutorials/compliance.mdx new file mode 100644 index 0000000000..63b2502b25 --- /dev/null +++ b/docs/user-guide/compliance/tutorials/compliance.mdx @@ -0,0 +1,267 @@ +--- +title: 'Compliance' +description: 'Run security checks against compliance frameworks, review posture across providers, and download CSV or PDF reports from Prowler Cloud, Prowler App, and Prowler CLI.' +--- + +Prowler maps every security check to one or more industry-standard compliance frameworks, so a single scan produces both technical findings and framework-aligned evidence. The same evaluation runs identically whether scans are launched from Prowler Cloud, Prowler App, or Prowler CLI. + +Out of the box, Prowler covers frameworks such as CIS Benchmarks, NIST 800-53, NIST CSF, NIS2, ENS RD2022, ISO 27001, PCI-DSS, SOC 2, GDPR, HIPAA, AWS Well-Architected, BSI C5, CSA CCM, MITRE ATT&CK, KISA ISMS-P, FedRAMP, and Prowler ThreatScore. The full catalog is available at [Prowler Hub](https://hub.prowler.com/compliance). + + +For the unified compliance score methodology used across frameworks, see [Prowler ThreatScore Documentation](/user-guide/compliance/tutorials/threatscore). + + + + + Review compliance posture using Prowler Cloud + + + Run compliance scans using Prowler CLI + + + +## Prowler Cloud + +The Compliance section in Prowler Cloud and Prowler App centralizes compliance posture across every connected provider. It aggregates scan results, surfaces Prowler ThreatScore, and exposes detailed requirement-level evidence for each supported framework. + +### Accessing the Compliance Section + +To open the compliance overview, follow these steps: + +1. Sign in to Prowler Cloud at [cloud.prowler.com](https://cloud.prowler.com/sign-in) or to a self-hosted Prowler App instance. +2. Select **Compliance** from the left navigation. + +The page lists every framework evaluated by the most recent completed scan of the selected provider. + +Compliance overview page in Prowler Cloud and App showing filters, the Prowler ThreatScore card, and the framework grid + + +Compliance results require at least one completed scan. If no scan has finished yet, Prowler Cloud and App display a notice prompting to launch or wait for a scan to complete. + + +### Filtering Compliance Results + +The filters bar at the top of the overview controls which scan and which regions feed every card on the page. + +#### Scan Selector + +The scan selector lists completed scans across all connected providers. Each entry includes the provider type, alias, and completion timestamp. Selecting a scan updates the entire page, including ThreatScore and every framework card. + +#### Region Filter + +The region multi-select narrows results to one or more regions detected in the selected scan. Use it to evaluate compliance posture for a specific geography or account boundary. The filter applies to: + +* The framework grid scores and pass/fail counts. +* The detailed requirement view inside each framework. + + +Region filters apply only to providers that report a region attribute (for example, AWS, Azure, and Google Cloud). Providers without regions ignore the filter. + + +#### Clearing Filters + +Select **Clear filters** to reset both the region filter and any other applied filter to its default state. The scan selector is preserved. + +### Reviewing the Prowler ThreatScore Card + +When the selected scan includes Prowler ThreatScore data, a dedicated card appears at the top of the overview, showing: + +* The overall ThreatScore (0–100) with a color-coded indicator. +* A progress bar reflecting current posture. +* Per-pillar bars for IAM, Attack Surface, and Logging and Monitoring. + +Prowler ThreatScore badge on the Compliance overview showing the overall score and per-pillar bars + +Selecting the card opens the ThreatScore framework detail page, covered in [Working With the Framework Detail Page](#working-with-the-framework-detail-page). + +For a complete explanation of the methodology, formula, and weighting, see [Prowler ThreatScore Documentation](/user-guide/compliance/tutorials/threatscore). + +### Exploring the Framework Grid + +Below ThreatScore, the framework grid shows one card per supported compliance framework. Each card includes: + +* **Framework logo and name:** Identifies the standard (CIS, NIST, ENS, ISO 27001, PCI-DSS, SOC 2, NIS2, CSA CCM, MITRE ATT&CK, and more). +* **Version:** Indicates the framework version applied to the scan. +* **Score:** The percentage of passing requirements over the total evaluated. +* **Passing Requirements:** A `passed / total` counter for additional context. +* **Download dropdown:** Quick access to the CSV report and, when supported, the PDF report. + +Download dropdown on a framework card showing CSV and PDF report options + +Select any card to open the framework detail page. + + +Score color coding follows three thresholds: red for severely low compliance, amber for partial compliance, and green for healthy posture. Hover over the score for the exact percentage. + + +### Working With the Framework Detail Page + +The detail page provides everything needed to evaluate a single framework: aggregate metrics, top failure sections, and a requirement-by-requirement view. + +#### Header, Summary Cards, and Download Actions + +The header shows the framework name, version, the provider scan being reviewed, and CSV / PDF download buttons. Below the header, summary cards condense the framework state at a glance: + +* **Requirements Status:** Donut chart with `Pass`, `Fail`, and `Manual` counts plus the total number of requirements. +* **Top Failed Sections:** Ranks the sections or pillars with the highest number of failing requirements. +* **ThreatScore Breakdown:** Appears only on the ThreatScore framework. It shows the overall score and per-pillar scores aligned with the ThreatScore pillars (IAM, Attack Surface, Logging and Monitoring, Encryption). + +The same layout applies to every compliance framework. ThreatScore is the only framework that includes the extra Breakdown card on the left; for any other framework, the Requirements Status and Top Failed Sections cards span the full row. + +Prowler ThreatScore detail page including the extra Breakdown card alongside Requirements Status and Top Failed Sections + +CIS framework detail page showing only the Requirements Status donut and the Top Failed Sections card, without the ThreatScore Breakdown + +#### Requirements Accordion + +Below the summary cards, an accordion organizes every requirement of the framework. Expand a section to see: + +* **Requirement ID and title:** Reflect the official identifier from the framework. +* **Pass / Fail / Manual badges:** Indicate the status of each requirement based on the underlying checks. +* **Custom details panel:** Opens additional context tailored to the framework. For frameworks with custom layouts, the panel surfaces fields such as control objectives, severity, attack tactics, regulatory references, or required evidence. + +Select a requirement to open the detail panel and review the failing checks, the resources affected, and remediation guidance. + +Expanded CIS requirement showing description, rationale, remediation procedure, audit procedure, profile and assessment tags, references, and the underlying check + +##### Frameworks With Custom Detail Layouts + +Several frameworks include enriched detail panels that highlight fields specific to the standard: + +* ASD Essential Eight +* AWS Well-Architected Framework +* BSI C5 +* Cloud Controls Matrix (CSA CCM) +* CIS Benchmarks +* CCC (Common Cloud Controls) +* ENS RD2022 +* ISO 27001 +* KISA ISMS-P +* MITRE ATT&CK +* Prowler ThreatScore + +Frameworks without a custom layout fall back to the generic details panel, which still exposes the official requirement metadata captured by Prowler. + +### Downloading Compliance Reports + +Prowler Cloud and App expose two formats: + +* **CSV report:** Every requirement, every check, and every finding for the selected scan and filters. Available for all supported frameworks. +* **PDF report:** Curated executive-style report. Currently supported for Prowler ThreatScore, ENS RD2022, NIS2, and CSA CCM. Additional PDF reports are added in subsequent Prowler releases. + + +**PDF detail section is capped at the first 100 failed findings per check.** The PDF is intended as an executive/auditor document, not a raw data dump: when a check produces more than 100 failed findings the report renders the first 100 and shows a banner pointing the reader to the CSV or JSON-OCSF export for the complete list. The compliance CSV and the scan outputs are never truncated. + +The cap is configurable per deployment via the `DJANGO_PDF_MAX_FINDINGS_PER_CHECK` environment variable on the Prowler API workers; set it to `0` to disable truncation entirely. The default value of `100` keeps the PDF readable and bounded in size on enterprise-scale scans (hundreds of thousands of findings) without affecting smaller scans, where the cap is rarely reached. + +Only **failed** findings are rendered in the detail section. PASS findings for the same check are excluded at query time. The PDF surfaces what needs attention, and the CSV/JSON exports surface everything for forensic review. + + +#### Downloading From the Detail Page + +Inside any framework detail page, the **CSV** and **PDF** buttons in the header trigger the same downloads as the overview dropdown. The PDF button only appears for frameworks that support it. + +Top of a framework detail page showing the CSV and PDF download buttons in the header + + +Region filters disable the per-card download dropdown to avoid generating partial reports. Open the framework detail page when downloads scoped to a region are required, or remove the region filter to download the full report. + + +#### Downloading the Full Scan Output + +To export every framework, finding, and resource at once, use the **Scan Jobs** section instead. The ZIP archive contains the CSV, JSON-OCSF, and HTML reports plus a `compliance/` subfolder with one CSV per framework. See [Prowler App — Getting Started](/user-guide/tutorials/prowler-app) for details. + +### API Access + +Every report available in the UI is also reachable through the Prowler API. The following endpoints are the most relevant: + +* [Retrieve a scan compliance report as CSV](https://api.prowler.com/api/v1/docs#tag/Scan/operation/scans_compliance_retrieve) +* [Download a complete scan output (ZIP)](https://api.prowler.com/api/v1/docs#tag/Scan/operation/scans_report_retrieve) + +Use the API to integrate compliance evidence into ticketing systems, executive dashboards, or downstream pipelines. + +## Prowler CLI + +Prowler CLI evaluates the same compliance frameworks as Prowler Cloud and App, and produces detailed CSV outputs alongside the standard scan results. By default, it runs every supported framework and prints a status summary at the end of the scan: + + + +Detailed compliance results are stored as CSV files under the `compliance/` subfolder of Prowler's output directory. + +### Scan a Specific Compliance Framework + +To scope a scan to a single framework and get the framework-specific summary, use the `--compliance` option: + +```sh +prowler --compliance +``` + +Standard results plus the framework breakdown are printed to the terminal. A dedicated CSV is also generated under the `compliance/` output folder. Sample output for CIS AWS 2.0: + + + + +If Prowler cannot find a resource related with a check from a compliance requirement, that requirement is omitted from the output. + + +### List Available Compliance Frameworks + +To see which compliance frameworks are covered by a given provider, use the `--list-compliance` option: + +```sh +prowler --list-compliance +``` + +The full catalog is also browsable at [Prowler Hub](https://hub.prowler.com/compliance). + +### List Requirements of a Compliance Framework + +To inspect the requirements that compose a specific framework, use the `--list-compliance-requirements` option: + +```sh +prowler --list-compliance-requirements +``` + +Sample output for the first requirements of CIS 1.5 for AWS: + +``` +Listing CIS 1.5 AWS Compliance Requirements: + +Requirement Id: 1.1 + - Description: Maintain current contact details + - Checks: + account_maintain_current_contact_details + +Requirement Id: 1.2 + - Description: Ensure security contact information is registered + - Checks: + account_security_contact_information_is_registered + +Requirement Id: 1.3 + - Description: Ensure security questions are registered in the AWS account + - Checks: + account_security_questions_are_registered_in_the_aws_account + +Requirement Id: 1.4 + - Description: Ensure no 'root' user account access key exists + - Checks: + iam_no_root_access_key + +Requirement Id: 1.5 + - Description: Ensure MFA is enabled for the 'root' user account + - Checks: + iam_root_mfa_enabled + +[redacted] + +``` + +## Contributing New Compliance Frameworks + +To request a new framework or contribute one, see [Creating a New Security Compliance Framework in Prowler](/developer-guide/security-compliance-framework). The developer guide covers the Pydantic schema, JSON catalog, output formatter, and PR submission steps required to ship a new framework end to end. + +## Related Documentation + +* [Prowler ThreatScore Documentation](/user-guide/compliance/tutorials/threatscore) +* [Creating a New Security Compliance Framework in Prowler](/developer-guide/security-compliance-framework) +* [Prowler App — Getting Started](/user-guide/tutorials/prowler-app) diff --git a/docs/user-guide/cookbooks/powerbi-cis-benchmarks.mdx b/docs/user-guide/cookbooks/powerbi-cis-benchmarks.mdx new file mode 100644 index 0000000000..9b9e41ed93 --- /dev/null +++ b/docs/user-guide/cookbooks/powerbi-cis-benchmarks.mdx @@ -0,0 +1,168 @@ +--- +title: "Visualize Multi-Cloud CIS Benchmarks With Power BI" +description: "Ingest Prowler compliance CSV exports into a ready-made Microsoft Power BI template that surfaces CIS Benchmark posture across AWS, Azure, Google Cloud, and Kubernetes." +--- + +The Multi-Cloud CIS Benchmarks Power BI template turns Prowler compliance CSV exports into an interactive dashboard. The template ingests scan results from Prowler CLI or Prowler Cloud and renders cross-provider CIS Benchmark coverage, profile-level breakdowns, regional drill-downs, and time-series trends. Center for Internet Security (CIS) Benchmarks are industry-standard configuration baselines maintained by CIS. + +The template and its source files live in the Prowler repository under [`contrib/PowerBI/Multicloud CIS Benchmarks`](https://github.com/prowler-cloud/prowler/tree/master/contrib/PowerBI/Multicloud%20CIS%20Benchmarks). + +Multi-Cloud CIS Benchmarks Power BI report cover showing aggregated compliance posture across providers + +## Prerequisites + +The setup requires the following components: + +* **Microsoft Power BI Desktop:** free download from Microsoft. +* **Prowler compliance CSV exports:** produced by Prowler CLI or downloaded from Prowler Cloud or Prowler App. +* **Local directory:** holds the CSV exports that the template ingests at load time. + +## Supported CIS Benchmarks + +The template ships with predefined mappings for the following CIS Benchmark versions. Exports must match these versions for the dashboard to populate correctly: + +| Compliance Framework | Version | +| ---------------------------------------------- | -------- | +| CIS Amazon Web Services Foundations Benchmark | v6.0 | +| CIS Microsoft Azure Foundations Benchmark | v5.0 | +| CIS Google Cloud Platform Foundation Benchmark | v4.0 | +| CIS Kubernetes Benchmark | v1.12.0 | + + +Other CIS Benchmark versions are not recognized by the template. Confirm the framework version before running the scan or downloading the export. + + +## Setup + +### Step 1: Install Microsoft Power BI Desktop + +Download and install Microsoft Power BI Desktop from the official Microsoft site. The template is opened with this application. + +### Step 2: Generate Compliance CSV Exports + +Compliance CSV exports can be generated through Prowler CLI or downloaded from Prowler Cloud and Prowler App. + +#### Option A: Prowler CLI + +Run a scan with the `--compliance` flag pointing to the appropriate CIS framework, for example: + +```sh +prowler aws --compliance cis_6.0_aws +prowler azure --compliance cis_5.0_azure +prowler gcp --compliance cis_4.0_gcp +prowler kubernetes --compliance cis_1.12_kubernetes +``` + +The compliance CSV exports are written to `output/compliance/` by default. + +#### Option B: Prowler Cloud or Prowler App + +Open the Compliance section, select the desired CIS Benchmark, and download the CSV export. + +Compliance section in Prowler Cloud showing the CSV download option for a CIS Benchmark scan + +### Step 3: Create a Local Directory for the Exports + +Place every CSV export in a single local directory. The template parses filenames to detect the provider, so filenames must keep the provider keyword (`aws`, `azure`, `gcp`, or `kubernetes`). + + +Time-series visualizations such as "Compliance Percent Over Time" require multiple scans from different dates in the same directory. + + +### Step 4: Open the Power BI Template + +Download the template file [`Prowler Multicloud CIS Benchmarks.pbit`](https://github.com/prowler-cloud/prowler/raw/master/contrib/PowerBI/Multicloud%20CIS%20Benchmarks/Prowler%20Multicloud%20CIS%20Benchmarks.pbit) and open it. Power BI Desktop prompts for the full filepath to the directory created in step 3. + +### Step 5: Provide the Directory Filepath + +Enter the absolute filepath without quotation marks. The Windows "copy as path" feature wraps the path in quotation marks automatically; remove them before submitting. + +### Step 6: Save the Report as a `.pbix` File + +Once the filepath is submitted, the template ingests the CSV exports and renders the report. Save the populated report as a `.pbix` file for future use. Re-running the `.pbit` template generates a fresh report against an updated directory. + +## Validation + +To confirm the CSV exports were ingested correctly, open the "Configuration" tab inside the report. + +Configuration tab in the Power BI report displaying loaded CIS Benchmarks, the Prowler CSV folder path, and the list of ingested exports + +The "Configuration" tab exposes three tables: + +* **Loaded CIS Benchmarks:** lists the benchmarks and versions supported by the template. This table is defined by the template itself and is not editable. All benchmarks remain listed regardless of which provider exports were supplied. +* **Prowler CSV Folder:** displays the absolute path provided during template load. +* **Loaded Prowler Exports:** lists every CSV file detected in the directory. A green checkmark identifies the file used as the latest assessment for each provider and benchmark combination. + +## Report Sections + +The report is organized into three navigable pages: + +| Report Page | Purpose | +| ----------- | ------------------------------------------------------------------------------------ | +| Overview | Aggregates CIS Benchmark posture across AWS, Azure, Google Cloud, and Kubernetes. | +| Benchmark | Focuses on a single CIS Benchmark with profile-level and regional filters. | +| Requirement | Drill-through page that surfaces details for a single benchmark requirement. | + +### Overview Page + +The Overview page summarizes CIS Benchmark posture across every supported provider. + +Overview page in the Power BI report aggregating CIS Benchmark posture across AWS, Azure, Google Cloud, and Kubernetes + +The Overview page contains the following components: + +| Component | Description | +| ---------------------------------------- | ---------------------------------------------------------------------------- | +| CIS Benchmark Overview | Table listing benchmark name, version, and overall compliance percentage. | +| Provider by Requirement Status | Bar chart breaking down requirements by status and provider. | +| Compliance Percent Heatmap | Heatmap of compliance percentage by benchmark and profile level. | +| Profile Level by Requirement Status | Bar chart breaking down requirements by status and profile level. | +| Compliance Percent Over Time by Provider | Line chart tracking overall compliance percentage over time by provider. | + +### Benchmark Page + +The Benchmark page focuses on a single CIS Benchmark. The benchmark, profile level, and region can be selected through dropdown filters. + +Benchmark page in the Power BI report showing region heatmap, section breakdown, time-series trend, and the requirements table + +The Benchmark page contains the following components: + +| Component | Description | +| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| Compliance Percent Heatmap | Heatmap of compliance percentage by region and profile level. | +| Benchmark Section by Requirement Status | Bar chart of requirements grouped by benchmark section and status. | +| Compliance Percent Over Time by Region | Line chart tracking compliance percentage over time by region. | +| Benchmark Requirements | Table listing requirement section, requirement number, requirement title, number of resources tested, status, and failing checks. | + +### Requirement Page + +The Requirement page is a drill-through view that exposes the full context of a single requirement. To populate the page, right-click a row in the "Benchmark Requirements" table on the Benchmark page and select "Drill through" > "Requirement". + +Requirement drill-through page in the Power BI report showing rationale, remediation, regional breakdown, and the resource-level check results + +The Requirement page contains the following components: + +| Component | Description | +| ------------------------------------------ | -------------------------------------------------------------------------------------------- | +| Title | Requirement title. | +| Rationale | Rationale for the requirement. | +| Remediation | Remediation guidance for the requirement. | +| Region by Check Status | Bar chart of Prowler check results grouped by region and status. | +| Resource Checks for Benchmark Requirements | Table listing resource ID, resource name, status, description, and the underlying Prowler check. | + +## Walkthrough Video + +A full walkthrough is available on YouTube: + +[![Multi-Cloud CIS Benchmarks Power BI walkthrough video thumbnail](/images/powerbi/walkthrough-video-thumb.png)](https://www.youtube.com/watch?v=lfKFkTqBxjU) + +## Related Resources + + + + Review the Compliance workflow across Prowler Cloud, Prowler App, and Prowler CLI. + + + Explore the built-in local dashboard for Prowler CSV exports. + + diff --git a/docs/user-guide/providers/alibabacloud/getting-started-alibabacloud.mdx b/docs/user-guide/providers/alibabacloud/getting-started-alibabacloud.mdx index a3de5fc8fa..36520a8f8c 100644 --- a/docs/user-guide/providers/alibabacloud/getting-started-alibabacloud.mdx +++ b/docs/user-guide/providers/alibabacloud/getting-started-alibabacloud.mdx @@ -40,13 +40,13 @@ Before you begin, make sure you have: ### Step 2: Access Prowler Cloud 1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app) -2. Go to "Configuration" > "Cloud Providers" +2. Go to "Configuration" > "Providers" - ![Cloud Providers Page](/images/prowler-app/cloud-providers-page.png) + ![Providers Page](/images/prowler-app/cloud-providers-page.png) -3. Click "Add Cloud Provider" +3. Click "Add Provider" - ![Add a Cloud Provider](/images/prowler-app/add-cloud-provider.png) + ![Add a Provider](/images/prowler-app/add-cloud-provider.png) 4. Select "Alibaba Cloud" diff --git a/docs/user-guide/providers/aws/cloudshell.mdx b/docs/user-guide/providers/aws/cloudshell.mdx index 7374fb1fbf..3c45cd1b2b 100644 --- a/docs/user-guide/providers/aws/cloudshell.mdx +++ b/docs/user-guide/providers/aws/cloudshell.mdx @@ -27,7 +27,7 @@ To download results from AWS CloudShell: ## Cloning Prowler from GitHub -Due to the limited storage in AWS CloudShell's home directory, installing Poetry dependencies for running Prowler from GitHub can be problematic. +Due to the limited storage in AWS CloudShell's home directory, installing uv dependencies for running Prowler from GitHub can be problematic. The following workaround ensures successful installation: @@ -37,17 +37,9 @@ adduser prowler su prowler git clone https://github.com/prowler-cloud/prowler.git cd prowler -pip install poetry -mkdir /tmp/poetry -poetry config cache-dir /tmp/poetry -eval $(poetry env activate) -poetry install +pip install uv +mkdir /tmp/uv-cache +UV_CACHE_DIR=/tmp/uv-cache uv sync +source .venv/bin/activate python prowler-cli.py -v ``` - - -Starting from Poetry v2.0.0, `poetry shell` has been deprecated in favor of `poetry env activate`. - -If your Poetry version is below v2.0.0, continue using `poetry shell` to activate your environment. For further guidance, refer to the Poetry Environment Activation Guide https://python-poetry.org/docs/managing-environments/#activating-the-environment. - - diff --git a/docs/user-guide/providers/aws/getting-started-aws.mdx b/docs/user-guide/providers/aws/getting-started-aws.mdx index 8127e873e7..f0c5ab882a 100644 --- a/docs/user-guide/providers/aws/getting-started-aws.mdx +++ b/docs/user-guide/providers/aws/getting-started-aws.mdx @@ -19,13 +19,13 @@ title: 'Getting Started With AWS on Prowler' ### Step 2: Access Prowler Cloud 1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app) -2. Go to "Configuration" > "Cloud Providers" +2. Go to "Configuration" > "Providers" - ![Cloud Providers Page](/images/prowler-app/cloud-providers-page.png) + ![Providers Page](/images/prowler-app/cloud-providers-page.png) -3. Click "Add Cloud Provider" +3. Click "Add Provider" - ![Add a Cloud Provider](/images/prowler-app/add-cloud-provider.png) + ![Add a Provider](/images/prowler-app/add-cloud-provider.png) 4. Select "Amazon Web Services" diff --git a/docs/user-guide/providers/aws/v2_to_v3_checks_mapping.mdx b/docs/user-guide/providers/aws/v2_to_v3_checks_mapping.mdx index 8937465cf7..82fbcea789 100644 --- a/docs/user-guide/providers/aws/v2_to_v3_checks_mapping.mdx +++ b/docs/user-guide/providers/aws/v2_to_v3_checks_mapping.mdx @@ -4,7 +4,7 @@ title: 'Check Mapping Prowler v4/v3 to v2' Prowler v3 and v4 introduce distinct identifiers while preserving the checks originally implemented in v2. This change was made because, in previous versions, check names were primarily derived from the CIS Benchmark for AWS. Starting with v3 and v4, all checks are independent of any security framework and have unique names and IDs. -For more details on the updated compliance implementation in Prowler v4 and v3, refer to the [Compliance](/user-guide/cli/tutorials/compliance) section. +For more details on the updated compliance implementation in Prowler v4 and v3, refer to the [Compliance](/user-guide/compliance/tutorials/compliance) section. ``` checks_v4_v3_to_v2_mapping = { diff --git a/docs/user-guide/providers/azure/getting-started-azure.mdx b/docs/user-guide/providers/azure/getting-started-azure.mdx index 456c226aab..66b3b14e3a 100644 --- a/docs/user-guide/providers/azure/getting-started-azure.mdx +++ b/docs/user-guide/providers/azure/getting-started-azure.mdx @@ -35,13 +35,13 @@ For detailed instructions on how to create the Service Principal and configure p ### Step 2: Access Prowler Cloud 1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app) -2. Navigate to `Configuration` > `Cloud Providers` +2. Navigate to `Configuration` > `Providers` - ![Cloud Providers Page](/images/prowler-app/cloud-providers-page.png) + ![Providers Page](/images/prowler-app/cloud-providers-page.png) -3. Click on `Add Cloud Provider` +3. Click on `Add Provider` - ![Add a Cloud Provider](/images/prowler-app/add-cloud-provider.png) + ![Add a Provider](/images/prowler-app/add-cloud-provider.png) 4. Select `Microsoft Azure` diff --git a/docs/user-guide/providers/cloudflare/authentication.mdx b/docs/user-guide/providers/cloudflare/authentication.mdx index eedcd57a33..fe69656a04 100644 --- a/docs/user-guide/providers/cloudflare/authentication.mdx +++ b/docs/user-guide/providers/cloudflare/authentication.mdx @@ -44,6 +44,15 @@ User API Tokens are the recommended authentication method because they: Create a **User API Token**, not an Account API Token. User API Tokens are created from the profile settings and offer finer permission control. +**Quick Setup:** Use these pre-configured links to open the Cloudflare Dashboard with the required permissions already selected: + +- [Create User API Token](https://dash.cloudflare.com/profile/api-tokens?permissionGroupKeys=%5B%7B%22key%22%3A%22account_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22dns%22%2C%22type%22%3A%22read%22%7D%5D&accountId=%2A&zoneId=all&name=Prowler%20Security%20Scanner) — creates a **User API Token** (recommended). Opens the **Create Custom Token** form prefilled with the four required read-only scopes (`Account Settings`, `Zone`, `Zone Settings`, `DNS`) and the name `Prowler Security Scanner`. Adjust **Account Resources** and **Zone Resources** to match the accounts and zones you want to scan, then click **Create Token**. +- [Create Account-Owned API Token](https://dash.cloudflare.com/?to=/:account/api-tokens&permissionGroupKeys=%5B%7B%22key%22%3A%22account_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22dns%22%2C%22type%22%3A%22read%22%7D%5D&name=Prowler%20Security%20Scanner) — creates an [account-owned token](https://developers.cloudflare.com/fundamentals/api/how-to/account-owned-token-template/) instead. Use this for automation or CI/CD where the token should not depend on a specific user account remaining active. Requires the **Super Administrator** or **Administrator** role on the account. + + +Template URLs only pre-fill the token creation form. Review the permissions, configure resources, and click **Create Token** to complete the process. + + ### Step 1: Create a User API Token 1. Log into the [Cloudflare Dashboard](https://dash.cloudflare.com). diff --git a/docs/user-guide/providers/cloudflare/getting-started-cloudflare.mdx b/docs/user-guide/providers/cloudflare/getting-started-cloudflare.mdx index 2bc18bcca8..c4303b4d50 100644 --- a/docs/user-guide/providers/cloudflare/getting-started-cloudflare.mdx +++ b/docs/user-guide/providers/cloudflare/getting-started-cloudflare.mdx @@ -14,6 +14,15 @@ Set up authentication for Cloudflare with the [Cloudflare Authentication](/user- - Grant the required read-only permissions (`Account Settings:Read`, `Zone:Read`, `Zone Settings:Read`, `DNS:Read`) - Identify the Cloudflare Account ID to use as the provider identifier + +**Quick Setup:** Use these pre-configured links to create a token with the required permissions already selected: + +- [Create User API Token](https://dash.cloudflare.com/profile/api-tokens?permissionGroupKeys=%5B%7B%22key%22%3A%22account_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22dns%22%2C%22type%22%3A%22read%22%7D%5D&accountId=%2A&zoneId=all&name=Prowler%20Security%20Scanner) — creates a User API Token (recommended). +- [Create Account-Owned API Token](https://dash.cloudflare.com/?to=/:account/api-tokens&permissionGroupKeys=%5B%7B%22key%22%3A%22account_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22dns%22%2C%22type%22%3A%22read%22%7D%5D&name=Prowler%20Security%20Scanner) — creates an [account-owned token](https://developers.cloudflare.com/fundamentals/api/how-to/account-owned-token-template/), better suited for automation and CI/CD. + +Both links open the Cloudflare Dashboard with the four required read-only scopes (`Account Settings`, `Zone`, `Zone Settings`, `DNS`) and the name `Prowler Security Scanner` prefilled. See [Cloudflare Authentication](/user-guide/providers/cloudflare/authentication#api-token-recommended) for the equivalent manual steps. + + Onboard Cloudflare using Prowler Cloud @@ -42,13 +51,13 @@ The Account ID is a 32-character hexadecimal string (e.g., `372e67954025e0ba6aaa ### Step 2: Open Prowler Cloud 1. Go to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app). -2. Navigate to "Configuration" > "Cloud Providers". +2. Navigate to "Configuration" > "Providers". - ![Cloud Providers Page](/images/prowler-app/cloud-providers-page.png) + ![Providers Page](/images/prowler-app/cloud-providers-page.png) -3. Click "Add Cloud Provider". +3. Click "Add Provider". - ![Add a Cloud Provider](/images/prowler-app/add-cloud-provider.png) + ![Add a Provider](/images/prowler-app/add-cloud-provider.png) 4. Select "Cloudflare". diff --git a/docs/user-guide/providers/gcp/getting-started-gcp.mdx b/docs/user-guide/providers/gcp/getting-started-gcp.mdx index c70250c8a9..e16114e19a 100644 --- a/docs/user-guide/providers/gcp/getting-started-gcp.mdx +++ b/docs/user-guide/providers/gcp/getting-started-gcp.mdx @@ -14,13 +14,13 @@ title: 'Getting Started With GCP on Prowler' ### Step 2: Access Prowler Cloud 1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app) -2. Go to "Configuration" > "Cloud Providers" +2. Go to "Configuration" > "Providers" - ![Cloud Providers Page](/images/prowler-app/cloud-providers-page.png) + ![Providers Page](/images/prowler-app/cloud-providers-page.png) -3. Click "Add Cloud Provider" +3. Click "Add Provider" - ![Add a Cloud Provider](/images/prowler-app/add-cloud-provider.png) + ![Add a Provider](/images/prowler-app/add-cloud-provider.png) 4. Select "Google Cloud Platform" diff --git a/docs/user-guide/providers/github/authentication.mdx b/docs/user-guide/providers/github/authentication.mdx index adbfd235a8..0b5ab6b720 100644 --- a/docs/user-guide/providers/github/authentication.mdx +++ b/docs/user-guide/providers/github/authentication.mdx @@ -275,7 +275,7 @@ For step-by-step setup instructions for Prowler Cloud, see the [Getting Started ### Using Personal Access Token -1. In Prowler Cloud, navigate to **Configuration** > **Cloud Providers** > **Add Cloud Provider** > **GitHub**. +1. In Prowler Cloud, navigate to **Configuration** > **Providers** > **Add Provider** > **GitHub**. 2. Enter your GitHub Account ID (username or organization name). diff --git a/docs/user-guide/providers/github/getting-started-github.mdx b/docs/user-guide/providers/github/getting-started-github.mdx index 3211d7058d..079f9a1d7b 100644 --- a/docs/user-guide/providers/github/getting-started-github.mdx +++ b/docs/user-guide/providers/github/getting-started-github.mdx @@ -49,13 +49,13 @@ Before adding GitHub to Prowler Cloud/App, ensure you have: ### Step 1: Access Prowler Cloud/App 1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app) -2. Go to **Configuration** → **Cloud Providers** +2. Go to **Configuration** → **Providers** - ![Cloud Providers Page](/images/prowler-app/cloud-providers-page.png) + ![Providers Page](/images/prowler-app/cloud-providers-page.png) -3. Click **Add Cloud Provider** +3. Click **Add Provider** - ![Add a Cloud Provider](/images/prowler-app/add-cloud-provider.png) + ![Add a Provider](/images/prowler-app/add-cloud-provider.png) 4. Select **GitHub** @@ -153,8 +153,8 @@ Before running Prowler CLI for GitHub, ensure you have: # Install via pip pip install prowler - # Or via poetry - poetry install + # Or via uv (from the cloned repo) + uv sync ``` 2. **Authentication Credentials** diff --git a/docs/user-guide/providers/googleworkspace/authentication.mdx b/docs/user-guide/providers/googleworkspace/authentication.mdx index 392518d85c..efed268b9a 100644 --- a/docs/user-guide/providers/googleworkspace/authentication.mdx +++ b/docs/user-guide/providers/googleworkspace/authentication.mdx @@ -18,7 +18,7 @@ Prowler requests the following read-only OAuth 2.0 scopes: | `https://www.googleapis.com/auth/admin.directory.domain.readonly` | Read access to domain information | | `https://www.googleapis.com/auth/admin.directory.customer.readonly` | Read access to customer information (Customer ID) | | `https://www.googleapis.com/auth/admin.directory.orgunit.readonly` | Read access to organizational unit hierarchy (identifies the root OU for policy filtering) | -| `https://www.googleapis.com/auth/cloud-identity.policies.readonly` | Read access to domain-level application policies (required for Calendar service checks) | +| `https://www.googleapis.com/auth/cloud-identity.policies.readonly` | Read access to domain-level application policies (required for Calendar, Gmail, Chat, and Drive service checks) | | `https://www.googleapis.com/auth/admin.directory.rolemanagement.readonly` | Read access to admin roles and role assignments | @@ -40,7 +40,7 @@ In the [Google Cloud Console](https://console.cloud.google.com), select the targ | API | Required For | |-----|--------------| | **Admin SDK API** | Directory service checks (users, roles, domains) | -| **Cloud Identity API** | Calendar service checks (domain-level sharing and invitation policies) | +| **Cloud Identity API** | Calendar, Gmail, Chat, and Drive service checks (domain-level application policies) | For each API: @@ -49,7 +49,7 @@ For each API: 3. Click **Enable** -Both APIs must be enabled in the same GCP project that hosts the Service Account. Calendar checks will return no findings if the Cloud Identity API is not enabled. +Both APIs must be enabled in the same GCP project that hosts the Service Account. Calendar, Gmail, Chat, and Drive checks will return no findings if the Cloud Identity API is not enabled. ### Step 3: Create a Service Account @@ -176,9 +176,9 @@ If Prowler connects but returns empty results or permission errors for specific - Verify all scopes are authorized in the Admin Console - Ensure the delegated user is an active super administrator -### Calendar Checks Return No Findings +### Policy API Checks Return No Findings -If the Directory checks run successfully but the Calendar checks (e.g., `calendar_external_sharing_primary_calendar`) return no findings, the Cloud Identity Policy API is not reachable for this Service Account. Verify: +If the Directory checks run successfully but the Calendar, Gmail, Chat, or Drive checks return no findings, the Cloud Identity Policy API is not reachable for this Service Account. Verify: - The **Cloud Identity API** is enabled in the GCP project hosting the Service Account (Step 2) - The scope `https://www.googleapis.com/auth/cloud-identity.policies.readonly` is included in the Domain-Wide Delegation OAuth scopes list in the Admin Console (Step 5) diff --git a/docs/user-guide/providers/googleworkspace/getting-started-googleworkspace.mdx b/docs/user-guide/providers/googleworkspace/getting-started-googleworkspace.mdx index af09ab75c3..8931c43ebd 100644 --- a/docs/user-guide/providers/googleworkspace/getting-started-googleworkspace.mdx +++ b/docs/user-guide/providers/googleworkspace/getting-started-googleworkspace.mdx @@ -43,13 +43,13 @@ The Customer ID starts with the letter "C" followed by alphanumeric characters ( ### Step 2: Open Prowler Cloud 1. Go to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app). -2. Navigate to "Configuration" > "Cloud Providers". +2. Navigate to "Configuration" > "Providers". - ![Cloud Providers Page](/images/prowler-app/cloud-providers-page.png) + ![Providers Page](/images/prowler-app/cloud-providers-page.png) -3. Click "Add Cloud Provider". +3. Click "Add Provider". - ![Add a Cloud Provider](/images/prowler-app/add-cloud-provider.png) + ![Add a Provider](/images/prowler-app/add-cloud-provider.png) 4. Select "Google Workspace". diff --git a/docs/user-guide/providers/iac/getting-started-iac.mdx b/docs/user-guide/providers/iac/getting-started-iac.mdx index 2a1e588018..d1e978dc75 100644 --- a/docs/user-guide/providers/iac/getting-started-iac.mdx +++ b/docs/user-guide/providers/iac/getting-started-iac.mdx @@ -42,13 +42,13 @@ Scanner selection is not configurable in Prowler App. Default scanners, misconfi ### Step 1: Access Prowler Cloud/App 1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app) -2. Go to "Configuration" > "Cloud Providers" +2. Go to "Configuration" > "Providers" - ![Cloud Providers Page](/images/prowler-app/cloud-providers-page.png) + ![Providers Page](/images/prowler-app/cloud-providers-page.png) -3. Click "Add Cloud Provider" +3. Click "Add Provider" - ![Add a Cloud Provider](/images/prowler-app/add-cloud-provider.png) + ![Add a Provider](/images/prowler-app/add-cloud-provider.png) 4. Select "Infrastructure as Code" diff --git a/docs/user-guide/providers/image/getting-started-image.mdx b/docs/user-guide/providers/image/getting-started-image.mdx index 9a3d67258d..b9c305d0ef 100644 --- a/docs/user-guide/providers/image/getting-started-image.mdx +++ b/docs/user-guide/providers/image/getting-started-image.mdx @@ -34,13 +34,13 @@ Prowler Cloud does not support scanner selection. The vulnerability, secret, and ### Step 1: Access Prowler Cloud 1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app) -2. Navigate to "Configuration" > "Cloud Providers" +2. Navigate to "Configuration" > "Providers" - ![Cloud Providers Page](/images/prowler-app/cloud-providers-page.png) + ![Providers Page](/images/prowler-app/cloud-providers-page.png) -3. Click "Add Cloud Provider" +3. Click "Add Provider" - ![Add a Cloud Provider](/images/prowler-app/add-cloud-provider.png) + ![Add a Provider](/images/prowler-app/add-cloud-provider.png) 4. Select "Container Registry" diff --git a/docs/user-guide/providers/kubernetes/getting-started-k8s.mdx b/docs/user-guide/providers/kubernetes/getting-started-k8s.mdx index aff63b81a3..9ca791110f 100644 --- a/docs/user-guide/providers/kubernetes/getting-started-k8s.mdx +++ b/docs/user-guide/providers/kubernetes/getting-started-k8s.mdx @@ -7,13 +7,13 @@ title: 'Getting Started with Kubernetes' ### Step 1: Access Prowler Cloud/App 1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app) -2. Go to "Configuration" > "Cloud Providers" +2. Go to "Configuration" > "Providers" - ![Cloud Providers Page](/images/prowler-app/cloud-providers-page.png) + ![Providers Page](/images/prowler-app/cloud-providers-page.png) -3. Click "Add Cloud Provider" +3. Click "Add Provider" - ![Add a Cloud Provider](/images/prowler-app/add-cloud-provider.png) + ![Add a Provider](/images/prowler-app/add-cloud-provider.png) 4. Select "Kubernetes" diff --git a/docs/user-guide/providers/llm/getting-started-llm.mdx b/docs/user-guide/providers/llm/getting-started-llm.mdx index 94256b3ee2..8cca7f2a08 100644 --- a/docs/user-guide/providers/llm/getting-started-llm.mdx +++ b/docs/user-guide/providers/llm/getting-started-llm.mdx @@ -22,7 +22,7 @@ Install promptfoo using one of the following methods: **Using npm:** ```bash -npm install -g promptfoo +npm install --global promptfoo@0.121.11 ``` **Using Homebrew (macOS):** diff --git a/docs/user-guide/providers/microsoft365/getting-started-m365.mdx b/docs/user-guide/providers/microsoft365/getting-started-m365.mdx index 1e6830c722..a21b796b5c 100644 --- a/docs/user-guide/providers/microsoft365/getting-started-m365.mdx +++ b/docs/user-guide/providers/microsoft365/getting-started-m365.mdx @@ -42,13 +42,13 @@ Set up authentication for Microsoft 365 with the [Microsoft 365 Authentication]( ### Step 2: Open Prowler Cloud 1. Go to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app). -2. Navigate to "Configuration" > "Cloud Providers". +2. Navigate to "Configuration" > "Providers". - ![Cloud Providers Page](/images/prowler-app/cloud-providers-page.png) + ![Providers Page](/images/prowler-app/cloud-providers-page.png) -3. Click "Add Cloud Provider". +3. Click "Add Provider". - ![Add a Cloud Provider](/images/prowler-app/add-cloud-provider.png) + ![Add a Provider](/images/prowler-app/add-cloud-provider.png) 4. Select "Microsoft 365". diff --git a/docs/user-guide/providers/mongodbatlas/getting-started-mongodbatlas.mdx b/docs/user-guide/providers/mongodbatlas/getting-started-mongodbatlas.mdx index c10c6aac30..c68bfac9c2 100644 --- a/docs/user-guide/providers/mongodbatlas/getting-started-mongodbatlas.mdx +++ b/docs/user-guide/providers/mongodbatlas/getting-started-mongodbatlas.mdx @@ -38,7 +38,7 @@ If **Require IP Access List for the Atlas Administration API** is enabled in you ### Step 1: Add the provider -1. Navigate to **Cloud Providers** and click **Add Cloud Provider**. +1. Navigate to **Providers** and click **Add Provider**. ![Add provider list](./img/add-provider-list.png) 2. Select **MongoDB Atlas** from the provider list. 3. Enter your **Organization ID** (24 hex characters). This value is visible in the Atlas UI under **Organization Settings**. diff --git a/docs/user-guide/providers/oci/getting-started-oci.mdx b/docs/user-guide/providers/oci/getting-started-oci.mdx index 459d9ad685..fc9fb620bf 100644 --- a/docs/user-guide/providers/oci/getting-started-oci.mdx +++ b/docs/user-guide/providers/oci/getting-started-oci.mdx @@ -16,8 +16,8 @@ The following steps apply to Prowler Cloud and the self-hosted Prowler App. ### Step 2: Access Prowler Cloud 1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app). -2. Go to **Configuration** → **Cloud Providers** and click **Add Cloud Provider**. -![Add OCI Cloud Provider](./images/oci-add-cloud-provider.png) +2. Go to **Configuration** → **Providers** and click **Add Provider**. +![Add OCI Provider](./images/oci-add-cloud-provider.png) 3. Select **Oracle Cloud** and enter the **Tenancy OCID** and an optional alias, then choose **Next**. ![Add OCI Cloud Tenancy](./images/oci-add-tenancy.png) @@ -46,7 +46,7 @@ Before you begin, ensure you have: ```bash pip install prowler # or for development: - poetry install + uv sync ``` 2. **OCI Python SDK** (automatically installed with Prowler): @@ -398,7 +398,7 @@ prowler oci --severity critical high ### Next Steps -- Learn about [Compliance Frameworks](/user-guide/cli/tutorials/compliance) in Prowler +- Learn about [Compliance Frameworks](/user-guide/compliance/tutorials/compliance) in Prowler - Review [Prowler Output Formats](/user-guide/cli/tutorials/reporting) - Explore [Integrations](/user-guide/cli/tutorials/integrations) with SIEM and ticketing systems diff --git a/docs/user-guide/providers/okta/authentication.mdx b/docs/user-guide/providers/okta/authentication.mdx new file mode 100644 index 0000000000..c9154ee936 --- /dev/null +++ b/docs/user-guide/providers/okta/authentication.mdx @@ -0,0 +1,218 @@ +--- +title: 'Okta Authentication in Prowler' +--- + +import { VersionBadge } from "/snippets/version-badge.mdx" + + + +Prowler authenticates to Okta as a **service application** using **OAuth 2.0 with a private-key JWT** (Client Credentials grant). The integration is read-only by scope and follows DISA STIG guidance for least-privilege access. + +## Common Setup + +### Prerequisites + +- An Okta organization. The UI examples below use **Identity Engine** terminology such as **Global Session Policy**; Classic Engine exposes equivalent sign-on policy concepts under older naming. +- A **Super Administrator** account on that organization for the one-time service-app setup. +- An **API Services** app integration created in the Okta Admin Console. + +### Authentication Method Overview + +| Method | Status | Use Case | +|---|---|---| +| **OAuth 2.0 (private-key JWT)** | Supported | Production scans, CI/CD, Prowler App. | + +The private-key JWT flow is the only supported authentication method in the initial release. The service application proves possession of a private key on every token request; Okta returns a short-lived access token, refreshed automatically by the SDK. + + +If a different authentication method is needed (SSWS API token, OAuth with user delegation, etc.), please open a [feature request](https://github.com/prowler-cloud/prowler/issues/new?template=feature-request.yml) describing the use case. + + +### Required OAuth Scopes + +The bundled checks require the following read-only scopes: + +- `okta.policies.read` +- `okta.brands.read` +- `okta.apps.read` + +Additional scopes will be needed as more services and checks are added. These are the current ones needed: + +| Scope | Used by | +|---|---| +| `okta.policies.read` | Sign-on, password, and authentication policies | +| `okta.brands.read` | Sign-in page customizations (DOD Notice and Consent Banner check) | +| `okta.apps.read` | First-party app settings (Okta Admin Console session), integrated app inventory, and the Authentication Policies bound to Okta applications | + +### Required Admin Role + +The service application must be assigned **one** of the following Okta admin roles: + +- **Read-Only Administrator** — covers every `signon` check and runs `application_authentication_policy_network_zone_enforced` against the apps it can see. **Visibility caveat:** under Read-Only Administrator the `/api/v1/apps` endpoint returns only the apps the service application is itself assigned to — typically just the service app's own row (for example, `Prowler Scanner`). The check still produces a finding for that app, but the rest of the org's app inventory is invisible at this role level. +- **Super Administrator** — required additionally to evaluate five application-service checks that target Okta's first-party apps (Okta Admin Console, Okta Dashboard). With Super Administrator, `application_authentication_policy_network_zone_enforced` also evaluates the full org-wide app inventory instead of the service-app-only slice. + +Okta's Management API enforces a two-layer authorization model: an OAuth **scope** decides which API endpoints the token can call, and an **admin role** decides whether the call returns data. With only a scope granted, the token mint succeeds but every read returns `403 Forbidden`. Read-Only Administrator is the minimum role that lets the granted `okta.*.read` scopes return configuration data to Prowler's checks; without it, the credential probe at provider startup fails and the scan never gets to evaluate any check. + +#### When Super Administrator is required + +Four checks need to resolve the Authentication Policy bound to Okta's first-party apps (Okta Admin Console, Okta Dashboard) and depend on `/api/v1/apps` returning those system apps — which Okta restricts to Super Administrator: + +| Check | STIG | +|---|---| +| `application_admin_console_mfa_required` | V-273193 | +| `application_admin_console_phishing_resistant_authentication` | V-273191 | +| `application_dashboard_mfa_required` | V-273194 | +| `application_dashboard_phishing_resistant_authentication` | V-273190 | + +Okta filters the first-party apps (`saasure`, `okta_enduser`) out of `/api/v1/apps` for every role below Super Administrator, so `okta.apps.read` alone is not enough. The `okta.apps.manageFirstPartyApps` permission exists only in the paid Okta Identity Governance role `ACCESS_REQUESTS_ADMIN` and cannot be added to custom roles ([Okta Permissions Catalog](https://developer.okta.com/docs/api/openapi/okta-management/guides/permissions)). + +A fifth check — `application_admin_console_session_idle_timeout_15min` (STIG V-273187) — also requires Super Administrator: it calls `GET /api/v1/first-party-app-settings/admin-console`, which returns `403 E0000006` for every role below Super Administrator. + +When the service app runs with Read-Only Administrator, the five checks listed in this section return **MANUAL** instead of PASS/FAIL — the rest of the scan keeps running. + + +Read-Only Administrator stays the recommended default for the least-privilege framing that aligns with DISA STIG. Assign Super Administrator on a separate run when full coverage of the first-party app checks is needed. + + +## Step-by-Step Setup + +### 1. Go to the admin console + +![Okta — admin console page](/user-guide/providers/okta/images/select-admin-console.png) + +### 2. [Optional] - Disable the privilege-escalation bypass (org-wide, one-time) + +In the Okta Admin Console, go to **Settings → Account → Public client app admins** and ensure it is **off**. When enabled, every API Services app can be auto-assigned the Super Administrator role after scopes are granted, which would invalidate the read-only premise of this integration. + +![Okta — disable Public client app admins](/user-guide/providers/okta/images/public-client-app-admins.png) + +### 3. Create the API Services app + +1. Go to **Applications → Applications**. + +![Okta — create API Services app](/user-guide/providers/okta/images/go-to-applications.png) + +2. **Create App Integration** + +![Okta — create App integration](/user-guide/providers/okta/images/create-new-application.png) + +3. Sign-in method: **API Services**. Click **Next**. +4. Name the app (for example, `Prowler Scanner`) and click **Save**. +5. Copy the displayed **Client ID** — you'll use it as `OKTA_CLIENT_ID`. + +![Okta — copy client id](/user-guide/providers/okta/images/copy-client-id.png) + +### 4. Switch to private-key authentication and generate a keypair + +On the new app's **General** tab, scroll to **Client Credentials**: + +1. Click **Edit**. +2. Set **Client authentication** to **Public key / Private key**. +3. Under **Public Keys**, click **Add key**. +4. In the modal, click **Generate new key**. Okta creates a JWK pair. +5. Click the **PEM** tab to switch the displayed format (or keep JWK — Prowler accepts both). +6. Copy the entire `-----BEGIN PRIVATE KEY-----` block (or the JWK JSON). +7. Click **Done**, then **Save**. + + +Okta displays the private key **only once**. If you close the modal without copying, you must generate a new key. + + +![Okta — create Public Key](/user-guide/providers/okta/images/create-public-key.png) + +### 5. Grant the required OAuth scopes + +On the app, open the **Okta API Scopes** tab and click **Grant** on every scope Prowler needs. The bundled checks require `okta.policies.read`, `okta.brands.read`, and `okta.apps.read`. + +![Okta — grant OAuth scopes](/user-guide/providers/okta/images/grant-permissions.png) + +### 6. Assign an admin role + +On the app, open the **Admin roles** tab and click **Edit assignments → Add assignment**: + +- **Role:** Read-Only Administrator (default) — covers every `signon` check and runs the per-app network-zone check against the apps the service app can see (typically only the service app's own row). +- **Resources:** All resources + +Save the changes. + +To additionally evaluate the first-party application checks (Okta Admin Console / Okta Dashboard idle timeout, MFA, and phishing-resistant authentication) and to widen the per-app network-zone check to the full org-wide app inventory, assign **Super Administrator** instead. Without Super Administrator, the five first-party checks return MANUAL and the network-zone check is limited to the service app's own visibility — the rest of the scan still runs. See [Required Admin Role](#required-admin-role) for the full breakdown. + +![Okta — grant Read-Only role](/user-guide/providers/okta/images/grant-roles.png) + +### 7. [Optional] Verify DPoP setting + +Prowler sends DPoP (Demonstrating Proof of Possession) proofs on every token request. The integration works whether the **Require Demonstrating Proof of Possession (DPoP) header in token requests** setting on the service app is on or off — but enabling it is the more secure default. + +## Prowler CLI Authentication + +### Using Environment Variables (Required for Secrets) + +Private key material **must** be supplied via environment variables — Prowler does not accept secrets through CLI flags. + +```bash +export OKTA_ORG_DOMAIN="YOUR-ORG.okta.com" +export OKTA_CLIENT_ID="0oa1234567890abcdef" + +# Either of the two — content takes precedence over file when both are set. +export OKTA_PRIVATE_KEY_FILE="/secure/path/to/prowler-okta.pem" +# or +export OKTA_PRIVATE_KEY="$(cat /secure/path/to/prowler-okta.pem)" + +# Optional — defaults to "okta.policies.read,okta.brands.read,okta.apps.read" +export OKTA_SCOPES="okta.policies.read,okta.brands.read,okta.apps.read" + +uv run python prowler-cli.py okta +``` + +### Non-Secret CLI Flags + +Non-secret values are also available as CLI flags for ergonomic overrides: + +| Flag | Equivalent env var | +|---|---| +| `--okta-org-domain` | `OKTA_ORG_DOMAIN` | +| `--okta-client-id` | `OKTA_CLIENT_ID` | +| `--okta-scopes` | `OKTA_SCOPES` | + +Run a single check directly: + +```bash +uv run python prowler-cli.py okta --check signon_global_session_idle_timeout_15min +``` + +## Troubleshooting + +### `OktaInvalidOrgDomainError` + +The org domain must be `.okta.com` (or `.oktapreview.com` / `.okta-emea.com` / `.okta-gov.com` / `.okta.mil` / `.okta-miltest.com` / `.trex-govcloud.com`). Pass the bare hostname only — no `https://` scheme, no path, no trailing slash. Custom (vanity) domains are not currently accepted. + +### `OktaPrivateKeyFileError` + +The file at `OKTA_PRIVATE_KEY_FILE` is missing, unreadable, or empty. Confirm the path and that the file contains a non-empty PEM block or JWK JSON document. + +### `OktaInvalidCredentialsError` at provider init + +Prowler validates credentials at startup by listing one sign-on policy. This error indicates the credential material itself was rejected: + +- **`invalid_client`** — the public key registered in Okta does not match the private key on disk. Generate a fresh keypair and try again. + +### `OktaInsufficientPermissionsError` at provider init + +Raised when the credential probe succeeds at the OAuth layer but the request is rejected because the service app lacks the required scope or admin role: + +- **`invalid_scope`** — one of the requested scopes (`okta.policies.read`, `okta.brands.read`, or `okta.apps.read`) is not granted on the service app. Grant the missing scope from **Okta API Scopes**. +- **`Forbidden` / `not authorized`** — no admin role is assigned to the service app. Assign **Read-Only Administrator** (or **Super Administrator** for the first-party application checks) from **Admin roles**. + +### Application-service checks return MANUAL on first-party apps + +When the service app runs with Read-Only Administrator, the five application-service checks targeting the Okta Admin Console and Okta Dashboard return MANUAL. This is by design — Okta restricts the underlying endpoints (`/api/v1/first-party-app-settings/{appName}` and `/api/v1/apps` for first-party app `name` values `saasure` / `okta_enduser`) to **Super Administrator**. Assign the Super Administrator role to the service app to evaluate those checks. See [Required Admin Role](#required-admin-role) for the full list. + +### `invalid_dpop_proof` + +The org or the service app requires DPoP. The provider always sends DPoP proofs, so this error indicates the SDK could not build a valid proof — typically because the private key on disk does not match the public key uploaded to Okta. Regenerate the keypair. + +## Additional Resources + +- [Implement OAuth 2.0 for an Okta service app](https://developer.okta.com/docs/guides/implement-oauth-for-okta-serviceapp/main/) +- [Okta Policy API reference](https://developer.okta.com/docs/api/openapi/okta-management/management/tag/Policy/) +- [DISA STIG for Okta (V-273186)](https://stigviewer.com/stigs/okta/) diff --git a/docs/user-guide/providers/okta/getting-started-okta.mdx b/docs/user-guide/providers/okta/getting-started-okta.mdx new file mode 100644 index 0000000000..66d3dd1808 --- /dev/null +++ b/docs/user-guide/providers/okta/getting-started-okta.mdx @@ -0,0 +1,191 @@ +--- +title: 'Getting Started With Okta on Prowler' +--- + +import { VersionBadge } from "/snippets/version-badge.mdx" + +Prowler for Okta scans an Okta organization for identity and session-management misconfigurations. The provider authenticates as a service application using **OAuth 2.0 with a private-key JWT** (Client Credentials grant) — no end-user login, read-only by scope. + +## Prerequisites + +Set up authentication for Okta with the [Okta Authentication](/user-guide/providers/okta/authentication) guide before starting: + +- An Okta organization. The UI examples below use **Identity Engine** terminology such as **Global Session Policy**; Classic Engine exposes the equivalent sign-on policy concepts under older names. +- A **Super Administrator** account on that organization for the one-time service-app setup. +- An **API Services** app integration in the Okta Admin Console with the `okta.policies.read`, `okta.brands.read`, and `okta.apps.read` scopes granted and an admin role assigned. **Read-Only Administrator** covers every `signon` check and runs the per-app network-zone check against the apps the service app can see (under Read-Only Administrator that is typically only the service app's own row — the rest of the org's app inventory stays invisible). **Super Administrator** is required additionally to evaluate the five first-party application checks (Okta Admin Console / Okta Dashboard idle timeout, MFA, phishing-resistant authentication) and to widen the network-zone check to the full app inventory — see [Okta Authentication](/user-guide/providers/okta/authentication#required-admin-role) for the full breakdown. +- Python 3.10+ and Prowler 5.27.0 or later installed locally. + + + + Onboard Okta using Prowler Cloud + + + Onboard Okta using Prowler CLI + + + +## Prowler Cloud + + + +### Step 1: Add the Provider + +1. Go to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app). +2. Navigate to "Configuration" > "Providers". + + ![Providers Page](/images/prowler-app/cloud-providers-page.png) + +3. Click "Add Provider". + + ![Add a Provider](/images/prowler-app/add-cloud-provider.png) + +4. Select "Okta". + + ![Select Okta](/user-guide/providers/okta/images/select-okta-provider.png) + +5. Enter the **Org Domain** of the target Okta organization and an optional alias, then click "Next". + + ![Add Okta Org Domain](/user-guide/providers/okta/images/okta-org-domain-form.png) + + +The Org Domain must be the bare hostname of an Okta-managed organization — for example, `acme.okta.com`, `acme.oktapreview.com`, `acme.okta-emea.com`, `acme.okta-gov.com`, `acme.okta.mil`, `acme.okta-miltest.com`, or `acme.trex-govcloud.com`. Omit the `https://` scheme, any path, and any trailing slash. + + +### Step 2: Provide Credentials + +Prowler Cloud authenticates to Okta with the **OAuth 2.0 Private Key JWT** flow exposed by an Okta **API Services** app. The service application, keypair, scope grants, and Read-Only Administrator role are set up once in the Okta Admin Console — full instructions are in the [Okta Authentication](/user-guide/providers/okta/authentication) guide. + +1. Enter the **Client ID** of the Okta API Services app (for example, `0oa123456789abcdef`). +2. Paste the **Private Key** whose matching public key (JWK) is registered on the service app. Both PEM-encoded RSA keys and JWK JSON documents are accepted. +3. Click "Next". + + ![Okta Credentials Form](/user-guide/providers/okta/images/okta-credentials-form.png) + + +The private key is transmitted over TLS and stored as an encrypted secret in the backend. Rotate or revoke the matching public key from the Okta Admin Console at any time to invalidate the credential without changes on the Prowler side. + + +### Step 3: Launch the Scan + +1. Review the connection summary. Prowler Cloud runs a credential probe against the Okta Management API before saving — a failed probe surfaces the underlying Okta error (`invalid_scope`, `Forbidden`, invalid credentials, etc.) so the configuration can be corrected before the first scan. +2. Choose the scan schedule: run a single scan or set up daily scans (every 24 hours). +3. Click **Launch Scan** to start auditing the Okta organization. + +--- + +## Prowler CLI + + + +### Step 1: Set Up Authentication + +Follow the [Okta Authentication](/user-guide/providers/okta/authentication) guide to create the service application, generate a keypair, grant scopes, and assign the Read-Only Administrator role. Then export the credentials: + +```bash +export OKTA_ORG_DOMAIN="acme.okta.com" +export OKTA_CLIENT_ID="0oa1234567890abcdef" +export OKTA_PRIVATE_KEY_FILE="/secure/path/to/prowler-okta.pem" +# Optional — defaults to "okta.policies.read,okta.brands.read,okta.apps.read" +export OKTA_SCOPES="okta.policies.read,okta.brands.read,okta.apps.read" +``` + +The private key file may contain either a PEM-encoded RSA key or a JWK JSON document. + +#### Supplying the Private Key as Content + +For automated environments where writing the key to disk is not desirable (CI runners, container secrets, etc.), the private key may be passed directly as a string: + +```bash +export OKTA_ORG_DOMAIN="acme.okta.com" +export OKTA_CLIENT_ID="0oa1234567890abcdef" +export OKTA_PRIVATE_KEY="$(cat /secure/path/to/prowler-okta.pem)" +``` + +`OKTA_PRIVATE_KEY` takes precedence over `OKTA_PRIVATE_KEY_FILE` when both are set. The private key is intentionally not exposed as a CLI flag — secrets must be supplied via environment variables only. + +### Step 2: Run the First Scan + +Run a baseline scan after credentials are configured: + +```bash +prowler okta +``` + +Or run a specific check directly: + +```bash +prowler okta --check signon_global_session_idle_timeout_15min +``` + +Prowler prints a summary table; full findings are written to the configured output formats. + +### Step 3: Use a Custom Configuration (Optional) + +Prowler uses a configuration file to customize check thresholds. The Okta configuration currently includes: + +```yaml +okta: + # okta.signon_global_session_idle_timeout_15min + # Defaults to 15 minutes per DISA STIG V-273186. + okta_max_session_idle_minutes: 15 + # okta.application_admin_console_session_idle_timeout_15min + # Defaults to 15 minutes per DISA STIG V-273187. + okta_admin_console_idle_timeout_max_minutes: 15 +``` + +To use a custom configuration: + +```bash +prowler okta --config-file /path/to/config.yaml +``` + +## Supported Services + +Prowler for Okta includes security checks across the following services: + +| Service | Description | +| --------------- | -------------------------------------------------------------------------------------------------------------------------------------- | +| **Sign-On** | Global session policy controls (idle timeout, lifetime, rule priority and ordering) | +| **Application** | Okta Admin Console sign-on settings plus Authentication Policy controls for Okta applications (session idle, MFA, phishing resistance, network zones) | + +## Troubleshooting + +### STIG Rule Ordering + +The initial check is mapped to DISA STIG `V-273186` / `OKTA-APP-000020`. Prowler implements the STIG procedure as written: the **Default Policy** must have a **Priority 1** rule that is **not** `Default Rule`, and that rule must set **Maximum Okta global session idle time** to 15 minutes or less. + +This is stricter than simply finding the same timeout value somewhere else in the policy set. A compliant custom rule in another policy, or a compliant timeout on the built-in `Default Rule`, does not satisfy this STIG procedure. + +### Default Scopes + +Prowler requests a fixed set of OAuth scopes on every token exchange. The defaults cover every bundled check across the Sign-On and Application services: + +- `okta.policies.read` +- `okta.brands.read` +- `okta.apps.read` + +The service app must have these scopes granted in the **Okta API Scopes** tab. When the granted set is narrower than the requested set, the token request fails with an `invalid_scope` error and the scan stops at provider initialization. + +When additional checks are enabled — or when running against a service app that exposes a different scope set — override the default with `OKTA_SCOPES` (comma-separated string for the env var) or `--okta-scopes` (space-separated list for the CLI): + +```bash +# Environment variable — comma-separated +export OKTA_SCOPES="okta.policies.read,okta.brands.read,okta.apps.read,okta.users.read" + +# CLI flag — space-separated +prowler okta --okta-scopes okta.policies.read okta.brands.read okta.apps.read okta.users.read +``` + +For the full catalog of OAuth scopes exposed by the Okta Management API, refer to the [Okta OAuth 2.0 scopes documentation](https://developer.okta.com/docs/api/oauth2/). + + +As new services and checks land in the Okta provider, the default scope list grows alongside them. Re-check the granted scopes on the service app after each Prowler upgrade and grant any newly required `okta.*.read` scopes in the Admin Console. + + +### Common Errors + +- **`OktaInvalidOrgDomainError`** — the org domain must be `.okta.com` (or `.oktapreview.com` / `.okta-emea.com` / `.okta-gov.com` / `.okta.mil` / `.okta-miltest.com` / `.trex-govcloud.com`). Pass the bare hostname only — no `https://` scheme, no path, no trailing slash. +- **`OktaPrivateKeyFileError`** — confirm the file is readable and contains a non-empty PEM or JWK body. +- **`OktaInsufficientPermissionsError`** — the credential probe reached Okta but the service app cannot perform the request. The error string carries `invalid_scope`, `Forbidden`, `not authorized`, or `permission`. Fix by granting the missing `okta.*.read` scope from **Okta API Scopes** and confirming the **Read-Only Administrator** role is assigned to the service app. +- **`OktaInvalidCredentialsError`** — the credential probe reached Okta but Okta rejected the JWT. Typically the private key on disk does not match the public JWK uploaded to the service app, or the JWT signing parameters are wrong. Regenerate the keypair and re-upload the public JWK. +- **Token requests failing for an unknown scope** — the app was granted a narrower scope set than `OKTA_SCOPES` requests. Either narrow `OKTA_SCOPES` or grant the missing scopes in the Admin Console. diff --git a/docs/user-guide/providers/okta/images/copy-client-id.png b/docs/user-guide/providers/okta/images/copy-client-id.png new file mode 100644 index 0000000000..f589acf4a5 Binary files /dev/null and b/docs/user-guide/providers/okta/images/copy-client-id.png differ diff --git a/docs/user-guide/providers/okta/images/create-new-application.png b/docs/user-guide/providers/okta/images/create-new-application.png new file mode 100644 index 0000000000..2df35a877b Binary files /dev/null and b/docs/user-guide/providers/okta/images/create-new-application.png differ diff --git a/docs/user-guide/providers/okta/images/create-public-key.png b/docs/user-guide/providers/okta/images/create-public-key.png new file mode 100644 index 0000000000..2597791785 Binary files /dev/null and b/docs/user-guide/providers/okta/images/create-public-key.png differ diff --git a/docs/user-guide/providers/okta/images/go-to-applications.png b/docs/user-guide/providers/okta/images/go-to-applications.png new file mode 100644 index 0000000000..fd8eecb2f5 Binary files /dev/null and b/docs/user-guide/providers/okta/images/go-to-applications.png differ diff --git a/docs/user-guide/providers/okta/images/grant-permissions.png b/docs/user-guide/providers/okta/images/grant-permissions.png new file mode 100644 index 0000000000..890bf3e862 Binary files /dev/null and b/docs/user-guide/providers/okta/images/grant-permissions.png differ diff --git a/docs/user-guide/providers/okta/images/grant-roles.png b/docs/user-guide/providers/okta/images/grant-roles.png new file mode 100644 index 0000000000..8a670ddf73 Binary files /dev/null and b/docs/user-guide/providers/okta/images/grant-roles.png differ diff --git a/docs/user-guide/providers/okta/images/okta-credentials-form.png b/docs/user-guide/providers/okta/images/okta-credentials-form.png new file mode 100644 index 0000000000..c37553c59d Binary files /dev/null and b/docs/user-guide/providers/okta/images/okta-credentials-form.png differ diff --git a/docs/user-guide/providers/okta/images/okta-org-domain-form.png b/docs/user-guide/providers/okta/images/okta-org-domain-form.png new file mode 100644 index 0000000000..fb145d2877 Binary files /dev/null and b/docs/user-guide/providers/okta/images/okta-org-domain-form.png differ diff --git a/docs/user-guide/providers/okta/images/public-client-app-admins.png b/docs/user-guide/providers/okta/images/public-client-app-admins.png new file mode 100644 index 0000000000..345b4adcca Binary files /dev/null and b/docs/user-guide/providers/okta/images/public-client-app-admins.png differ diff --git a/docs/user-guide/providers/okta/images/select-admin-console.png b/docs/user-guide/providers/okta/images/select-admin-console.png new file mode 100644 index 0000000000..849bd465d2 Binary files /dev/null and b/docs/user-guide/providers/okta/images/select-admin-console.png differ diff --git a/docs/user-guide/providers/okta/images/select-okta-provider.png b/docs/user-guide/providers/okta/images/select-okta-provider.png new file mode 100644 index 0000000000..1a854bab37 Binary files /dev/null and b/docs/user-guide/providers/okta/images/select-okta-provider.png differ diff --git a/docs/user-guide/providers/openstack/getting-started-openstack.mdx b/docs/user-guide/providers/openstack/getting-started-openstack.mdx index b80ebe0e9f..1ff80c3e2e 100644 --- a/docs/user-guide/providers/openstack/getting-started-openstack.mdx +++ b/docs/user-guide/providers/openstack/getting-started-openstack.mdx @@ -34,7 +34,7 @@ Before running Prowler with the OpenStack provider, ensure you have: ### Step 1: Add the Provider -1. Navigate to "Cloud Providers" and click "Add Cloud Provider". +1. Navigate to "Providers" and click "Add Provider". ![Providers List](./images/select-provider.png) 2. Select "OpenStack" from the provider list. 3. Enter the "Project ID" from the OpenStack provider. diff --git a/docs/user-guide/providers/scaleway/authentication.mdx b/docs/user-guide/providers/scaleway/authentication.mdx new file mode 100644 index 0000000000..cecabf3a97 --- /dev/null +++ b/docs/user-guide/providers/scaleway/authentication.mdx @@ -0,0 +1,37 @@ +--- +title: 'Scaleway Authentication in Prowler' +--- + +Prowler authenticates to Scaleway using a **Scaleway API key** (access key + secret key). The integration is read-only and only needs permission to list IAM users and API keys in the audited organization. + +## Prerequisites + +1. A Scaleway organization with IAM access. +2. A Scaleway API key with at least the `IAMReadOnly` policy bound to a dedicated IAM user (do not use the account root user). +3. Your organization ID (visible at the top right of the Scaleway console). + +## Authentication Method + +Prowler reads credentials **exclusively** from the standard Scaleway environment variables. There are no credential CLI flags, so secrets are never exposed in shell history or process listings. + +| Variable | Purpose | +|---|---| +| `SCW_ACCESS_KEY` | API key access key | +| `SCW_SECRET_KEY` | API key secret key | +| `SCW_DEFAULT_ORGANIZATION_ID` | Optional, required when the key bearer is an application | +| `SCW_DEFAULT_PROJECT_ID` | Optional, default project for project-scoped resources | +| `SCW_DEFAULT_REGION` | Optional, defaults to `fr-par` | + +The scope variables can also be passed as CLI flags (`--organization-id`, `--project-id`, `--region`), which override the corresponding environment variables. + +```bash +export SCW_ACCESS_KEY="SCW..." +export SCW_SECRET_KEY="..." +export SCW_DEFAULT_ORGANIZATION_ID="..." + +prowler scaleway +``` + +## Required Scaleway Permissions + +The API key bearer needs read access to the IAM API in order to list users and API keys. The `IAMReadOnly` policy is sufficient. Refer to the [Scaleway IAM policy reference](https://www.scaleway.com/en/docs/identity-and-access-management/iam/reference-content/permission-sets/) for the full list of permissions. diff --git a/docs/user-guide/providers/scaleway/getting-started-scaleway.mdx b/docs/user-guide/providers/scaleway/getting-started-scaleway.mdx new file mode 100644 index 0000000000..283a7fe2e8 --- /dev/null +++ b/docs/user-guide/providers/scaleway/getting-started-scaleway.mdx @@ -0,0 +1,37 @@ +--- +title: "Getting Started With Scaleway on Prowler" +--- + +Prowler for Scaleway scans IAM resources in your Scaleway organization for security misconfigurations. The current release ships one check that flags API keys still owned by the account root user. + +## Prerequisites + +1. A Scaleway organization with IAM access. +2. A Scaleway API key with at least the `IAMReadOnly` policy bound to a dedicated IAM user (do not use the account root user). +3. Your organization ID (visible at the top right of the Scaleway console). + +## Authentication + +Prowler authenticates to Scaleway with a Scaleway API key. See [Scaleway Authentication in Prowler](./authentication) for the full setup, environment variables, CLI flags, and required permissions. + +## Run a scan + +```bash +export SCW_ACCESS_KEY="SCW..." +export SCW_SECRET_KEY="..." +export SCW_DEFAULT_ORGANIZATION_ID="..." + +prowler scaleway +``` + +To run only the IAM root-key check: + +```bash +prowler scaleway --check iam_api_keys_no_root_owned +``` + +## Checks shipped + +| Check ID | Severity | Description | +|---|---|---| +| `iam_api_keys_no_root_owned` | Critical | Fails when any Scaleway IAM API key is still owned by the account root user. | diff --git a/docs/user-guide/providers/vercel/getting-started-vercel.mdx b/docs/user-guide/providers/vercel/getting-started-vercel.mdx index 8a6fdecc22..c39c5f1e6a 100644 --- a/docs/user-guide/providers/vercel/getting-started-vercel.mdx +++ b/docs/user-guide/providers/vercel/getting-started-vercel.mdx @@ -29,13 +29,13 @@ Set up authentication for Vercel with the [Vercel Authentication](/user-guide/pr ### Step 1: Add the Provider 1. Go to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app). -2. Navigate to "Configuration" > "Cloud Providers". +2. Navigate to "Configuration" > "Providers". - ![Cloud Providers Page](/images/prowler-app/cloud-providers-page.png) + ![Providers Page](/images/prowler-app/cloud-providers-page.png) -3. Click "Add Cloud Provider". +3. Click "Add Provider". - ![Add a Cloud Provider](/images/prowler-app/add-cloud-provider.png) + ![Add a Provider](/images/prowler-app/add-cloud-provider.png) 4. Select "Vercel". diff --git a/docs/user-guide/tutorials/PowerBI.mdx b/docs/user-guide/tutorials/PowerBI.mdx deleted file mode 100644 index 03faedd478..0000000000 --- a/docs/user-guide/tutorials/PowerBI.mdx +++ /dev/null @@ -1,115 +0,0 @@ -# Prowler Multicloud CIS Benchmarks PowerBI Template -![Prowler Report](https://github.com/user-attachments/assets/560f7f83-1616-4836-811a-16963223c72f) - -## Getting Started - -1. Install Microsoft PowerBI Desktop - - This report requires the Microsoft PowerBI Desktop software which can be downloaded for free from Microsoft. -2. Run compliance scans in Prowler - - The report uses compliance csv outputs from Prowler. Compliance scans be run using either [Prowler CLI](https://docs.prowler.com/projects/prowler-open-source/en/latest/#prowler-cli) or [Prowler Cloud/App](https://cloud.prowler.com/sign-in) - 1. Prowler CLI -> Run a Prowler scan using the --compliance option - 2. Prowler Cloud/App -> Navigate to the compliance section to download csv outputs -![Download Compliance Scan](https://github.com/user-attachments/assets/42c11a60-8ce8-4c60-a663-2371199c052b) - - - The template supports the following CIS Benchmarks only: - - | Compliance Framework | Version | - | ---------------------------------------------- | ------- | - | CIS Amazon Web Services Foundations Benchmark | v4.0.1 | - | CIS Google Cloud Platform Foundation Benchmark | v3.0.0 | - | CIS Microsoft Azure Foundations Benchmark | v3.0.0 | - | CIS Kubernetes Benchmark | v1.10.0 | - - Ensure you run or download the correct benchmark versions. -3. Create a local directory to store Prowler csvoutputs - - Once downloaded, place your csv outputs in a directory on your local machine. If you rename the files, they must maintain the provider in the filename. - - To use time-series capabilities such as "compliance percent over time" you'll need scans from multiple dates. -4. Download and run the PowerBI template file (.pbit) - - Running the .pbit file will open PowerBI Desktop and prompt you for the full filepath to the local directory -5. Enter the full filepath to the directory created in step 3 - - Provide the full filepath from the root directory. - - Ensure that the filepath is not wrapped in quotation marks (""). If you use Window's "copy as path" feature, it will automatically include quotation marks. -6. Save the report as a PowerBI file (.pbix) - - Once the filepath is entered, the template will automatically ingest and populate the report. You can then save this file as a new PowerBI report. If you'd like to generate another report, simply re-run the template file (.pbit) from step 4. - -## Validation - -After setting up your dashboard, you may want to validate the Prowler csv files were ingested correctly. To do this, navigate to the "Configuration" tab. - -The "loaded CIS Benchmarks" table shows the supported benchmarks and versions. This is defined by the template file and not editable by the user. All benchmarks will be loaded regardless of which providers you provided csv outputs for. - -The "Prowler CSV Folder" shows the path to the local directory you provided. - -The "Loaded Prowler Exports" table shows the ingested csv files from the local directory. It will mark files that are treated as the latest assessment with a green checkmark. - -![Prowler Validation](https://github.com/user-attachments/assets/a543ca9b-6cbe-4ad1-b32a-d4ac2163d447) - -## Report Sections - -The PowerBI Report is broken into three main report pages - -| Report Page | Description | -| ----------- | ----------------------------------------------------------------------------------- | -| Overview | Provides general CIS Benchmark overview across both AWS, Azure, GCP, and Kubernetes | -| Benchmark | Provides overview of a single CIS Benchmark | -| Requirement | Drill-through page to view details of a single requirement | - - -### Overview Page - -The overview page is a general CIS Benchmark overview across both AWS, Azure, GCP, and Kubernetes. - -![image](https://github.com/user-attachments/assets/94164fa9-36a4-4bb9-890d-e9a9a63a3e7d) - -The page has the following components: - -| Component | Description | -| ---------------------------------------- | ------------------------------------------------------------------------ | -| CIS Benchmark Overview | Table with benchmark name, Version, and overall compliance percentage | -| Provider by Requirement Status | Bar chart showing benchmark requirements by status by provider | -| Compliance Percent Heatmap | Heatmap showing compliance percent by benchmark and profile level | -| Profile level by Requirement Status | Bar chart showing requirements by status and profile level | -| Compliance Percent Over Time by Provider | Line chart showing overall compliance perecentage over time by provider. | - -### Benchmark Page - -The benchmark page provides an overview of a single CIS Benchmark. You can select the benchmark from the dropdown as well as scope down to specific profile levels or regions. - -![image](https://github.com/user-attachments/assets/34498ee8-317b-4b81-b241-c561451d8def) - -The page has the following components: - -| Component | Description | -| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -| Compliance Percent Heatmap | Heatmap showing compliance percent by region and profile level | -| Benchmark Section by Requirement Status | Bar chart showing benchmark requirements by bennchmark section and status | -| Compliance percent Over Time by Region | Line chart showing overall compliance percentage over time by region | -| Benchmark Requirements | Table showing requirement section, requirement number, reuqirement title, number of resources tested, status, and number of failing checks | - -### Requirement Page - -The requirement page is a drill-through page to view details of a single requirement. To populate the requirement page right click on a requiement from the "Benchmark Requirements" table on the benchmark page and select "Drill through" -> "Requirement". - -![image](https://github.com/user-attachments/assets/5c9172d9-56fe-4514-b341-7e708863fad6) - -The requirement page has the following components: - -| Component | Description | -| ------------------------------------------ | --------------------------------------------------------------------------------- | -| Title | Title of the requirement | -| Rationale | Rationale of the requirement | -| Remediation | Remedation guidance for the requirement | -| Region by Check Status | Bar chart showing Prowler checks by region and status | -| Resource Checks for Benchmark Requirements | Table showing Resource ID, Resource Name, Status, Description, and Prowler Checkl | - -## Walkthrough Video -[![image](https://github.com/user-attachments/assets/866642c6-43ac-4aac-83d3-bb625002da0b)](https://www.youtube.com/watch?v=lfKFkTqBxjU) diff --git a/docs/user-guide/tutorials/prowler-app-alerts.mdx b/docs/user-guide/tutorials/prowler-app-alerts.mdx new file mode 100644 index 0000000000..f607844f97 --- /dev/null +++ b/docs/user-guide/tutorials/prowler-app-alerts.mdx @@ -0,0 +1,146 @@ +--- +title: 'Alerts' +description: 'Create email alerts from Prowler Cloud findings to monitor relevant security changes after scans or in daily digests.' +--- + +import { VersionBadge } from "/snippets/version-badge.mdx" + + + +Alerts notify recipients by email when security findings match saved filter conditions. Use Alerts to track high-priority findings, monitor specific providers or services, and keep teams informed about scan results that match defined criteria. + + +This feature is available exclusively in **Prowler Cloud** with a paid subscription. + + +## Prerequisites + +Before creating Alerts, ensure that: + +* At least one scan has completed and produced findings. +* The user role includes the `manage_alerts` permission. + +The `manage_alerts` permission is required to create, edit, test, enable, disable, and delete Alerts. See [RBAC Administrative Permissions](/user-guide/tutorials/prowler-app-rbac#rbac-administrative-permissions) for details. + +## How Alerts Work + +Alerts are created from Findings filters. When an Alert runs, Prowler Cloud evaluates the saved conditions against findings and sends an email digest when matching findings exist. + + +Alerts evaluate findings with status `FAIL` only. Findings with status `PASS` or `MANUAL`, and muted findings, never trigger an Alert regardless of the saved filters. + + +Alerts run on one of three schedules: + +| Frequency | Description | +|-----------|-------------| +| After each scan | Evaluates the Alert after each completed scan. | +| Daily digest | Evaluates the Alert once per day and sends a digest when findings match. | +| After each scan and daily | Evaluates the Alert after every scan and in the daily digest. | + +## Creating an Alert From Findings + +To create an Alert: + +1. Navigate to **Findings** in Prowler Cloud. +2. Apply at least one [Alert-compatible filter](#alert-compatible-filters) to define the findings that should trigger the Alert. +3. Click **Create Alert**. + + ![Create Alert From Findings](/images/prowler-app/alerts/create-alert-from-findings.png) + +4. Configure the Alert settings: + * **Name:** Add a short, descriptive name. + * **Description:** Add optional context for the Alert. + * **Frequency:** Select when Prowler Cloud should evaluate the Alert. + * **Recipients:** Select the recipients who should receive the email digest. + + ![Create Alert Modal](/images/prowler-app/alerts/create-alert-modal.png) + +5. Click **Create**. + +After the Alert is created, Prowler Cloud evaluates it based on the selected frequency. + +## Alert-Compatible Filters + +An **Alert-compatible filter** is a Findings-page filter that the Alert condition language can evaluate when the Alert runs. The Findings page exposes many filters, but only a specific subset can be saved into an Alert. Filters outside this subset, such as **Status**, free-text search, sort, or pagination, are ignored when seeding an Alert from the current Findings view. + +When **Create Alert** is clicked on the Findings page, Prowler Cloud takes the active filters, keeps only the Alert-compatible ones, and uses them to build the Alert condition. + +The following filters are Alert-compatible: + +* Provider type +* Provider +* Severity +* Delta (new findings since the previous scan) +* Region +* Service +* Resource type +* Category +* Resource group + +If only the **Status** filter is applied on the Findings page, Prowler Cloud substitutes all severities as the condition base so the Alert can still be created. Status itself never becomes part of the Alert condition. + +## Managing Alerts + +Navigate to **Alerts** to review and manage existing Alerts. + +![Alerts List](/images/prowler-app/alerts/alerts-list.png) + +Each Alert provides these actions: + +| Action | Description | +|--------|-------------| +| Edit | Update name, description, recipients, frequency, or filters. | +| Enable/Disable | Start or stop Alert evaluation without deleting the Alert. | +| Delete | Permanently remove the Alert. | + +## Testing Alert Filters + +When editing an Alert, click **Test** to preview whether the current filters match existing findings. + +The test result indicates whether the filters match findings and includes a summary of the matching results. + +![Edit Alert Test Result](/images/prowler-app/alerts/edit-alert-test.png) + + +**The Test result is a snapshot, not a guarantee of future Alert triggers.** + +The Test evaluates the current filters against existing findings at the moment **Test** is clicked. It does not predict whether the Alert will trigger on its next evaluation. The Alert trigger depends on the state at evaluation time: + +* **After each scan:** The Alert is evaluated against the findings produced by that scan only. If the next scan produces no findings that match the filters, the Alert will not trigger, even if a Test run earlier in the day showed matches. +* **Daily digest:** The Alert is evaluated against the findings present on the digest day. If no matching findings exist for that day, the Alert will not trigger, even if previous days had matches. + +The reverse is also true: a Test showing no matches does not guarantee the Alert will stay silent. Future scans may produce matching findings. + +Use **Test** to validate that the filters are well-formed and target the intended findings, not to forecast future Alert behavior. + + +## Recipients + +Alert recipients are selected from the email addresses available in the tenant. Recipients receive an email digest each time an Alert evaluates and matches findings. + + +By default, the **organization owner** receives a **daily digest** for **critical findings**. Adjust the recipient, frequency, or filters in the Alert configuration to change this behavior. + + +If a recipient unsubscribes from Alerts, that address stops receiving digests until it is reconfirmed. + +## Email Notifications + +When an Alert matches findings, Prowler Cloud sends a security alert email that summarizes the matching findings. The email includes: + +* The scan name and evaluation time. +* The total number of matching findings. +* The number of Alert rules that triggered. +* A preview of the affected findings, grouped by severity, with resource details and the originating rule. +* A direct link to view all matching findings in Prowler Cloud. + +![Alert Email Example](/images/prowler-app/alerts/alert-email-example.png) + +## Best Practices + +* **Start with focused filters:** Create Alerts for specific high-priority scopes, such as critical findings, production providers, or important services. +* **Use clear names:** Choose names that explain the intent of the Alert. +* **Review recipients regularly:** Keep recipient lists aligned with current ownership. +* **Test before saving edits:** Use **Test** after changing filters to confirm that the Alert matches the expected findings. +* **Disable instead of deleting during tuning:** Disable Alerts temporarily when adjusting filters or recipients. diff --git a/docs/user-guide/tutorials/prowler-app-attack-paths.mdx b/docs/user-guide/tutorials/prowler-app-attack-paths.mdx index ad0b1fcce1..23d8b8d5a2 100644 --- a/docs/user-guide/tutorials/prowler-app-attack-paths.mdx +++ b/docs/user-guide/tutorials/prowler-app-attack-paths.mdx @@ -173,6 +173,215 @@ RETURN r.name AS role_name, r.arn AS role_arn, p.arn AS trusted_service LIMIT 25 ``` +### Advanced Attack Path Scenarios + +The following scenarios show how to compose graph traversals into real attack-path stories. Each query can be pasted directly into the custom query box: the API auto-scopes them to the selected provider and injects tenant/provider isolation, so there is no need to include account identifiers or `$provider_uid` in the text. All queries are openCypher v9 (Neo4j and Neptune compatible). + +#### 1. Live attacker on the box that owns the keys + +**Query story:** Finds an internet-exposed EC2 under an active GuardDuty SSH brute-force whose instance role can assume a higher-privileged role that can read a sensitive S3 bucket. + +```cypher +MATCH path_ec2 = (acct:AWSAccount)--(ec2:EC2Instance) +WHERE ec2.exposed_internet = true +MATCH p0 = (gd:GuardDutyFinding)-[:AFFECTS]->(ec2) +MATCH p1 = (ec2)-[:INSTANCE_PROFILE]->(prof:AWSInstanceProfile)-[:ASSOCIATED_WITH]->(low:AWSRole) +MATCH p2 = (low)-[:STS_ASSUMEROLE_ALLOW]-(high:AWSRole) +MATCH p3 = (high)--(pol:AWSPolicy)--(stmt:AWSPolicyStatement) +OPTIONAL MATCH path_net = (internet:Internet)-[:CAN_ACCESS]->(ec2) +MATCH path_s3 = (acct)--(s3:S3Bucket) +WHERE high <> low + AND stmt.effect = 'Allow' + AND size([a IN stmt.action WHERE + toLower(a) STARTS WITH 's3:getobject' + OR toLower(a) STARTS WITH 's3:listbucket' + OR toLower(a) IN ['s3:*'] + ]) > 0 + AND size([r IN stmt.resource WHERE + r CONTAINS s3.name + ]) > 0 +RETURN path_net, path_ec2, p0, p1, p2, p3, path_s3 +``` + +**How it's built:** + +- `path_ec2` anchors the graph on the account node and its internet-exposed EC2 instance, via a real account-to-resource edge. This is the visible spine that keeps everything connected. +- `p0` ties a `GuardDutyFinding` to that instance through the `AFFECTS` edge (the live SSH brute-force alert). +- `p1` walks the real graph edges from the instance to its instance profile to the role it runs as. +- `p2` follows the `STS_ASSUMEROLE_ALLOW` edge to the higher-privileged role the low role can assume. It is undirected so it works regardless of how the assume edge was ingested. `high <> low` stops a role matching itself. +- `p3` walks that role into its policy and policy statement. +- `path_net` is the optional `Internet -[:CAN_ACCESS]-> instance` edge. It makes "from the internet" literal on screen. Optional so a missing `Internet` node never breaks the query live. +- `path_s3` connects the sensitive bucket to the same account node, so it draws connected instead of floating. There is no physical edge from a role to a bucket; the grant is logical, enforced in the `WHERE`: the statement must allow an S3 read action (list comprehension over the `action` array) and its resource must cover the bucket (`CONTAINS s3.name`). The account is the shared hub; the bucket hanging off it next to the role chain is the teaching moment — the access exists only in IAM. + +#### 2. Who can read the crown jewels + +**Query story:** The sensitive bucket from the previous scenario seen from the data side: every role whose IAM policy can read it, regardless of how the role is reached. + +```cypher +MATCH (s3:S3Bucket) +WHERE toLower(s3.name) CONTAINS 'sensitive' +MATCH (role:AWSRole)--(pol:AWSPolicy)--(stmt:AWSPolicyStatement) +WHERE stmt.effect = 'Allow' + AND size([a IN stmt.action WHERE + toLower(a) STARTS WITH 's3:get' + OR toLower(a) STARTS WITH 's3:list' + OR toLower(a) IN ['s3:*'] + ]) > 0 + AND size([r IN stmt.resource WHERE + r CONTAINS s3.name + ]) > 0 +WITH DISTINCT s3, role +LIMIT 25 +MATCH path_s3 = (acct:AWSAccount)--(s3) +MATCH path_role = (acct)--(role) +RETURN path_s3, path_role +``` + +**How it's built:** data-centric, not attacker-centric — the same bucket the previous kill chain exfiltrates, approached from the other direction. + +- The `S3Bucket` is bound first by name (one node), so everything else filters against it. +- `(role:AWSRole)--(pol:AWSPolicy)--(stmt:AWSPolicyStatement)` reaches statements only *through a role*, never via a global statement scan. A blanket `AWSPolicyStatement` scan also hits resource-policy statements whose shape differs and makes the list comprehension fail outright. +- The `WHERE` filters in place: an S3 read action plus a resource that names that bucket. +- `WITH DISTINCT s3, role LIMIT 25` collapses undirected-traversal duplicates and hard-caps the result. +- `path_s3` and `path_role` attach the account hubs only after the cap, against at most 25 rows, so the bucket and role(s) draw connected through the account instead of floating. +- No internet or EC2 here; this answers "who has the keys" instead of "how would an attacker get in." + +#### 3. Lateral reach from an internet-exposed instance + +**Query story:** The wide-angle view of the live-attacker scenario: every internet-exposed EC2, the role it runs as, and every role that role can assume. The first scenario is one specific exfiltration path inside this reach, under live attack. + +```cypher +MATCH path_ec2 = (acct:AWSAccount)--(ec2:EC2Instance) +WHERE ec2.exposed_internet = true +MATCH p1 = (ec2)-[:INSTANCE_PROFILE]->(prof:AWSInstanceProfile)-[:ASSOCIATED_WITH]->(low:AWSRole) +MATCH p2 = (low)-[:STS_ASSUMEROLE_ALLOW]-(high:AWSRole) +OPTIONAL MATCH path_net = (internet:Internet)-[:CAN_ACCESS]->(ec2) +WHERE high <> low +RETURN path_net, path_ec2, p1, p2 +``` + +**How it's built:** widens the lens instead of filtering down. It stops at the assume-role hop and shows every role reachable from any internet-exposed instance, without filtering down to a specific S3 leg. + +- `path_ec2` is the account-to-instance spine. +- `p1` walks to the instance role. +- `p2` fans out to every role that role can assume. +- `path_net` adds the optional `Internet -[:CAN_ACCESS]->` edge. +- The first scenario is the specific exfiltration path under live attack; this is the broader privilege reach an attacker inherits the moment they land on the box. + +#### 4. Role-chain privilege escalation + +**Query story:** A pure-IAM escalation, no compromised instance: a role that can assume a second role whose policy lets it assume a third, admin-level role. + +```cypher +MATCH path_root = (acct:AWSAccount)--(r1:AWSRole) +MATCH p1 = (r1)-[:STS_ASSUMEROLE_ALLOW]-(r2:AWSRole) +MATCH p2 = (r2)--(pol:AWSPolicy)--(stmt:AWSPolicyStatement) +MATCH path_admin = (acct)--(admin:AWSRole) +WHERE r1 <> r2 AND r1 <> admin AND r2 <> admin + AND stmt.effect = 'Allow' + AND size([a IN stmt.action WHERE + toLower(a) IN ['sts:*', 'sts:assumerole'] + ]) > 0 + AND size([res IN stmt.resource WHERE + res CONTAINS admin.name + ]) > 0 +RETURN path_root, p1, p2, path_admin +``` + +**How it's built:** + +- `path_root` anchors role 1 to the account node, the spine that keeps the picture connected. +- `p1` is the one real assume edge in the chain (role 1 to role 2). +- `p2` walks role 2 into its policy and statement. +- `path_admin` connects the target admin role to the same account node so it draws connected. The third hop is not a graph edge: it exists only as `sts:AssumeRole` on that role's ARN inside the statement. The query proves it the same way the first scenario proves S3 access — the statement action must include an assume-role action and its resource list must reference the admin role's name. +- The three `<>` guards stop a role matching itself at any position. + +#### 5. External identity trust map + +**Query story:** Finds external identity providers (SSO, GitHub, GitLab, Terraform Cloud) and the AWS roles they are trusted to assume. + +```cypher +MATCH p = (role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]-(idp:AWSPrincipal) +WHERE idp.arn CONTAINS 'saml-provider' + OR idp.arn CONTAINS 'oidc-provider' +MATCH path_role = (acct:AWSAccount)--(role) +RETURN p, path_role +``` + +**How it's built:** federated principals are stored as `AWSPrincipal` nodes whose ARN contains `saml-provider` (SSO) or `oidc-provider` (GitHub, GitLab, Terraform Cloud). + +- `p` matches the trust edge undirected. It is written `(AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(AWSPrincipal)`, role to principal, so a directed `principal -> role` match returns nothing; undirected matches regardless of ingest direction. +- The `WHERE` keeps only SAML or OIDC providers, drawing a fan-out from each external identity provider to every role it can assume (including reserved SSO admin roles). +- `path_role` ties every trusted role to the account node so the provider stars share one spine instead of drawing as separate islands. + +#### 6. Federated SSO roles flagged as admin or privesc + +**Query story:** The dangerous subset of the trust map above — externally-federated SSO roles that Prowler also flags for AdministratorAccess or privilege escalation. + +```cypher +MATCH (idp:AWSPrincipal)-[:TRUSTS_AWS_PRINCIPAL]-(role:AWSRole) +WHERE idp.arn CONTAINS 'saml-provider' + OR idp.arn CONTAINS 'oidc-provider' +MATCH (role)-[:HAS_FINDING]-(pf:ProwlerFinding) +WHERE pf.status = 'FAIL' + AND pf.check_id IN [ + 'iam_inline_policy_allows_privilege_escalation', + 'iam_role_administratoraccess_policy', + 'iam_inline_policy_no_administrative_privileges', + 'iam_user_administrator_access_policy' + ] +WITH DISTINCT idp, role, pf +LIMIT 60 +MATCH path_root = (acct:AWSAccount)--(role) +MATCH p_trust = (idp)-[:TRUSTS_AWS_PRINCIPAL]-(role) +MATCH p_find = (role)-[:HAS_FINDING]-(pf) +RETURN path_root, p_trust, p_find +``` + +**How it's built:** a plain "list every flagged identity" query is a wide fan that draws as a column, and `ProwlerFinding` nodes accumulate across scans with no scan filter available in custom queries. + +- The first MATCH plus `WHERE` keeps only roles trusted by a SAML or OIDC provider (trust edge undirected, so direction does not matter). +- The second MATCH plus `check_id IN [...]` keeps only those carrying one of the four privilege-escalation or admin checks. +- `WITH DISTINCT ... LIMIT 60` collapses duplicate finding nodes and hard-caps the result. +- `p_trust`, `p_find`, and `path_root` draw it connected three ways: provider to role through the trust edge, role to its finding, and role to the account. +- The previous scenario shows who can walk in; this shows which of those roles Prowler already flags as over-privileged. + +#### 7. World-readable S3 buckets + +**Query story:** Unlike the IAM-gated sensitive bucket in scenarios 1 and 2, these buckets are open to anyone on the internet with no credentials at all. + +```cypher +MATCH path_s3 = (acct:AWSAccount)--(s3:S3Bucket) +WHERE s3.anonymous_access = true +OPTIONAL MATCH p = (s3)--(stmt:S3PolicyStatement) +RETURN path_s3, p +``` + +**How it's built:** the counterpoint to scenarios 1 and 2 — there the sensitive bucket is reachable only through an IAM role chain; here the bucket needs no role at all. + +- `path_s3` connects each public bucket to its account node so they draw connected. Cartography sets `anonymous_access = true` when a bucket's policy or ACL allows public access. +- `p` is an optional match that pulls in the `S3PolicyStatement` granting the access where one exists, so the public grant is visible next to the bucket. Buckets that are public via ACL only still show, connected to the account. + +#### 8. Internet exposure surface + +**Query story:** The raw external attack surface behind scenarios 1 and 3: every internet-exposed EC2 instance with its security groups and the exact inbound ports left open. + +```cypher +MATCH path_ec2 = (acct:AWSAccount)--(ec2:EC2Instance) +WHERE ec2.exposed_internet = true +MATCH p1 = (ec2)--(sg:EC2SecurityGroup)--(rule:IpPermissionInbound) +OPTIONAL MATCH path_net = (internet:Internet)-[:CAN_ACCESS]->(ec2) +OPTIONAL MATCH p2 = (ec2)-[:INSTANCE_PROFILE]->(:AWSInstanceProfile)-[:ASSOCIATED_WITH]->(:AWSRole) +RETURN path_net, path_ec2, p1, p2 +``` + +**How it's built:** `exposed_internet = true` is Cartography's computed reachability flag. + +- `path_ec2` hubs all exposed instances on the account node so they draw as one picture. +- `p1` joins each instance to its security groups and inbound rules so the open ports are on screen. +- `path_net` adds the optional `Internet -[:CAN_ACCESS]->` edge so the external reachability is explicit. +- `p2` optionally adds the instance role, which connects this surface view back to the kill chains in scenarios 1 and 3. + ### Tips for Writing Queries - Start small with `LIMIT` to inspect the shape of the data before broadening the pattern. diff --git a/docs/user-guide/tutorials/prowler-app-multi-tenant.mdx b/docs/user-guide/tutorials/prowler-app-multi-tenant.mdx index 26f73000b3..b353bc85c5 100644 --- a/docs/user-guide/tutorials/prowler-app-multi-tenant.mdx +++ b/docs/user-guide/tutorials/prowler-app-multi-tenant.mdx @@ -67,7 +67,14 @@ The currently active organization is indicated by an **Active** badge. Switching ## Editing an Organization Name -Organization owners with the **Manage Account** permission can rename an organization: +Renaming an organization requires **both** of the following conditions to be met: + +* The user's **membership role** in that organization must be `owner` (visible as the `owner` badge in the Organizations card). +* The user must have a role that grants the **Manage Account** permission. + +Users who only meet one of the two conditions will not see the **Edit** button. For example, a user whose membership role is `member` will not see the **Edit** button even if their role grants `Manage Account`. + +To rename an organization: 1. Navigate to the **Profile** page. @@ -175,6 +182,6 @@ If the expelled organization was the user's **only** organization, the account i | View organizations | Any authenticated user | | Create an organization | Any authenticated user | | Switch organizations | Any authenticated user | -| Edit organization name | Organization owner with **Manage Account** permission | -| Delete an organization | Organization owner with **Manage Account** permission; must belong to more than one organization | +| Edit organization name | Membership role `owner` **and** a role with **Manage Account** permission | +| Delete an organization | Membership role `owner` **and** a role with **Manage Account** permission; must belong to more than one organization | | Expel a user from an organization | Organization owner (no additional permission required); last remaining owner cannot expel themselves | diff --git a/docs/user-guide/tutorials/prowler-app-rbac.mdx b/docs/user-guide/tutorials/prowler-app-rbac.mdx index 31fd4a730e..cccbbc27cc 100644 --- a/docs/user-guide/tutorials/prowler-app-rbac.mdx +++ b/docs/user-guide/tutorials/prowler-app-rbac.mdx @@ -123,7 +123,7 @@ The Roles section in Prowler is designed to facilitate the assignment of custom
### Provider Groups -Provider Groups control visibility across specific providers. When creating a new role, you can assign specific groups to define their Cloud Provider visibility. This ensures that users with that role have access only to the Cloud Providers that are required. +Provider Groups control visibility across specific providers. When creating a new role, you can assign specific groups to define their Provider visibility. This ensures that users with that role have access only to the Providers that are required. By default, a new user role does not have visibility into any group. @@ -223,7 +223,7 @@ Assign administrative permissions by selecting from the following options: | Invite and Manage Users | All | Invite new users and manage existing ones. | | Manage Account | All | Adjust account settings, delete users and read/manage users permissions. | | Manage Scans | All | Run and review scans. | -| Manage Cloud Providers | All | Add or modify connected cloud providers. | +| Manage Providers | All | Add or modify connected providers. | | Manage Integrations | All | Add or modify the Prowler Integrations. | | Manage Ingestions | Prowler Cloud | Allow or deny the ability to submit findings ingestion batches via the API. | | Manage Billing | Prowler Cloud | Access and manage billing settings and subscription information. | diff --git a/docs/user-guide/tutorials/prowler-app-s3-integration.mdx b/docs/user-guide/tutorials/prowler-app-s3-integration.mdx index 728e4a4354..284b10eaaf 100644 --- a/docs/user-guide/tutorials/prowler-app-s3-integration.mdx +++ b/docs/user-guide/tutorials/prowler-app-s3-integration.mdx @@ -320,7 +320,7 @@ Once the required permissions are set up, proceed to configure the S3 integratio ![Add integration button](/images/prowler-app/s3/s3-integration-ui-3.png) 4. Complete the configuration form with the following details: - - **Cloud Providers:** Select the providers whose scan results should be exported to this S3 bucket + - **Providers:** Select the providers whose scan results should be exported to this S3 bucket - **Bucket Name:** Enter the name of the target S3 bucket (e.g., `my-security-findings-bucket`) - **Output Directory:** Specify the directory path within the bucket (e.g., `/prowler-findings/`, defaults to `output`) diff --git a/docs/user-guide/tutorials/prowler-app.mdx b/docs/user-guide/tutorials/prowler-app.mdx index 0b368c5ad4..5e99a41ae7 100644 --- a/docs/user-guide/tutorials/prowler-app.mdx +++ b/docs/user-guide/tutorials/prowler-app.mdx @@ -72,8 +72,8 @@ To perform security scans, link a cloud provider account. Prowler supports the f Steps to add a provider: -1. Navigate to `Settings > Cloud Providers`. -2. Click `Add Account` to set up a new provider and provide your credentials. +1. Navigate to `Settings > Providers`. +2. Click `Add Provider` to set up a new provider and provide your credentials. Add Provider diff --git a/docs/user-guide/tutorials/prowler-cloud-aws-organizations.mdx b/docs/user-guide/tutorials/prowler-cloud-aws-organizations.mdx index e56627ee20..d9c17aaa5f 100644 --- a/docs/user-guide/tutorials/prowler-cloud-aws-organizations.mdx +++ b/docs/user-guide/tutorials/prowler-cloud-aws-organizations.mdx @@ -246,10 +246,10 @@ Now that both roles are deployed — the management account role (Step 1) and th ### Open the Wizard -1. Navigate to **Cloud Providers** and click **Add Cloud Provider**. +1. Navigate to **Providers** and click **Add Provider**. - Cloud Providers page showing the Add Cloud Provider button + Providers page showing the Add Provider button 2. Select **Amazon Web Services** as the provider. diff --git a/docs/user-guide/tutorials/v2_to_v3_checks_mapping.mdx b/docs/user-guide/tutorials/v2_to_v3_checks_mapping.mdx index 4262a22be7..04c00bee17 100644 --- a/docs/user-guide/tutorials/v2_to_v3_checks_mapping.mdx +++ b/docs/user-guide/tutorials/v2_to_v3_checks_mapping.mdx @@ -2,7 +2,7 @@ Prowler v3 and v4 introduce distinct identifiers while preserving the checks originally implemented in v2. This change was made because, in previous versions, check names were primarily derived from the CIS Benchmark for AWS. Starting with v3 and v4, all checks are independent of any security framework and have unique names and IDs. -For more details on the updated compliance implementation in Prowler v4 and v3, refer to the [Compliance](/user-guide/cli/tutorials/compliance) section. +For more details on the updated compliance implementation in Prowler v4 and v3, refer to the [Compliance](/user-guide/compliance/tutorials/compliance) section. ``` checks_v4_v3_to_v2_mapping = { diff --git a/mcp_server/AGENTS.md b/mcp_server/AGENTS.md index c8f77bd4b1..a82cc42e33 100644 --- a/mcp_server/AGENTS.md +++ b/mcp_server/AGENTS.md @@ -2,7 +2,7 @@ > **Skills Reference**: See [`prowler-mcp`](../skills/prowler-mcp/SKILL.md) -### Auto-invoke Skills +## Auto-invoke Skills When performing these actions, ALWAYS invoke the corresponding skill FIRST: @@ -68,7 +68,7 @@ Python 3.12+ | FastMCP 2.13.1 | httpx (async) | Pydantic | uv ## PROJECT STRUCTURE -``` +```text mcp_server/prowler_mcp_server/ ├── server.py # Main orchestration ├── prowler_hub/server.py # Hub tools (no auth) diff --git a/mcp_server/CHANGELOG.md b/mcp_server/CHANGELOG.md index 64ea033f0e..8f1438a3bb 100644 --- a/mcp_server/CHANGELOG.md +++ b/mcp_server/CHANGELOG.md @@ -2,6 +2,34 @@ All notable changes to the **Prowler MCP Server** are documented in this file. +## [0.7.2] (Prowler v5.28.1) + +### 🐞 Fixed + +- Preserve authorization header in HTTP mode [(#11366)](https://github.com/prowler-cloud/prowler/pull/11366) + +--- + +## [0.7.1] (Prowler v5.28.0) + +### 🔐 Security + +- `fastmcp` from 2.14.0 to 3.2.4 for GHSA-5h2m-4q8j-pqpj, GHSA-rww4-4w9c-7733, and GHSA-vv7q-7jx5-f767, which also pulls fixed `jaraco.context`, `python-multipart`, `starlette`, and drops the vulnerable `lupa`/`urllib3` transitive deps [(#11284)](https://github.com/prowler-cloud/prowler/pull/11284) + +--- + +## [0.7.0] (Prowler v5.27.0) + +### 🚀 Added + +- Finding Groups tools [(#11140)](https://github.com/prowler-cloud/prowler/pull/11140) + +### 🔐 Security + +- `cryptography` from 46.0.1 to 47.0.0 (transitive) for CVE-2026-39892 and CVE-2026-26007 / CVE-2026-34073 [(#10978)](https://github.com/prowler-cloud/prowler/pull/10978) + +--- + ## [0.6.0] (Prowler v5.23.0) ### 🚀 Added @@ -24,6 +52,8 @@ All notable changes to the **Prowler MCP Server** are documented in this file. - Attack Path tool to get Neo4j DB schema [(#10321)](https://github.com/prowler-cloud/prowler/pull/10321) +--- + ## [0.4.0] (Prowler v5.19.0) ### 🚀 Added diff --git a/mcp_server/README.md b/mcp_server/README.md index 41c5271d61..e990f0f363 100644 --- a/mcp_server/README.md +++ b/mcp_server/README.md @@ -10,6 +10,7 @@ Full access to Prowler Cloud platform and self-managed Prowler App for: - **Findings Analysis**: Query, filter, and analyze security findings across all your cloud environments +- **Finding Groups Analysis**: Triage findings grouped by check ID and drill down into affected resources - **Provider Management**: Create, configure, and manage your configured Prowler providers (AWS, Azure, GCP, etc.) - **Scan Orchestration**: Trigger on-demand scans and schedule recurring security assessments - **Resource Inventory**: Search and view detailed information about your audited resources @@ -56,13 +57,21 @@ Prowler MCP Server can be used in three ways: - Managed and maintained by Prowler team - Always up-to-date +Install a reviewed version of `mcp-remote` in a dedicated local workspace first. Avoid running `npx mcp-remote` directly because it can download and execute a new package version on each run. + +```bash +mkdir -p ~/.local/share/prowler-mcp-bridge +cd ~/.local/share/prowler-mcp-bridge +npm init -y +npm install --save-exact mcp-remote@0.1.38 +``` + ```json { "mcpServers": { "prowler": { - "command": "npx", + "command": "/absolute/path/to/.local/share/prowler-mcp-bridge/node_modules/.bin/mcp-remote", "args": [ - "mcp-remote", "https://mcp.prowler.com/mcp", "--header", "Authorization: Bearer pk_YOUR_API_KEY_HERE" @@ -74,14 +83,14 @@ Prowler MCP Server can be used in three ways: ### 2. Local STDIO Mode -**Run the server locally on your machine** +Run the server locally on your machine: - Runs as a subprocess of your MCP client - Requires Python 3.12+ or Docker ### 3. Self-Hosted HTTP Mode -**Deploy your own remote MCP server** +Deploy your own remote MCP server: - Full control over deployment - Requires Python 3.12+ or Docker @@ -123,7 +132,7 @@ All tools follow a consistent naming pattern with prefixes: ## Architecture -``` +```text prowler_mcp_server/ ├── server.py # Main orchestrator (imports sub-servers with prefixes) ├── main.py # CLI entry point @@ -145,17 +154,20 @@ prowler_mcp_server/ The Prowler MCP Server enables powerful workflows through AI assistants: -**Security Operations** +### Security Operations + - "Show me all critical findings from my AWS production accounts" - "Register my new AWS account in Prowler and run a scheduled scan every day" - "List all muted findings and detect what findgings are muted by a not enough good reason in relation to their severity" -**Security Research** +### Security Research + - "Explain what the S3 bucket public access Prowler check does" - "Find all Prowler checks related to encryption at rest" - "What is the latest version of the CIS that Prowler is covering per provider?" -**Documentation & Learning** +### Documentation & Learning + - "How do I configure Prowler to scan my GCP organization?" - "What authentication methods does Prowler support for Azure?" - "How can I contribute with a new security check to Prowler?" diff --git a/mcp_server/prowler_mcp_server/prowler_app/models/finding_groups.py b/mcp_server/prowler_mcp_server/prowler_app/models/finding_groups.py new file mode 100644 index 0000000000..9ff9822977 --- /dev/null +++ b/mcp_server/prowler_mcp_server/prowler_app/models/finding_groups.py @@ -0,0 +1,300 @@ +"""Pydantic models for Prowler Finding Groups responses.""" + +from typing import Literal + +from pydantic import Field + +from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin + + +FindingStatus = Literal["FAIL", "PASS", "MANUAL"] +FindingSeverity = Literal["critical", "high", "medium", "low", "informational"] +FindingDelta = Literal["new", "changed"] + + +def _attributes(data: dict) -> dict: + return data.get("attributes", {}) + + +def _counter(attributes: dict, key: str) -> int: + return attributes.get(key) or 0 + + +def _simplified_group_kwargs(data: dict) -> dict: + attributes = _attributes(data) + return { + "check_id": attributes.get("check_id", data.get("id", "")), + "check_title": attributes.get("check_title"), + "severity": attributes.get("severity", "informational"), + "status": attributes.get("status", "MANUAL"), + "muted": attributes.get("muted", False), + "impacted_providers": attributes.get("impacted_providers") or [], + "resources_fail": _counter(attributes, "resources_fail"), + "resources_total": _counter(attributes, "resources_total"), + "pass_count": _counter(attributes, "pass_count"), + "fail_count": _counter(attributes, "fail_count"), + "manual_count": _counter(attributes, "manual_count"), + "muted_count": _counter(attributes, "muted_count"), + "new_count": _counter(attributes, "new_count"), + "changed_count": _counter(attributes, "changed_count"), + "first_seen_at": attributes.get("first_seen_at"), + "last_seen_at": attributes.get("last_seen_at"), + "failing_since": attributes.get("failing_since"), + } + + +class SimplifiedFindingGroup(MinimalSerializerMixin): + """Finding group summary optimized for browsing many checks.""" + + check_id: str = Field(description="Public check ID that identifies this group") + check_title: str | None = Field( + default=None, description="Human-readable check title" + ) + severity: FindingSeverity = Field(description="Highest severity in the group") + status: FindingStatus = Field(description="Aggregated finding group status") + muted: bool = Field( + description="Whether all findings in this group are muted or accepted" + ) + impacted_providers: list[str] = Field( + default_factory=list, + description="Provider types impacted by this finding group", + ) + resources_fail: int = Field( + description="Number of non-muted failing resources in this group", ge=0 + ) + resources_total: int = Field( + description="Total number of resources in this group", ge=0 + ) + pass_count: int = Field( + description="Number of non-muted PASS findings in this group", ge=0 + ) + fail_count: int = Field( + description="Number of non-muted FAIL findings in this group", ge=0 + ) + manual_count: int = Field( + description="Number of non-muted MANUAL findings in this group", ge=0 + ) + muted_count: int = Field(description="Total muted findings in this group", ge=0) + new_count: int = Field(description="Number of new non-muted findings", ge=0) + changed_count: int = Field( + description="Number of changed non-muted findings", ge=0 + ) + first_seen_at: str | None = Field( + default=None, description="First time this group was detected" + ) + last_seen_at: str | None = Field( + default=None, description="Last time this group was detected" + ) + failing_since: str | None = Field( + default=None, description="First time this group started failing" + ) + + @classmethod + def from_api_response(cls, data: dict) -> "SimplifiedFindingGroup": + """Transform JSON:API finding group response to simplified format.""" + return cls(**_simplified_group_kwargs(data)) + + +class DetailedFindingGroup(SimplifiedFindingGroup): + """Finding group with complete counters and descriptive context.""" + + check_description: str | None = Field( + default=None, description="Description of the check behind this group" + ) + pass_muted_count: int = Field(description="Muted PASS findings", ge=0) + fail_muted_count: int = Field(description="Muted FAIL findings", ge=0) + manual_muted_count: int = Field(description="Muted MANUAL findings", ge=0) + new_fail_count: int = Field(description="New non-muted FAIL findings", ge=0) + new_fail_muted_count: int = Field(description="New muted FAIL findings", ge=0) + new_pass_count: int = Field(description="New non-muted PASS findings", ge=0) + new_pass_muted_count: int = Field(description="New muted PASS findings", ge=0) + new_manual_count: int = Field(description="New non-muted MANUAL findings", ge=0) + new_manual_muted_count: int = Field( + description="New muted MANUAL findings", ge=0 + ) + changed_fail_count: int = Field( + description="Changed non-muted FAIL findings", ge=0 + ) + changed_fail_muted_count: int = Field( + description="Changed muted FAIL findings", ge=0 + ) + changed_pass_count: int = Field( + description="Changed non-muted PASS findings", ge=0 + ) + changed_pass_muted_count: int = Field( + description="Changed muted PASS findings", ge=0 + ) + changed_manual_count: int = Field( + description="Changed non-muted MANUAL findings", ge=0 + ) + changed_manual_muted_count: int = Field( + description="Changed muted MANUAL findings", ge=0 + ) + + @classmethod + def from_api_response(cls, data: dict) -> "DetailedFindingGroup": + """Transform JSON:API finding group response to detailed format.""" + attributes = _attributes(data) + + return cls( + **_simplified_group_kwargs(data), + check_description=attributes.get("check_description"), + pass_muted_count=_counter(attributes, "pass_muted_count"), + fail_muted_count=_counter(attributes, "fail_muted_count"), + manual_muted_count=_counter(attributes, "manual_muted_count"), + new_fail_count=_counter(attributes, "new_fail_count"), + new_fail_muted_count=_counter(attributes, "new_fail_muted_count"), + new_pass_count=_counter(attributes, "new_pass_count"), + new_pass_muted_count=_counter(attributes, "new_pass_muted_count"), + new_manual_count=_counter(attributes, "new_manual_count"), + new_manual_muted_count=_counter(attributes, "new_manual_muted_count"), + changed_fail_count=_counter(attributes, "changed_fail_count"), + changed_fail_muted_count=_counter(attributes, "changed_fail_muted_count"), + changed_pass_count=_counter(attributes, "changed_pass_count"), + changed_pass_muted_count=_counter(attributes, "changed_pass_muted_count"), + changed_manual_count=_counter(attributes, "changed_manual_count"), + changed_manual_muted_count=_counter( + attributes, "changed_manual_muted_count" + ), + ) + + +class FindingGroupsListResponse(MinimalSerializerMixin): + """Paginated response for finding group list queries.""" + + groups: list[SimplifiedFindingGroup] = Field( + description="Finding groups matching the query" + ) + total_num_groups: int = Field( + description="Total groups matching the query across all pages", ge=0 + ) + total_num_pages: int = Field(description="Total pages available", ge=0) + current_page: int = Field(description="Current page number", ge=1) + + @classmethod + def from_api_response(cls, response: dict) -> "FindingGroupsListResponse": + """Transform JSON:API list response to simplified format.""" + pagination = response.get("meta", {}).get("pagination", {}) + groups = [ + SimplifiedFindingGroup.from_api_response(item) + for item in response.get("data", []) + ] + + return cls( + groups=groups, + total_num_groups=pagination.get("count", len(groups)), + total_num_pages=pagination.get("pages", 1), + current_page=pagination.get("page", 1), + ) + + +class FindingGroupResourceInfo(MinimalSerializerMixin): + """Nested resource information for a finding group row.""" + + uid: str = Field(description="Provider-native resource UID") + name: str = Field(description="Resource name") + service: str = Field(description="Cloud service") + region: str = Field(description="Cloud region") + type: str = Field(description="Resource type") + resource_group: str | None = Field( + default=None, description="Provider resource group or equivalent" + ) + + @classmethod + def from_api_response(cls, data: dict) -> "FindingGroupResourceInfo": + """Transform nested resource data to simplified format.""" + return cls( + uid=data.get("uid", ""), + name=data.get("name", ""), + service=data.get("service", ""), + region=data.get("region", ""), + type=data.get("type", ""), + resource_group=data.get("resource_group"), + ) + + +class FindingGroupProviderInfo(MinimalSerializerMixin): + """Nested provider information for a finding group resource row.""" + + type: str = Field(description="Provider type") + uid: str = Field(description="Provider-native account or subscription ID") + alias: str | None = Field(default=None, description="Provider alias") + + @classmethod + def from_api_response(cls, data: dict) -> "FindingGroupProviderInfo": + """Transform nested provider data to simplified format.""" + return cls( + type=data.get("type", ""), + uid=data.get("uid", ""), + alias=data.get("alias"), + ) + + +class FindingGroupResource(MinimalSerializerMixin): + """Resource row affected by a finding group.""" + + id: str = Field(description="Row identifier for this finding group resource") + resource: FindingGroupResourceInfo = Field(description="Affected resource") + provider: FindingGroupProviderInfo = Field(description="Affected provider") + finding_id: str = Field( + description="Finding UUID to use with prowler_app_get_finding_details" + ) + status: FindingStatus = Field(description="Finding status for this resource") + severity: FindingSeverity = Field(description="Finding severity") + muted: bool = Field(description="Whether the finding is muted") + delta: FindingDelta | None = Field(default=None, description="Change status") + first_seen_at: str | None = Field(default=None, description="First seen time") + last_seen_at: str | None = Field(default=None, description="Last seen time") + muted_reason: str | None = Field(default=None, description="Mute reason") + + @classmethod + def from_api_response(cls, data: dict) -> "FindingGroupResource": + """Transform JSON:API finding group resource response.""" + attributes = _attributes(data) + + return cls( + id=data.get("id", ""), + resource=FindingGroupResourceInfo.from_api_response( + attributes.get("resource") or {} + ), + provider=FindingGroupProviderInfo.from_api_response( + attributes.get("provider") or {} + ), + finding_id=str(attributes.get("finding_id", "")), + status=attributes.get("status", "MANUAL"), + severity=attributes.get("severity", "informational"), + muted=attributes.get("muted", False), + delta=attributes.get("delta"), + first_seen_at=attributes.get("first_seen_at"), + last_seen_at=attributes.get("last_seen_at"), + muted_reason=attributes.get("muted_reason"), + ) + + +class FindingGroupResourcesListResponse(MinimalSerializerMixin): + """Paginated response for finding group resource queries.""" + + resources: list[FindingGroupResource] = Field( + description="Resources matching the finding group query" + ) + total_num_resources: int = Field( + description="Total resources matching the query across all pages", ge=0 + ) + total_num_pages: int = Field(description="Total pages available", ge=0) + current_page: int = Field(description="Current page number", ge=1) + + @classmethod + def from_api_response(cls, response: dict) -> "FindingGroupResourcesListResponse": + """Transform JSON:API resource list response to simplified format.""" + pagination = response.get("meta", {}).get("pagination", {}) + resources = [ + FindingGroupResource.from_api_response(item) + for item in response.get("data", []) + ] + + return cls( + resources=resources, + total_num_resources=pagination.get("count", len(resources)), + total_num_pages=pagination.get("pages", 1), + current_page=pagination.get("page", 1), + ) diff --git a/mcp_server/prowler_mcp_server/prowler_app/tools/finding_groups.py b/mcp_server/prowler_mcp_server/prowler_app/tools/finding_groups.py new file mode 100644 index 0000000000..363e45d970 --- /dev/null +++ b/mcp_server/prowler_mcp_server/prowler_app/tools/finding_groups.py @@ -0,0 +1,473 @@ +"""Finding Groups tools for Prowler App MCP Server. + +This module provides read-only tools for finding group triage and drill-downs. +""" + +from typing import Any, Literal +from urllib.parse import quote + +from pydantic import Field + +from prowler_mcp_server.prowler_app.models.finding_groups import ( + DetailedFindingGroup, + FindingGroupResourcesListResponse, + FindingGroupsListResponse, +) +from prowler_mcp_server.prowler_app.tools.base import BaseTool + + +StatusFilter = Literal["FAIL", "PASS", "MANUAL"] +SeverityFilter = Literal["critical", "high", "medium", "low", "informational"] +DeltaFilter = Literal["new", "changed"] + +GROUP_DETAIL_FIELDS = ( + "check_id,check_title,check_description,severity,status,muted," + "impacted_providers,resources_fail,resources_total,pass_count,fail_count," + "manual_count,pass_muted_count,fail_muted_count,manual_muted_count," + "muted_count,new_count,changed_count,new_fail_count,new_fail_muted_count," + "new_pass_count,new_pass_muted_count,new_manual_count,new_manual_muted_count," + "changed_fail_count,changed_fail_muted_count,changed_pass_count," + "changed_pass_muted_count,changed_manual_count,changed_manual_muted_count," + "first_seen_at,last_seen_at,failing_since" +) + +GROUP_LIST_FIELDS = ( + "check_id,check_title,severity,status,muted,impacted_providers," + "resources_fail,resources_total,pass_count,fail_count,manual_count," + "muted_count,new_count,changed_count,first_seen_at,last_seen_at,failing_since" +) + +RESOURCE_FIELDS = ( + "resource,provider,finding_id,status,severity,muted,delta," + "first_seen_at,last_seen_at,muted_reason" +) + + +class FindingGroupsTools(BaseTool): + """Tools for Finding Groups operations.""" + + @staticmethod + def _bool_value(value: bool | str) -> bool: + """Normalize bool-like MCP client values.""" + if isinstance(value, bool): + return value + return value.lower() == "true" + + @staticmethod + def _group_endpoint(date_range: tuple[str, str] | None) -> str: + return "/finding-groups/latest" if date_range is None else "/finding-groups" + + @staticmethod + def _resource_endpoint(check_id: str, date_range: tuple[str, str] | None) -> str: + escaped_check_id = quote(check_id, safe="") + if date_range is None: + return f"/finding-groups/latest/{escaped_check_id}/resources" + return f"/finding-groups/{escaped_check_id}/resources" + + def _base_date_params( + self, date_from: str | None, date_to: str | None + ) -> tuple[tuple[str, str] | None, dict[str, Any]]: + date_range = self.api_client.normalize_date_range( + date_from, date_to, max_days=2 + ) + if date_range is None: + return None, {} + + return date_range, { + "filter[inserted_at__gte]": date_range[0], + "filter[inserted_at__lte]": date_range[1], + } + + def _apply_common_filters( + self, + params: dict[str, Any], + provider: list[str], + provider_type: list[str], + provider_uid: list[str], + provider_alias: str | None, + region: list[str], + service: list[str], + resource_type: list[str], + resource_name: str | None, + resource_uid: str | None, + resource_group: list[str], + category: list[str], + check_id: list[str], + check_title: str | None, + severity: list[SeverityFilter], + status: list[StatusFilter], + muted: bool | str | None, + delta: list[DeltaFilter], + ) -> None: + if provider: + params["filter[provider__in]"] = provider + if provider_type: + params["filter[provider_type__in]"] = provider_type + if provider_uid: + params["filter[provider_uid__in]"] = provider_uid + if provider_alias: + params["filter[provider_alias__icontains]"] = provider_alias + if region: + params["filter[region__in]"] = region + if service: + params["filter[service__in]"] = service + if resource_type: + params["filter[resource_type__in]"] = resource_type + if resource_name: + params["filter[resource_name__icontains]"] = resource_name + if resource_uid: + params["filter[resource_uid__icontains]"] = resource_uid + if resource_group: + params["filter[resource_groups__in]"] = resource_group + if category: + params["filter[category__in]"] = category + if check_id: + params["filter[check_id__in]"] = check_id + if check_title: + params["filter[check_title__icontains]"] = check_title + if severity: + params["filter[severity__in]"] = severity + if status: + params["filter[status__in]"] = status + if muted is not None: + params["filter[muted]"] = self._bool_value(muted) + if delta: + params["filter[delta__in]"] = delta + + async def list_finding_groups( + self, + provider: list[str] = Field( + default=[], + description="Filter by provider UUIDs. Multiple values allowed. If empty, all visible providers are returned.", + ), + provider_type: list[str] = Field( + default=[], + description="Filter by provider type. Multiple values allowed, such as aws, azure, gcp, kubernetes, github, or m365.", + ), + provider_uid: list[str] = Field( + default=[], + description="Filter by provider-native account, subscription, or project IDs. Multiple values allowed.", + ), + provider_alias: str | None = Field( + default=None, + description="Filter by provider alias/name using partial matching.", + ), + region: list[str] = Field( + default=[], + description="Filter by cloud regions. Multiple values allowed.", + ), + service: list[str] = Field( + default=[], + description="Filter by cloud services. Multiple values allowed.", + ), + resource_type: list[str] = Field( + default=[], + description="Filter by resource types. Multiple values allowed.", + ), + resource_name: str | None = Field( + default=None, + description="Filter by resource name using partial matching.", + ), + resource_uid: str | None = Field( + default=None, + description="Filter by resource UID using partial matching.", + ), + resource_group: list[str] = Field( + default=[], + description="Filter by resource group values. Multiple values allowed.", + ), + category: list[str] = Field( + default=[], + description="Filter by finding categories. Multiple values allowed.", + ), + check_id: list[str] = Field( + default=[], + description="Filter by check IDs. Multiple values allowed.", + ), + check_title: str | None = Field( + default=None, + description="Filter by check title using partial matching.", + ), + severity: list[SeverityFilter] = Field( + default=[], + description="Filter by aggregated severity. Empty returns all severities.", + ), + status: list[StatusFilter] = Field( + default=["FAIL"], + description="Filter by aggregated status. Default returns failing groups. Pass [] to return all statuses.", + ), + muted: bool | str | None = Field( + default=None, + description="Filter by fully muted group state. Accepts true/false.", + ), + include_muted: bool | str = Field( + default=False, + description="When false, excludes fully muted groups. Set true to include fully muted groups.", + ), + delta: list[DeltaFilter] = Field( + default=[], + description="Filter by group delta values: new or changed.", + ), + date_from: str | None = Field( + default=None, + description="Start date for historical query in YYYY-MM-DD format. Maximum range is 2 days.", + ), + date_to: str | None = Field( + default=None, + description="End date for historical query in YYYY-MM-DD format. Maximum range is 2 days.", + ), + sort: str | None = Field( + default=None, + description="Optional sort expression supported by the finding-groups API, such as -fail_count,-severity,check_id.", + ), + page_size: int = Field( + default=50, description="Number of groups to return per page" + ), + page_number: int = Field( + default=1, description="Page number to retrieve (1-indexed)" + ), + ) -> dict[str, Any]: + """List finding groups aggregated by check ID. + + Default behavior returns the latest non-muted FAIL groups for fast triage. + Without dates this uses `/finding-groups/latest`. With `date_from` or + `date_to`, this uses `/finding-groups` with a maximum 2-day date window. + + Use this tool to find noisy or high-impact checks, then call + prowler_app_get_finding_group_details for complete counters or + prowler_app_list_finding_group_resources to drill into affected resources. + """ + try: + self.api_client.validate_page_size(page_size) + date_range, params = self._base_date_params(date_from, date_to) + endpoint = self._group_endpoint(date_range) + + self._apply_common_filters( + params, + provider, + provider_type, + provider_uid, + provider_alias, + region, + service, + resource_type, + resource_name, + resource_uid, + resource_group, + category, + check_id, + check_title, + severity, + status, + muted, + delta, + ) + + params["filter[include_muted]"] = self._bool_value(include_muted) + params["page[size]"] = page_size + params["page[number]"] = page_number + params["fields[finding-groups]"] = GROUP_LIST_FIELDS + if sort: + params["sort"] = sort + + clean_params = self.api_client.build_filter_params(params) + api_response = await self.api_client.get(endpoint, params=clean_params) + response = FindingGroupsListResponse.from_api_response(api_response) + return response.model_dump() + except Exception as e: + self.logger.error(f"Error listing finding groups: {e}") + return {"error": str(e), "status": "failed"} + + async def get_finding_group_details( + self, + check_id: str = Field( + description="Public check ID that identifies the finding group. This is not a UUID." + ), + date_from: str | None = Field( + default=None, + description="Start date for historical query in YYYY-MM-DD format. Maximum range is 2 days.", + ), + date_to: str | None = Field( + default=None, + description="End date for historical query in YYYY-MM-DD format. Maximum range is 2 days.", + ), + ) -> dict[str, Any]: + """Get complete details for one finding group by exact check ID. + + Uses `filter[check_id]` exact matching against latest data by default, + or historical data when dates are provided. Fully muted groups are + included by default so accepted risk does not look like a missing group. + """ + try: + date_range, params = self._base_date_params(date_from, date_to) + endpoint = self._group_endpoint(date_range) + + params.update( + { + "filter[check_id]": check_id, + "filter[include_muted]": True, + "page[size]": 1, + "page[number]": 1, + "fields[finding-groups]": GROUP_DETAIL_FIELDS, + } + ) + + clean_params = self.api_client.build_filter_params(params) + api_response = await self.api_client.get(endpoint, params=clean_params) + data = api_response.get("data", []) + + if not data: + return { + "error": f"Finding group '{check_id}' not found.", + "status": "not_found", + } + + group = DetailedFindingGroup.from_api_response(data[0]) + return group.model_dump() + except Exception as e: + self.logger.error(f"Error getting finding group details: {e}") + return {"error": str(e), "status": "failed"} + + async def list_finding_group_resources( + self, + check_id: str = Field( + description="Public check ID that identifies the finding group. This is not a UUID." + ), + provider: list[str] = Field( + default=[], + description="Filter by provider UUIDs. Multiple values allowed.", + ), + provider_type: list[str] = Field( + default=[], + description="Filter by provider type. Multiple values allowed.", + ), + provider_uid: list[str] = Field( + default=[], + description="Filter by provider-native account, subscription, or project IDs. Multiple values allowed.", + ), + provider_alias: str | None = Field( + default=None, + description="Filter by provider alias/name using partial matching.", + ), + region: list[str] = Field( + default=[], + description="Filter by cloud regions. Multiple values allowed.", + ), + service: list[str] = Field( + default=[], + description="Filter by cloud services. Multiple values allowed.", + ), + resource_type: list[str] = Field( + default=[], + description="Filter by resource types. Multiple values allowed.", + ), + resource_name: str | None = Field( + default=None, + description="Filter by resource name using partial matching.", + ), + resource_uid: str | None = Field( + default=None, + description="Filter by resource UID using partial matching.", + ), + resource_group: list[str] = Field( + default=[], + description="Filter by resource group values. Multiple values allowed.", + ), + category: list[str] = Field( + default=[], + description="Filter by finding categories. Multiple values allowed.", + ), + severity: list[SeverityFilter] = Field( + default=[], + description="Filter by severity. Empty returns all severities.", + ), + status: list[StatusFilter] = Field( + default=["FAIL"], + description="Filter by status. Default returns failing resources. Pass [] to return all statuses.", + ), + muted: bool | str | None = Field( + default=None, + description="Filter by muted state. Accepts true/false. Overrides include_muted when provided.", + ), + include_muted: bool | str = Field( + default=False, + description="When false, returns only actionable unmuted resources by applying muted=false. Set true to include muted and unmuted resources.", + ), + delta: list[DeltaFilter] = Field( + default=[], description="Filter by delta values: new or changed." + ), + date_from: str | None = Field( + default=None, + description="Start date for historical query in YYYY-MM-DD format. Maximum range is 2 days.", + ), + date_to: str | None = Field( + default=None, + description="End date for historical query in YYYY-MM-DD format. Maximum range is 2 days.", + ), + sort: str | None = Field( + default=None, + description="Optional sort expression supported by the finding group resources API.", + ), + page_size: int = Field( + default=50, description="Number of resources to return per page" + ), + page_number: int = Field( + default=1, description="Page number to retrieve (1-indexed)" + ), + ) -> dict[str, Any]: + """List resources affected by a finding group. + + Without dates this uses `/finding-groups/latest/{check_id}/resources`. + With `date_from` or `date_to`, this uses + `/finding-groups/{check_id}/resources` with a maximum 2-day date window. + + Default behavior returns FAIL, unmuted resources so the result is + actionable. Set `include_muted=True` to include accepted/suppressed + resources too. Each row includes nested resource and provider data plus + `finding_id`. Use `prowler_app_get_finding_details(finding_id)` to + retrieve complete remediation guidance for a specific resource finding. + """ + try: + self.api_client.validate_page_size(page_size) + date_range, params = self._base_date_params(date_from, date_to) + endpoint = self._resource_endpoint(check_id, date_range) + + if muted is None and not self._bool_value(include_muted): + muted = False + + self._apply_common_filters( + params, + provider, + provider_type, + provider_uid, + provider_alias, + region, + service, + resource_type, + resource_name, + resource_uid, + resource_group, + category, + [], + None, + severity, + status, + muted, + delta, + ) + + params["page[size]"] = page_size + params["page[number]"] = page_number + params["fields[finding-group-resources]"] = RESOURCE_FIELDS + if sort: + params["sort"] = sort + + clean_params = self.api_client.build_filter_params(params) + api_response = await self.api_client.get(endpoint, params=clean_params) + response = FindingGroupResourcesListResponse.from_api_response( + api_response + ) + return response.model_dump() + except Exception as e: + self.logger.error(f"Error listing finding group resources: {e}") + return {"error": str(e), "status": "failed"} diff --git a/mcp_server/prowler_mcp_server/prowler_app/utils/auth.py b/mcp_server/prowler_mcp_server/prowler_app/utils/auth.py index 32ab72e574..48535fb10a 100644 --- a/mcp_server/prowler_mcp_server/prowler_app/utils/auth.py +++ b/mcp_server/prowler_mcp_server/prowler_app/utils/auth.py @@ -5,6 +5,7 @@ from datetime import datetime from typing import Dict, Optional from fastmcp.server.dependencies import get_http_headers + from prowler_mcp_server import __version__ from prowler_mcp_server.lib.logger import logger @@ -68,7 +69,7 @@ class ProwlerAppAuth: async def authenticate(self) -> str: """Authenticate and return token (API key for STDIO, API key or JWT for HTTP).""" if self.mode == "http": - headers = get_http_headers() + headers = get_http_headers(include={"authorization"}) authorization_header = headers.get("authorization", None) if not authorization_header: diff --git a/mcp_server/prowler_mcp_server/server.py b/mcp_server/prowler_mcp_server/server.py index 12060366ad..20b7966b45 100644 --- a/mcp_server/prowler_mcp_server/server.py +++ b/mcp_server/prowler_mcp_server/server.py @@ -1,5 +1,3 @@ -import asyncio - from fastmcp import FastMCP from prowler_mcp_server import __version__ from prowler_mcp_server.lib.logger import logger @@ -8,55 +6,58 @@ from starlette.responses import JSONResponse prowler_mcp_server = FastMCP("prowler-mcp-server") -async def setup_main_server(): +def setup_main_server(): """Set up the main Prowler MCP server with all available integrations.""" - # Import Prowler Hub tools with prowler_hub_ prefix + # Mount Prowler Hub tools with prowler_hub_ namespace try: - logger.info("Importing Prowler Hub server...") + logger.info("Mounting Prowler Hub server...") from prowler_mcp_server.prowler_hub.server import hub_mcp_server - await prowler_mcp_server.import_server(hub_mcp_server, prefix="prowler_hub") - logger.info("Successfully imported Prowler Hub server") + prowler_mcp_server.mount(hub_mcp_server, namespace="prowler_hub") + logger.info("Successfully mounted Prowler Hub server") except Exception as e: - logger.error(f"Failed to import Prowler Hub server: {e}") + logger.error(f"Failed to mount Prowler Hub server: {e}") - # Import Prowler App tools with prowler_app_ prefix + # Mount Prowler App tools with prowler_app_ namespace try: - logger.info("Importing Prowler App server...") + logger.info("Mounting Prowler App server...") from prowler_mcp_server.prowler_app.server import app_mcp_server - await prowler_mcp_server.import_server(app_mcp_server, prefix="prowler_app") - logger.info("Successfully imported Prowler App server") + prowler_mcp_server.mount(app_mcp_server, namespace="prowler_app") + logger.info("Successfully mounted Prowler App server") except Exception as e: - logger.error(f"Failed to import Prowler App server: {e}") + logger.error(f"Failed to mount Prowler App server: {e}") - # Import Prowler Documentation tools with prowler_docs_ prefix + # Mount Prowler Documentation tools with prowler_docs_ namespace try: - logger.info("Importing Prowler Documentation server...") + logger.info("Mounting Prowler Documentation server...") from prowler_mcp_server.prowler_documentation.server import docs_mcp_server - await prowler_mcp_server.import_server(docs_mcp_server, prefix="prowler_docs") - logger.info("Successfully imported Prowler Documentation server") + prowler_mcp_server.mount(docs_mcp_server, namespace="prowler_docs") + logger.info("Successfully mounted Prowler Documentation server") except Exception as e: - logger.error(f"Failed to import Prowler Documentation server: {e}") + logger.error(f"Failed to mount Prowler Documentation server: {e}") -# Add health check endpoint +# Response follows the IETF Health Check Response Format +# (draft-inadarei-api-health-check-06). `version` is the contract version of +# this endpoint; `releaseId` is the package build version. @prowler_mcp_server.custom_route("/health", methods=["GET"]) -async def health_check(request) -> JSONResponse: +async def health_check(_request) -> JSONResponse: """Health check endpoint.""" return JSONResponse( - {"status": "healthy", "service": "prowler-mcp-server", "version": __version__} + { + "status": "pass", + "version": "1", + "releaseId": __version__, + "serviceId": "prowler-mcp-server", + "description": "Prowler MCP Server", + }, + media_type="application/health+json", + headers={"Cache-Control": "no-store"}, ) -# Get or create the event loop -try: - loop = asyncio.get_running_loop() - # If we have a running loop, schedule the setup as a task - loop.create_task(setup_main_server()) -except RuntimeError: - # No running loop, use asyncio.run (for standalone execution) - asyncio.run(setup_main_server()) +setup_main_server() app = prowler_mcp_server.http_app() diff --git a/mcp_server/pyproject.toml b/mcp_server/pyproject.toml index 2a6885fedb..154b835281 100644 --- a/mcp_server/pyproject.toml +++ b/mcp_server/pyproject.toml @@ -2,9 +2,16 @@ build-backend = "setuptools.build_meta" requires = ["setuptools>=61.0", "wheel"] +[dependency-groups] +dev = [ + "bandit==1.8.3", + "pytest==9.0.3", + "vulture==2.14" +] + [project] dependencies = [ - "fastmcp==2.14.0", + "fastmcp==3.2.4", "httpx==0.28.1" ] description = "MCP server for Prowler ecosystem" @@ -16,5 +23,8 @@ version = "0.5.0" [project.scripts] prowler-mcp = "prowler_mcp_server.main:main" +[tool.pytest.ini_options] +testpaths = ["tests"] + [tool.uv] package = true diff --git a/tests/providers/googleworkspace/services/gmail/__init__.py b/mcp_server/tests/__init__.py similarity index 100% rename from tests/providers/googleworkspace/services/gmail/__init__.py rename to mcp_server/tests/__init__.py diff --git a/mcp_server/tests/test_health.py b/mcp_server/tests/test_health.py new file mode 100644 index 0000000000..47a676960d --- /dev/null +++ b/mcp_server/tests/test_health.py @@ -0,0 +1,46 @@ +"""Tests for the Prowler MCP Server health endpoint.""" + +from starlette.testclient import TestClient + +from prowler_mcp_server import __version__ +from prowler_mcp_server.server import app + + +def test_health_returns_ietf_pass_response(): + """GET /health returns 200 with the IETF health-check body and headers.""" + client = TestClient(app) + + response = client.get("/health") + + assert response.status_code == 200 + assert response.headers["content-type"] == "application/health+json" + assert response.headers["cache-control"] == "no-store" + assert response.json() == { + "status": "pass", + "version": "1", + "releaseId": __version__, + "serviceId": "prowler-mcp-server", + "description": "Prowler MCP Server", + } + + +def test_health_release_id_matches_package_version(): + """The endpoint must surface the current package __version__ as releaseId. + + Drift between the response and the installed package would mislead any + monitoring tool that uses releaseId to identify the running build. + """ + client = TestClient(app) + + response = client.get("/health") + + assert response.json()["releaseId"] == __version__ + + +def test_health_rejects_non_get_methods(): + """The endpoint only exposes GET; other verbs return 405.""" + client = TestClient(app) + + response = client.post("/health") + + assert response.status_code == 405 diff --git a/mcp_server/uv.lock b/mcp_server/uv.lock index 5daf74c94a..fd0a9349f9 100644 --- a/mcp_server/uv.lock +++ b/mcp_server/uv.lock @@ -2,6 +2,18 @@ version = 1 revision = 3 requires-python = ">=3.12" +[[package]] +name = "aiofile" +version = "3.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "caio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/e2/d7cb819de8df6b5c1968a2756c3cb4122d4fa2b8fc768b53b7c9e5edb646/aiofile-3.9.0.tar.gz", hash = "sha256:e5ad718bb148b265b6df1b3752c4d1d83024b93da9bd599df74b9d9ffcf7919b", size = 17943, upload-time = "2024-10-08T10:39:35.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/25/da1f0b4dd970e52bf5a36c204c107e11a0c6d3ed195eba0bfbc664c312b2/aiofile-3.9.0-py3-none-any.whl", hash = "sha256:ce2f6c1571538cbdfa0143b04e16b208ecb0e9cb4148e528af8a640ed51cc8aa", size = 19539, upload-time = "2024-10-08T10:39:32.955Z" }, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -13,64 +25,100 @@ wheels = [ [[package]] name = "anyio" -version = "4.10.0" +version = "4.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, - { name = "sniffio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" }, + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, ] [[package]] name = "attrs" -version = "25.3.0" +version = "26.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] [[package]] name = "authlib" -version = "1.6.9" +version = "1.7.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, + { name = "joserfc" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/af/98/00d3dd826d46959ad8e32af2dbb2398868fd9fd0683c26e56d0789bd0e68/authlib-1.6.9.tar.gz", hash = "sha256:d8f2421e7e5980cc1ddb4e32d3f5fa659cfaf60d8eaf3281ebed192e4ab74f04", size = 165134, upload-time = "2026-03-02T07:44:01.998Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/98/7d93f30d029643c0275dbc0bd6d5a6f670661ee6c9a94d93af7ab4887600/authlib-1.7.2.tar.gz", hash = "sha256:2cea25fefcd4e7173bdf1372c0afc265c8034b23a8cd5dcb6a9164b826c64231", size = 176511, upload-time = "2026-05-06T08:10:23.116Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/53/23/b65f568ed0c22f1efacb744d2db1a33c8068f384b8c9b482b52ebdbc3ef6/authlib-1.6.9-py2.py3-none-any.whl", hash = "sha256:f08b4c14e08f0861dc18a32357b33fbcfd2ea86cfe3fe149484b4d764c4a0ac3", size = 244197, upload-time = "2026-03-02T07:44:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/fb/95/adcb68e20c34162e9135f370d6e31737719c2b6f94bc953fe7ed1f10fe21/authlib-1.7.2-py2.py3-none-any.whl", hash = "sha256:3e1faedc9d87e7d56a164eca3ccb6ace0d61b94abe83e92242f8dc8bba9b4a9f", size = 259548, upload-time = "2026-05-06T08:10:21.436Z" }, +] + +[[package]] +name = "bandit" +version = "1.8.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "stevedore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/a5/144a45f8e67df9d66c3bc3f7e69a39537db8bff1189ab7cff4e9459215da/bandit-1.8.3.tar.gz", hash = "sha256:f5847beb654d309422985c36644649924e0ea4425c76dec2e89110b87506193a", size = 4232005, upload-time = "2025-02-17T05:24:57.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/85/db74b9233e0aa27ec96891045c5e920a64dd5cbccd50f8e64e9460f48d35/bandit-1.8.3-py3-none-any.whl", hash = "sha256:28f04dc0d258e1dd0f99dee8eefa13d1cb5e3fde1a5ab0c523971f97b289bcd8", size = 129078, upload-time = "2025-02-17T05:24:54.068Z" }, ] [[package]] name = "beartype" -version = "0.22.6" +version = "0.22.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/88/e2/105ceb1704cb80fe4ab3872529ab7b6f365cf7c74f725e6132d0efcf1560/beartype-0.22.6.tar.gz", hash = "sha256:97fbda69c20b48c5780ac2ca60ce3c1bb9af29b3a1a0216898ffabdd523e48f4", size = 1588975, upload-time = "2025-11-20T04:47:14.736Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/c9/ceecc71fe2c9495a1d8e08d44f5f31f5bca1350d5b2e27a4b6265424f59e/beartype-0.22.6-py3-none-any.whl", hash = "sha256:0584bc46a2ea2a871509679278cda992eadde676c01356ab0ac77421f3c9a093", size = 1324807, upload-time = "2025-11-20T04:47:11.837Z" }, + { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" }, ] [[package]] name = "cachetools" -version = "6.2.2" +version = "7.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fb/44/ca1675be2a83aeee1886ab745b28cda92093066590233cc501890eb8417a/cachetools-6.2.2.tar.gz", hash = "sha256:8e6d266b25e539df852251cfd6f990b4bc3a141db73b939058d809ebd2590fc6", size = 31571, upload-time = "2025-11-13T17:42:51.465Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/e2/85f227594656000ff4d8adadae91a21f536d4a84c6c716a86bd6685874be/cachetools-7.1.1.tar.gz", hash = "sha256:27bdf856d68fd3c71c26c01b5edc312124ed427524d1ddb31aa2b7746fe20d4b", size = 40202, upload-time = "2026-05-03T20:00:29.391Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/46/eb6eca305c77a4489affe1c5d8f4cae82f285d9addd8de4ec084a7184221/cachetools-6.2.2-py3-none-any.whl", hash = "sha256:6c09c98183bf58560c97b2abfcedcbaf6a896a490f534b031b661d3723b45ace", size = 11503, upload-time = "2025-11-13T17:42:50.232Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0f/f897abe4ea0a8c408ae65c8c83bffab4936ad65d6032d4fb4cd35bbdc3ee/cachetools-7.1.1-py3-none-any.whl", hash = "sha256:0335cd7a0952d2b22327441fb0628139e234c565559eeb91a8a4ac7551c5353d", size = 16775, upload-time = "2026-05-03T20:00:27.857Z" }, +] + +[[package]] +name = "caio" +version = "0.9.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/88/b8527e1b00c1811db339a1df8bd1ae49d146fcea9d6a5c40e3a80aaeb38d/caio-0.9.25.tar.gz", hash = "sha256:16498e7f81d1d0f5a4c0ad3f2540e65fe25691376e0a5bd367f558067113ed10", size = 26781, upload-time = "2025-12-26T15:21:36.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/25/79c98ebe12df31548ba4eaf44db11b7cad6b3e7b4203718335620939083c/caio-0.9.25-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fb7ff95af4c31ad3f03179149aab61097a71fd85e05f89b4786de0359dffd044", size = 36983, upload-time = "2025-12-26T15:21:36.075Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2b/21288691f16d479945968a0a4f2856818c1c5be56881d51d4dac9b255d26/caio-0.9.25-cp312-cp312-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:97084e4e30dfa598449d874c4d8e0c8d5ea17d2f752ef5e48e150ff9d240cd64", size = 82012, upload-time = "2025-12-26T15:22:20.983Z" }, + { url = "https://files.pythonhosted.org/packages/03/c4/8a1b580875303500a9c12b9e0af58cb82e47f5bcf888c2457742a138273c/caio-0.9.25-cp312-cp312-manylinux_2_34_aarch64.whl", hash = "sha256:4fa69eba47e0f041b9d4f336e2ad40740681c43e686b18b191b6c5f4c5544bfb", size = 81502, upload-time = "2026-03-04T22:08:22.381Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/0fe770b8ffc8362c48134d1592d653a81a3d8748d764bec33864db36319d/caio-0.9.25-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:6bebf6f079f1341d19f7386db9b8b1f07e8cc15ae13bfdaff573371ba0575d69", size = 80200, upload-time = "2026-03-04T22:08:23.382Z" }, + { url = "https://files.pythonhosted.org/packages/31/57/5e6ff127e6f62c9f15d989560435c642144aa4210882f9494204bc892305/caio-0.9.25-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d6c2a3411af97762a2b03840c3cec2f7f728921ff8adda53d7ea2315a8563451", size = 36979, upload-time = "2025-12-26T15:21:35.484Z" }, + { url = "https://files.pythonhosted.org/packages/a3/9f/f21af50e72117eb528c422d4276cbac11fb941b1b812b182e0a9c70d19c5/caio-0.9.25-cp313-cp313-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0998210a4d5cd5cb565b32ccfe4e53d67303f868a76f212e002a8554692870e6", size = 81900, upload-time = "2025-12-26T15:22:21.919Z" }, + { url = "https://files.pythonhosted.org/packages/9c/12/c39ae2a4037cb10ad5eb3578eb4d5f8c1a2575c62bba675f3406b7ef0824/caio-0.9.25-cp313-cp313-manylinux_2_34_aarch64.whl", hash = "sha256:1a177d4777141b96f175fe2c37a3d96dec7911ed9ad5f02bac38aaa1c936611f", size = 81523, upload-time = "2026-03-04T22:08:25.187Z" }, + { url = "https://files.pythonhosted.org/packages/22/59/f8f2e950eb4f1a5a3883e198dca514b9d475415cb6cd7b78b9213a0dd45a/caio-0.9.25-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:9ed3cfb28c0e99fec5e208c934e5c157d0866aa9c32aa4dc5e9b6034af6286b7", size = 80243, upload-time = "2026-03-04T22:08:26.449Z" }, + { url = "https://files.pythonhosted.org/packages/69/ca/a08fdc7efdcc24e6a6131a93c85be1f204d41c58f474c42b0670af8c016b/caio-0.9.25-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fab6078b9348e883c80a5e14b382e6ad6aabbc4429ca034e76e730cf464269db", size = 36978, upload-time = "2025-12-26T15:21:41.055Z" }, + { url = "https://files.pythonhosted.org/packages/5e/6c/d4d24f65e690213c097174d26eda6831f45f4734d9d036d81790a27e7b78/caio-0.9.25-cp314-cp314-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:44a6b58e52d488c75cfaa5ecaa404b2b41cc965e6c417e03251e868ecd5b6d77", size = 81832, upload-time = "2025-12-26T15:22:22.757Z" }, + { url = "https://files.pythonhosted.org/packages/87/a4/e534cf7d2d0e8d880e25dd61e8d921ffcfe15bd696734589826f5a2df727/caio-0.9.25-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:628a630eb7fb22381dd8e3c8ab7f59e854b9c806639811fc3f4310c6bd711d79", size = 81565, upload-time = "2026-03-04T22:08:27.483Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ed/bf81aeac1d290017e5e5ac3e880fd56ee15e50a6d0353986799d1bc5cfd5/caio-0.9.25-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:0ba16aa605ccb174665357fc729cf500679c2d94d5f1458a6f0d5ca48f2060a7", size = 80071, upload-time = "2026-03-04T22:08:28.751Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/1f76c8d1bafe3b0614e06b2195784a3765bbf7b0a067661af9e2dd47fc33/caio-0.9.25-py3-none-any.whl", hash = "sha256:06c0bb02d6b929119b1cfbe1ca403c768b2013a369e2db46bfa2a5761cf82e40", size = 19087, upload-time = "2025-12-26T15:22:00.221Z" }, ] [[package]] name = "certifi" -version = "2025.8.3" +version = "2026.4.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, ] [[package]] @@ -130,67 +178,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] -[[package]] -name = "charset-normalizer" -version = "3.4.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" }, - { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" }, - { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" }, - { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" }, - { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" }, - { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" }, - { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" }, - { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" }, - { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" }, - { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" }, - { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, - { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, - { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, - { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, - { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, - { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, - { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, - { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, - { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, - { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, - { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, - { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, - { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, - { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, - { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, - { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, - { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, - { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, - { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, - { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, - { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, - { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, - { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, -] - [[package]] name = "click" -version = "8.3.0" +version = "8.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, -] - -[[package]] -name = "cloudpickle" -version = "3.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, + { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, ] [[package]] @@ -204,63 +201,60 @@ wheels = [ [[package]] name = "cryptography" -version = "46.0.1" +version = "48.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a9/62/e3664e6ffd7743e1694b244dde70b43a394f6f7fbcacf7014a8ff5197c73/cryptography-46.0.1.tar.gz", hash = "sha256:ed570874e88f213437f5cf758f9ef26cbfc3f336d889b1e592ee11283bb8d1c7", size = 749198, upload-time = "2025-09-17T00:10:35.797Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/8c/44ee01267ec01e26e43ebfdae3f120ec2312aa72fa4c0507ebe41a26739f/cryptography-46.0.1-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:1cd6d50c1a8b79af1a6f703709d8973845f677c8e97b1268f5ff323d38ce8475", size = 7285044, upload-time = "2025-09-17T00:08:36.807Z" }, - { url = "https://files.pythonhosted.org/packages/22/59/9ae689a25047e0601adfcb159ec4f83c0b4149fdb5c3030cc94cd218141d/cryptography-46.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0ff483716be32690c14636e54a1f6e2e1b7bf8e22ca50b989f88fa1b2d287080", size = 4308182, upload-time = "2025-09-17T00:08:39.388Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ee/ca6cc9df7118f2fcd142c76b1da0f14340d77518c05b1ebfbbabca6b9e7d/cryptography-46.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9873bf7c1f2a6330bdfe8621e7ce64b725784f9f0c3a6a55c3047af5849f920e", size = 4572393, upload-time = "2025-09-17T00:08:41.663Z" }, - { url = "https://files.pythonhosted.org/packages/7f/a3/0f5296f63815d8e985922b05c31f77ce44787b3127a67c0b7f70f115c45f/cryptography-46.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0dfb7c88d4462a0cfdd0d87a3c245a7bc3feb59de101f6ff88194f740f72eda6", size = 4308400, upload-time = "2025-09-17T00:08:43.559Z" }, - { url = "https://files.pythonhosted.org/packages/5d/8c/74fcda3e4e01be1d32775d5b4dd841acaac3c1b8fa4d0774c7ac8d52463d/cryptography-46.0.1-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e22801b61613ebdebf7deb18b507919e107547a1d39a3b57f5f855032dd7cfb8", size = 4015786, upload-time = "2025-09-17T00:08:45.758Z" }, - { url = "https://files.pythonhosted.org/packages/dc/b8/85d23287baeef273b0834481a3dd55bbed3a53587e3b8d9f0898235b8f91/cryptography-46.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:757af4f6341ce7a1e47c326ca2a81f41d236070217e5fbbad61bbfe299d55d28", size = 4982606, upload-time = "2025-09-17T00:08:47.602Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d3/de61ad5b52433b389afca0bc70f02a7a1f074651221f599ce368da0fe437/cryptography-46.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f7a24ea78de345cfa7f6a8d3bde8b242c7fac27f2bd78fa23474ca38dfaeeab9", size = 4604234, upload-time = "2025-09-17T00:08:49.879Z" }, - { url = "https://files.pythonhosted.org/packages/dc/1f/dbd4d6570d84748439237a7478d124ee0134bf166ad129267b7ed8ea6d22/cryptography-46.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e8776dac9e660c22241b6587fae51a67b4b0147daa4d176b172c3ff768ad736", size = 4307669, upload-time = "2025-09-17T00:08:52.321Z" }, - { url = "https://files.pythonhosted.org/packages/ec/fd/ca0a14ce7f0bfe92fa727aacaf2217eb25eb7e4ed513b14d8e03b26e63ed/cryptography-46.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9f40642a140c0c8649987027867242b801486865277cbabc8c6059ddef16dc8b", size = 4947579, upload-time = "2025-09-17T00:08:54.697Z" }, - { url = "https://files.pythonhosted.org/packages/89/6b/09c30543bb93401f6f88fce556b3bdbb21e55ae14912c04b7bf355f5f96c/cryptography-46.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:449ef2b321bec7d97ef2c944173275ebdab78f3abdd005400cc409e27cd159ab", size = 4603669, upload-time = "2025-09-17T00:08:57.16Z" }, - { url = "https://files.pythonhosted.org/packages/23/9a/38cb01cb09ce0adceda9fc627c9cf98eb890fc8d50cacbe79b011df20f8a/cryptography-46.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2dd339ba3345b908fa3141ddba4025568fa6fd398eabce3ef72a29ac2d73ad75", size = 4435828, upload-time = "2025-09-17T00:08:59.606Z" }, - { url = "https://files.pythonhosted.org/packages/0f/53/435b5c36a78d06ae0bef96d666209b0ecd8f8181bfe4dda46536705df59e/cryptography-46.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7411c910fb2a412053cf33cfad0153ee20d27e256c6c3f14d7d7d1d9fec59fd5", size = 4709553, upload-time = "2025-09-17T00:09:01.832Z" }, - { url = "https://files.pythonhosted.org/packages/f5/c4/0da6e55595d9b9cd3b6eb5dc22f3a07ded7f116a3ea72629cab595abb804/cryptography-46.0.1-cp311-abi3-win32.whl", hash = "sha256:cbb8e769d4cac884bb28e3ff620ef1001b75588a5c83c9c9f1fdc9afbe7f29b0", size = 3058327, upload-time = "2025-09-17T00:09:03.726Z" }, - { url = "https://files.pythonhosted.org/packages/95/0f/cd29a35e0d6e78a0ee61793564c8cff0929c38391cb0de27627bdc7525aa/cryptography-46.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:92e8cfe8bd7dd86eac0a677499894862cd5cc2fd74de917daa881d00871ac8e7", size = 3523893, upload-time = "2025-09-17T00:09:06.272Z" }, - { url = "https://files.pythonhosted.org/packages/f2/dd/eea390f3e78432bc3d2f53952375f8b37cb4d37783e626faa6a51e751719/cryptography-46.0.1-cp311-abi3-win_arm64.whl", hash = "sha256:db5597a4c7353b2e5fb05a8e6cb74b56a4658a2b7bf3cb6b1821ae7e7fd6eaa0", size = 2932145, upload-time = "2025-09-17T00:09:08.568Z" }, - { url = "https://files.pythonhosted.org/packages/0a/fb/c73588561afcd5e24b089952bd210b14676c0c5bf1213376350ae111945c/cryptography-46.0.1-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:4c49eda9a23019e11d32a0eb51a27b3e7ddedde91e099c0ac6373e3aacc0d2ee", size = 7193928, upload-time = "2025-09-17T00:09:10.595Z" }, - { url = "https://files.pythonhosted.org/packages/26/34/0ff0bb2d2c79f25a2a63109f3b76b9108a906dd2a2eb5c1d460b9938adbb/cryptography-46.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9babb7818fdd71394e576cf26c5452df77a355eac1a27ddfa24096665a27f8fd", size = 4293515, upload-time = "2025-09-17T00:09:12.861Z" }, - { url = "https://files.pythonhosted.org/packages/df/b7/d4f848aee24ecd1be01db6c42c4a270069a4f02a105d9c57e143daf6cf0f/cryptography-46.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9f2c4cc63be3ef43c0221861177cee5d14b505cd4d4599a89e2cd273c4d3542a", size = 4545619, upload-time = "2025-09-17T00:09:15.397Z" }, - { url = "https://files.pythonhosted.org/packages/44/a5/42fedefc754fd1901e2d95a69815ea4ec8a9eed31f4c4361fcab80288661/cryptography-46.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:41c281a74df173876da1dc9a9b6953d387f06e3d3ed9284e3baae3ab3f40883a", size = 4299160, upload-time = "2025-09-17T00:09:17.155Z" }, - { url = "https://files.pythonhosted.org/packages/86/a1/cd21174f56e769c831fbbd6399a1b7519b0ff6280acec1b826d7b072640c/cryptography-46.0.1-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0a17377fa52563d730248ba1f68185461fff36e8bc75d8787a7dd2e20a802b7a", size = 3994491, upload-time = "2025-09-17T00:09:18.971Z" }, - { url = "https://files.pythonhosted.org/packages/8d/2f/a8cbfa1c029987ddc746fd966711d4fa71efc891d37fbe9f030fe5ab4eec/cryptography-46.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:0d1922d9280e08cde90b518a10cd66831f632960a8d08cb3418922d83fce6f12", size = 4960157, upload-time = "2025-09-17T00:09:20.923Z" }, - { url = "https://files.pythonhosted.org/packages/67/ae/63a84e6789e0d5a2502edf06b552bcb0fa9ff16147265d5c44a211942abe/cryptography-46.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:af84e8e99f1a82cea149e253014ea9dc89f75b82c87bb6c7242203186f465129", size = 4577263, upload-time = "2025-09-17T00:09:23.356Z" }, - { url = "https://files.pythonhosted.org/packages/ef/8f/1b9fa8e92bd9cbcb3b7e1e593a5232f2c1e6f9bd72b919c1a6b37d315f92/cryptography-46.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ef648d2c690703501714588b2ba640facd50fd16548133b11b2859e8655a69da", size = 4298703, upload-time = "2025-09-17T00:09:25.566Z" }, - { url = "https://files.pythonhosted.org/packages/c3/af/bb95db070e73fea3fae31d8a69ac1463d89d1c084220f549b00dd01094a8/cryptography-46.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:e94eb5fa32a8a9f9bf991f424f002913e3dd7c699ef552db9b14ba6a76a6313b", size = 4926363, upload-time = "2025-09-17T00:09:27.451Z" }, - { url = "https://files.pythonhosted.org/packages/f5/3b/d8fb17ffeb3a83157a1cc0aa5c60691d062aceecba09c2e5e77ebfc1870c/cryptography-46.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:534b96c0831855e29fc3b069b085fd185aa5353033631a585d5cd4dd5d40d657", size = 4576958, upload-time = "2025-09-17T00:09:29.924Z" }, - { url = "https://files.pythonhosted.org/packages/d9/46/86bc3a05c10c8aa88c8ae7e953a8b4e407c57823ed201dbcba55c4d655f4/cryptography-46.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9b55038b5c6c47559aa33626d8ecd092f354e23de3c6975e4bb205df128a2a0", size = 4422507, upload-time = "2025-09-17T00:09:32.222Z" }, - { url = "https://files.pythonhosted.org/packages/a8/4e/387e5a21dfd2b4198e74968a541cfd6128f66f8ec94ed971776e15091ac3/cryptography-46.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ec13b7105117dbc9afd023300fb9954d72ca855c274fe563e72428ece10191c0", size = 4683964, upload-time = "2025-09-17T00:09:34.118Z" }, - { url = "https://files.pythonhosted.org/packages/25/a3/f9f5907b166adb8f26762071474b38bbfcf89858a5282f032899075a38a1/cryptography-46.0.1-cp314-cp314t-win32.whl", hash = "sha256:504e464944f2c003a0785b81668fe23c06f3b037e9cb9f68a7c672246319f277", size = 3029705, upload-time = "2025-09-17T00:09:36.381Z" }, - { url = "https://files.pythonhosted.org/packages/12/66/4d3a4f1850db2e71c2b1628d14b70b5e4c1684a1bd462f7fffb93c041c38/cryptography-46.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c52fded6383f7e20eaf70a60aeddd796b3677c3ad2922c801be330db62778e05", size = 3502175, upload-time = "2025-09-17T00:09:38.261Z" }, - { url = "https://files.pythonhosted.org/packages/52/c7/9f10ad91435ef7d0d99a0b93c4360bea3df18050ff5b9038c489c31ac2f5/cryptography-46.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:9495d78f52c804b5ec8878b5b8c7873aa8e63db9cd9ee387ff2db3fffe4df784", size = 2912354, upload-time = "2025-09-17T00:09:40.078Z" }, - { url = "https://files.pythonhosted.org/packages/98/e5/fbd632385542a3311915976f88e0dfcf09e62a3fc0aff86fb6762162a24d/cryptography-46.0.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:d84c40bdb8674c29fa192373498b6cb1e84f882889d21a471b45d1f868d8d44b", size = 7255677, upload-time = "2025-09-17T00:09:42.407Z" }, - { url = "https://files.pythonhosted.org/packages/56/3e/13ce6eab9ad6eba1b15a7bd476f005a4c1b3f299f4c2f32b22408b0edccf/cryptography-46.0.1-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9ed64e5083fa806709e74fc5ea067dfef9090e5b7a2320a49be3c9df3583a2d8", size = 4301110, upload-time = "2025-09-17T00:09:45.614Z" }, - { url = "https://files.pythonhosted.org/packages/a2/67/65dc233c1ddd688073cf7b136b06ff4b84bf517ba5529607c9d79720fc67/cryptography-46.0.1-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:341fb7a26bc9d6093c1b124b9f13acc283d2d51da440b98b55ab3f79f2522ead", size = 4562369, upload-time = "2025-09-17T00:09:47.601Z" }, - { url = "https://files.pythonhosted.org/packages/17/db/d64ae4c6f4e98c3dac5bf35dd4d103f4c7c345703e43560113e5e8e31b2b/cryptography-46.0.1-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6ef1488967e729948d424d09c94753d0167ce59afba8d0f6c07a22b629c557b2", size = 4302126, upload-time = "2025-09-17T00:09:49.335Z" }, - { url = "https://files.pythonhosted.org/packages/3d/19/5f1eea17d4805ebdc2e685b7b02800c4f63f3dd46cfa8d4c18373fea46c8/cryptography-46.0.1-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7823bc7cdf0b747ecfb096d004cc41573c2f5c7e3a29861603a2871b43d3ef32", size = 4009431, upload-time = "2025-09-17T00:09:51.239Z" }, - { url = "https://files.pythonhosted.org/packages/81/b5/229ba6088fe7abccbfe4c5edb96c7a5ad547fac5fdd0d40aa6ea540b2985/cryptography-46.0.1-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:f736ab8036796f5a119ff8211deda416f8c15ce03776db704a7a4e17381cb2ef", size = 4980739, upload-time = "2025-09-17T00:09:54.181Z" }, - { url = "https://files.pythonhosted.org/packages/3a/9c/50aa38907b201e74bc43c572f9603fa82b58e831bd13c245613a23cff736/cryptography-46.0.1-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:e46710a240a41d594953012213ea8ca398cd2448fbc5d0f1be8160b5511104a0", size = 4592289, upload-time = "2025-09-17T00:09:56.731Z" }, - { url = "https://files.pythonhosted.org/packages/5a/33/229858f8a5bb22f82468bb285e9f4c44a31978d5f5830bb4ea1cf8a4e454/cryptography-46.0.1-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:84ef1f145de5aee82ea2447224dc23f065ff4cc5791bb3b506615957a6ba8128", size = 4301815, upload-time = "2025-09-17T00:09:58.548Z" }, - { url = "https://files.pythonhosted.org/packages/52/cb/b76b2c87fbd6ed4a231884bea3ce073406ba8e2dae9defad910d33cbf408/cryptography-46.0.1-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9394c7d5a7565ac5f7d9ba38b2617448eba384d7b107b262d63890079fad77ca", size = 4943251, upload-time = "2025-09-17T00:10:00.475Z" }, - { url = "https://files.pythonhosted.org/packages/94/0f/f66125ecf88e4cb5b8017ff43f3a87ede2d064cb54a1c5893f9da9d65093/cryptography-46.0.1-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ed957044e368ed295257ae3d212b95456bd9756df490e1ac4538857f67531fcc", size = 4591247, upload-time = "2025-09-17T00:10:02.874Z" }, - { url = "https://files.pythonhosted.org/packages/f6/22/9f3134ae436b63b463cfdf0ff506a0570da6873adb4bf8c19b8a5b4bac64/cryptography-46.0.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f7de12fa0eee6234de9a9ce0ffcfa6ce97361db7a50b09b65c63ac58e5f22fc7", size = 4428534, upload-time = "2025-09-17T00:10:04.994Z" }, - { url = "https://files.pythonhosted.org/packages/89/39/e6042bcb2638650b0005c752c38ea830cbfbcbb1830e4d64d530000aa8dc/cryptography-46.0.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7fab1187b6c6b2f11a326f33b036f7168f5b996aedd0c059f9738915e4e8f53a", size = 4699541, upload-time = "2025-09-17T00:10:06.925Z" }, - { url = "https://files.pythonhosted.org/packages/68/46/753d457492d15458c7b5a653fc9a84a1c9c7a83af6ebdc94c3fc373ca6e8/cryptography-46.0.1-cp38-abi3-win32.whl", hash = "sha256:45f790934ac1018adeba46a0f7289b2b8fe76ba774a88c7f1922213a56c98bc1", size = 3043779, upload-time = "2025-09-17T00:10:08.951Z" }, - { url = "https://files.pythonhosted.org/packages/2f/50/b6f3b540c2f6ee712feeb5fa780bb11fad76634e71334718568e7695cb55/cryptography-46.0.1-cp38-abi3-win_amd64.whl", hash = "sha256:7176a5ab56fac98d706921f6416a05e5aff7df0e4b91516f450f8627cda22af3", size = 3517226, upload-time = "2025-09-17T00:10:10.769Z" }, - { url = "https://files.pythonhosted.org/packages/ff/e8/77d17d00981cdd27cc493e81e1749a0b8bbfb843780dbd841e30d7f50743/cryptography-46.0.1-cp38-abi3-win_arm64.whl", hash = "sha256:efc9e51c3e595267ff84adf56e9b357db89ab2279d7e375ffcaf8f678606f3d9", size = 2923149, upload-time = "2025-09-17T00:10:13.236Z" }, + { url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" }, + { url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" }, + { url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" }, + { url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" }, + { url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" }, + { url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" }, + { url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" }, + { url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" }, + { url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" }, + { url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" }, + { url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" }, + { url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" }, + { url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" }, + { url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" }, + { url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" }, + { url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" }, + { url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" }, + { url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" }, + { url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" }, + { url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" }, + { url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" }, + { url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" }, + { url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" }, + { url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" }, + { url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" }, + { url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" }, + { url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" }, + { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" }, + { url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" }, ] [[package]] name = "cyclopts" -version = "4.2.5" +version = "4.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -268,18 +262,9 @@ dependencies = [ { name = "rich" }, { name = "rich-rst" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/94/f0/5a83ac365800969552a7eb261b03860dea38647fb6cc31574bdbcdc9f0f5/cyclopts-4.2.5.tar.gz", hash = "sha256:91aff5da5b8b841ccb32b5142c2d12fd93678fbc9eba7ef7319452323e24d6d4", size = 149497, upload-time = "2025-11-22T02:33:37.526Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/c3/d3f095120329616cc364af2bedcffd518d4db18c978f2f6c892d29e6af2f/cyclopts-4.12.0.tar.gz", hash = "sha256:86bfb5b35cb078decc1cca6c1be41f9a0e6202dc43b4f6056d5cfc6d1f4a69d1", size = 176123, upload-time = "2026-05-13T13:26:31.243Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/00/a9b81bdba88e2904602e970e46ffd18b6a833d902f18d91bdce6fc271c49/cyclopts-4.2.5-py3-none-any.whl", hash = "sha256:361be316ce7f6ce674cad8d34bf6c5e39c34daaeceae40632a55b599472975c7", size = 185196, upload-time = "2025-11-22T02:33:36.103Z" }, -] - -[[package]] -name = "diskcache" -version = "5.6.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d0/17b6b7d5f64dea337a7a409a1e4e0eeceda724046b9acd158fd1aa2f2328/cyclopts-4.12.0-py3-none-any.whl", hash = "sha256:ee03d2b9ef790d866cb3823a7e54b2be5252c82d34536579846fce068b30c38f", size = 213706, upload-time = "2026-05-13T13:26:29.744Z" }, ] [[package]] @@ -293,20 +278,20 @@ wheels = [ [[package]] name = "docstring-parser" -version = "0.17.0" +version = "0.18.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, + { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, ] [[package]] name = "docutils" -version = "0.22.2" +version = "0.22.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4a/c0/89fe6215b443b919cb98a5002e107cb5026854ed1ccb6b5833e0768419d1/docutils-0.22.2.tar.gz", hash = "sha256:9fdb771707c8784c8f2728b67cb2c691305933d68137ef95a75db5f4dfbc213d", size = 2289092, upload-time = "2025-09-20T17:55:47.994Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/66/dd/f95350e853a4468ec37478414fc04ae2d61dad7a947b3015c3dcc51a09b9/docutils-0.22.2-py3-none-any.whl", hash = "sha256:b0e98d679283fc3bb0ead8a5da7f501baa632654e7056e9c5846842213d674d8", size = 632667, upload-time = "2025-09-20T17:55:43.052Z" }, + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, ] [[package]] @@ -324,59 +309,56 @@ wheels = [ [[package]] name = "exceptiongroup" -version = "1.3.0" +version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +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 = [ - { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, -] - -[[package]] -name = "fakeredis" -version = "2.33.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "redis" }, - { name = "sortedcontainers" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5f/f9/57464119936414d60697fcbd32f38909bb5688b616ae13de6e98384433e0/fakeredis-2.33.0.tar.gz", hash = "sha256:d7bc9a69d21df108a6451bbffee23b3eba432c21a654afc7ff2d295428ec5770", size = 175187, upload-time = "2025-12-16T19:45:52.269Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/78/a850fed8aeef96d4a99043c90b818b2ed5419cd5b24a4049fd7cfb9f1471/fakeredis-2.33.0-py3-none-any.whl", hash = "sha256:de535f3f9ccde1c56672ab2fdd6a8efbc4f2619fc2f1acc87b8737177d71c965", size = 119605, upload-time = "2025-12-16T19:45:51.08Z" }, -] - -[package.optional-dependencies] -lua = [ - { name = "lupa" }, + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] [[package]] name = "fastmcp" -version = "2.14.0" +version = "3.2.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "authlib" }, { name = "cyclopts" }, { name = "exceptiongroup" }, + { name = "griffelib" }, { name = "httpx" }, + { name = "jsonref" }, { name = "jsonschema-path" }, { name = "mcp" }, { name = "openapi-pydantic" }, + { name = "opentelemetry-api" }, + { name = "packaging" }, { name = "platformdirs" }, - { name = "py-key-value-aio", extra = ["disk", "keyring", "memory"] }, + { name = "py-key-value-aio", extra = ["filetree", "keyring", "memory"] }, { name = "pydantic", extra = ["email"] }, - { name = "pydocket" }, { name = "pyperclip" }, { name = "python-dotenv" }, + { name = "pyyaml" }, { name = "rich" }, + { name = "uncalled-for" }, { name = "uvicorn" }, + { name = "watchfiles" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/35/50/9bb042a2d290ccadb35db3580ac507f192e1a39c489eb8faa167cd5e3b57/fastmcp-2.14.0.tar.gz", hash = "sha256:c1f487b36a3e4b043dbf3330e588830047df2e06f8ef0920d62dfb34d0905727", size = 8232562, upload-time = "2025-12-11T23:04:27.134Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/13/29544fbc6dfe45ea38046af0067311e0bad7acc7d1f2ad38bb08f2409fe2/fastmcp-3.2.4.tar.gz", hash = "sha256:083ecb75b44a4169e7fc0f632f94b781bdb0ff877c6b35b9877cbb566fd4d4d1", size = 28746127, upload-time = "2026-04-14T01:42:24.174Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/73/b5656172a6beb2eacec95f04403ddea1928e4b22066700fd14780f8f45d1/fastmcp-2.14.0-py3-none-any.whl", hash = "sha256:7b374c0bcaf1ef1ef46b9255ea84c607f354291eaf647ff56a47c69f5ec0c204", size = 398965, upload-time = "2025-12-11T23:04:25.587Z" }, + { url = "https://files.pythonhosted.org/packages/cf/76/b310d52fa0e30d39bd937eb58ec2c1f1ea1b5f519f0575e9dd9612f01deb/fastmcp-3.2.4-py3-none-any.whl", hash = "sha256:e6c9c429171041455e47ab94bb3f83c4657622a0ec28922f6940053959bd58a9", size = 728599, upload-time = "2026-04-14T01:42:26.85Z" }, +] + +[[package]] +name = "griffelib" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/82/74f4a3310cdabfbb10da554c3a672847f1ed33c6f61dd472681ce7f1fe67/griffelib-2.0.2.tar.gz", hash = "sha256:3cf20b3bc470e83763ffbf236e0076b1211bac1bc67de13daf494640f2de707e", size = 166461, upload-time = "2026-03-27T11:34:51.091Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl", hash = "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1", size = 142357, upload-time = "2026-03-27T11:34:46.275Z" }, ] [[package]] @@ -418,20 +400,20 @@ wheels = [ [[package]] name = "httpx-sse" -version = "0.4.1" +version = "0.4.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6e/fa/66bd985dd0b7c109a3bcb89272ee0bfb7e2b4d06309ad7b38ff866734b2a/httpx_sse-0.4.1.tar.gz", hash = "sha256:8f44d34414bc7b21bf3602713005c5df4917884f76072479b21f68befa4ea26e", size = 12998, upload-time = "2025-06-24T13:21:05.71Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/0a/6269e3473b09aed2dab8aa1a600c70f31f00ae1349bee30658f7e358a159/httpx_sse-0.4.1-py3-none-any.whl", hash = "sha256:cba42174344c3a5b06f255ce65b350880f962d99ead85e776f23c6618a377a37", size = 8054, upload-time = "2025-06-24T13:21:04.772Z" }, + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, ] [[package]] name = "idna" -version = "3.10" +version = "3.15" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, + { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, ] [[package]] @@ -446,6 +428,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "jaraco-classes" version = "3.4.0" @@ -460,23 +451,23 @@ wheels = [ [[package]] name = "jaraco-context" -version = "6.0.1" +version = "6.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912, upload-time = "2024-08-20T03:39:27.358Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/50/4763cd07e722bb6285316d390a164bc7e479db9d90daa769f22578f698b4/jaraco_context-6.1.2.tar.gz", hash = "sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3", size = 16801, upload-time = "2026-03-20T22:13:33.922Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825, upload-time = "2024-08-20T03:39:25.966Z" }, + { url = "https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl", hash = "sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535", size = 7871, upload-time = "2026-03-20T22:13:32.808Z" }, ] [[package]] name = "jaraco-functools" -version = "4.3.0" +version = "4.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "more-itertools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f7/ed/1aa2d585304ec07262e1a83a9889880701079dde796ac7b1d1826f40c63d/jaraco_functools-4.3.0.tar.gz", hash = "sha256:cfd13ad0dd2c47a3600b439ef72d8615d482cedcff1632930d6f28924d92f294", size = 19755, upload-time = "2025-08-18T20:05:09.91Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/09/726f168acad366b11e420df31bf1c702a54d373a83f968d94141a8c3fde0/jaraco_functools-4.3.0-py3-none-any.whl", hash = "sha256:227ff8ed6f7b8f62c56deff101545fa7543cf2c8e7b82a7c2116e672f29c26e8", size = 10408, upload-time = "2025-08-18T20:05:08.69Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" }, ] [[package]] @@ -488,9 +479,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, ] +[[package]] +name = "joserfc" +version = "1.6.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/dc/5f768c2e391e9afabe5d18e3221346deb5fb6338565f1ccc9e7c6d7befdd/joserfc-1.6.5.tar.gz", hash = "sha256:1482a7db78fb4602e44ed89e51b599d052e091288c7c532c5b694e20149dec48", size = 231881, upload-time = "2026-05-06T04:58:13.408Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/3b/ad1cb22e75c963b1f07c8a2329bf47227ce7e4361df5eb2fb101b2ce33ef/joserfc-1.6.5-py3-none-any.whl", hash = "sha256:e9878a0f8243fe7b95e11fdda81374ca9f7a689e302751579d3dfdeec559675e", size = 70464, upload-time = "2026-05-06T04:58:11.668Z" }, +] + +[[package]] +name = "jsonref" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/0d/c1f3277e90ccdb50d33ed5ba1ec5b3f0a242ed8c1b1a85d3afeb68464dca/jsonref-1.1.0.tar.gz", hash = "sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552", size = 8814, upload-time = "2023-01-16T16:10:04.455Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/ec/e1db9922bceb168197a558a2b8c03a7963f1afe93517ddd3cf99f202f996/jsonref-1.1.0-py3-none-any.whl", hash = "sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9", size = 9425, upload-time = "2023-01-16T16:10:02.255Z" }, +] + [[package]] name = "jsonschema" -version = "4.25.1" +version = "4.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -498,24 +510,23 @@ dependencies = [ { name = "referencing" }, { name = "rpds-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, ] [[package]] name = "jsonschema-path" -version = "0.3.4" +version = "0.4.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pathable" }, { name = "pyyaml" }, { name = "referencing" }, - { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6e/45/41ebc679c2a4fced6a722f624c18d658dee42612b83ea24c1caf7c0eb3a8/jsonschema_path-0.3.4.tar.gz", hash = "sha256:8365356039f16cc65fddffafda5f58766e34bebab7d6d105616ab52bc4297001", size = 11159, upload-time = "2025-01-24T14:33:16.547Z" } +sdist = { url = "https://files.pythonhosted.org/packages/01/86/cfee6dd25843bec0760f456599a4f7e7e40221a934b9229fda0662c859bc/jsonschema_path-0.4.6.tar.gz", hash = "sha256:c89eb635f4d497c9ac328eeff359c489755838806a7d033510a692e9576f5c4b", size = 15302, upload-time = "2026-04-27T18:57:08.412Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/58/3485da8cb93d2f393bce453adeef16896751f14ba3e2024bc21dc9597646/jsonschema_path-0.3.4-py3-none-any.whl", hash = "sha256:f502191fdc2b22050f9a81c9237be9d27145b9001c55842bece5e94e382e52f8", size = 14810, upload-time = "2025-01-24T14:33:14.652Z" }, + { url = "https://files.pythonhosted.org/packages/6c/43/3d3065c05a04bb550c143bfbb8e4fd7022cd327e1082bf257bac74923783/jsonschema_path-0.4.6-py3-none-any.whl", hash = "sha256:451354b5311fa955c3144e6e4e255388c751c0121c5570ec5bb9291dd42d08c9", size = 19565, upload-time = "2026-04-27T18:57:06.792Z" }, ] [[package]] @@ -547,73 +558,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, ] -[[package]] -name = "lupa" -version = "2.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b8/1c/191c3e6ec6502e3dbe25a53e27f69a5daeac3e56de1f73c0138224171ead/lupa-2.6.tar.gz", hash = "sha256:9a770a6e89576be3447668d7ced312cd6fd41d3c13c2462c9dc2c2ab570e45d9", size = 7240282, upload-time = "2025-10-24T07:20:29.738Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/86/ce243390535c39d53ea17ccf0240815e6e457e413e40428a658ea4ee4b8d/lupa-2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47ce718817ef1cc0c40d87c3d5ae56a800d61af00fbc0fad1ca9be12df2f3b56", size = 951707, upload-time = "2025-10-24T07:18:03.884Z" }, - { url = "https://files.pythonhosted.org/packages/86/85/cedea5e6cbeb54396fdcc55f6b741696f3f036d23cfaf986d50d680446da/lupa-2.6-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7aba985b15b101495aa4b07112cdc08baa0c545390d560ad5cfde2e9e34f4d58", size = 1916703, upload-time = "2025-10-24T07:18:05.6Z" }, - { url = "https://files.pythonhosted.org/packages/24/be/3d6b5f9a8588c01a4d88129284c726017b2089f3a3fd3ba8bd977292fea0/lupa-2.6-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:b766f62f95b2739f2248977d29b0722e589dcf4f0ccfa827ccbd29f0148bd2e5", size = 985152, upload-time = "2025-10-24T07:18:08.561Z" }, - { url = "https://files.pythonhosted.org/packages/eb/23/9f9a05beee5d5dce9deca4cb07c91c40a90541fc0a8e09db4ee670da550f/lupa-2.6-cp312-cp312-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:00a934c23331f94cb51760097ebfab14b005d55a6b30a2b480e3c53dd2fa290d", size = 1159599, upload-time = "2025-10-24T07:18:10.346Z" }, - { url = "https://files.pythonhosted.org/packages/40/4e/e7c0583083db9d7f1fd023800a9767d8e4391e8330d56c2373d890ac971b/lupa-2.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21de9f38bd475303e34a042b7081aabdf50bd9bafd36ce4faea2f90fd9f15c31", size = 1038686, upload-time = "2025-10-24T07:18:12.112Z" }, - { url = "https://files.pythonhosted.org/packages/1c/9f/5a4f7d959d4feba5e203ff0c31889e74d1ca3153122be4a46dca7d92bf7c/lupa-2.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf3bda96d3fc41237e964a69c23647d50d4e28421111360274d4799832c560e9", size = 2071956, upload-time = "2025-10-24T07:18:14.572Z" }, - { url = "https://files.pythonhosted.org/packages/92/34/2f4f13ca65d01169b1720176aedc4af17bc19ee834598c7292db232cb6dc/lupa-2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a76ead245da54801a81053794aa3975f213221f6542d14ec4b859ee2e7e0323", size = 1057199, upload-time = "2025-10-24T07:18:16.379Z" }, - { url = "https://files.pythonhosted.org/packages/35/2a/5f7d2eebec6993b0dcd428e0184ad71afb06a45ba13e717f6501bfed1da3/lupa-2.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8dd0861741caa20886ddbda0a121d8e52fb9b5bb153d82fa9bba796962bf30e8", size = 1173693, upload-time = "2025-10-24T07:18:18.153Z" }, - { url = "https://files.pythonhosted.org/packages/e4/29/089b4d2f8e34417349af3904bb40bec40b65c8731f45e3fd8d497ca573e5/lupa-2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:239e63948b0b23023f81d9a19a395e768ed3da6a299f84e7963b8f813f6e3f9c", size = 2164394, upload-time = "2025-10-24T07:18:20.403Z" }, - { url = "https://files.pythonhosted.org/packages/f3/1b/79c17b23c921f81468a111cad843b076a17ef4b684c4a8dff32a7969c3f0/lupa-2.6-cp312-cp312-win32.whl", hash = "sha256:325894e1099499e7a6f9c351147661a2011887603c71086d36fe0f964d52d1ce", size = 1420647, upload-time = "2025-10-24T07:18:23.368Z" }, - { url = "https://files.pythonhosted.org/packages/b8/15/5121e68aad3584e26e1425a5c9a79cd898f8a152292059e128c206ee817c/lupa-2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c735a1ce8ee60edb0fe71d665f1e6b7c55c6021f1d340eb8c865952c602cd36f", size = 1688529, upload-time = "2025-10-24T07:18:25.523Z" }, - { url = "https://files.pythonhosted.org/packages/28/1d/21176b682ca5469001199d8b95fa1737e29957a3d185186e7a8b55345f2e/lupa-2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:663a6e58a0f60e7d212017d6678639ac8df0119bc13c2145029dcba084391310", size = 947232, upload-time = "2025-10-24T07:18:27.878Z" }, - { url = "https://files.pythonhosted.org/packages/ce/4c/d327befb684660ca13cf79cd1f1d604331808f9f1b6fb6bf57832f8edf80/lupa-2.6-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:d1f5afda5c20b1f3217a80e9bc1b77037f8a6eb11612fd3ada19065303c8f380", size = 1908625, upload-time = "2025-10-24T07:18:29.944Z" }, - { url = "https://files.pythonhosted.org/packages/66/8e/ad22b0a19454dfd08662237a84c792d6d420d36b061f239e084f29d1a4f3/lupa-2.6-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:26f2b3c085fe76e9119e48c1013c1cccdc1f51585d456858290475aa38e7089e", size = 981057, upload-time = "2025-10-24T07:18:31.553Z" }, - { url = "https://files.pythonhosted.org/packages/5c/48/74859073ab276bd0566c719f9ca0108b0cfc1956ca0d68678d117d47d155/lupa-2.6-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:60d2f902c7b96fb8ab98493dcff315e7bb4d0b44dc9dd76eb37de575025d5685", size = 1156227, upload-time = "2025-10-24T07:18:33.981Z" }, - { url = "https://files.pythonhosted.org/packages/09/6c/0e9ded061916877253c2266074060eb71ed99fb21d73c8c114a76725bce2/lupa-2.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a02d25dee3a3250967c36590128d9220ae02f2eda166a24279da0b481519cbff", size = 1035752, upload-time = "2025-10-24T07:18:36.32Z" }, - { url = "https://files.pythonhosted.org/packages/dd/ef/f8c32e454ef9f3fe909f6c7d57a39f950996c37a3deb7b391fec7903dab7/lupa-2.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6eae1ee16b886b8914ff292dbefbf2f48abfbdee94b33a88d1d5475e02423203", size = 2069009, upload-time = "2025-10-24T07:18:38.072Z" }, - { url = "https://files.pythonhosted.org/packages/53/dc/15b80c226a5225815a890ee1c11f07968e0aba7a852df41e8ae6fe285063/lupa-2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0edd5073a4ee74ab36f74fe61450148e6044f3952b8d21248581f3c5d1a58be", size = 1056301, upload-time = "2025-10-24T07:18:40.165Z" }, - { url = "https://files.pythonhosted.org/packages/31/14/2086c1425c985acfb30997a67e90c39457122df41324d3c179d6ee2292c6/lupa-2.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0c53ee9f22a8a17e7d4266ad48e86f43771951797042dd51d1494aaa4f5f3f0a", size = 1170673, upload-time = "2025-10-24T07:18:42.426Z" }, - { url = "https://files.pythonhosted.org/packages/10/e5/b216c054cf86576c0191bf9a9f05de6f7e8e07164897d95eea0078dca9b2/lupa-2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:de7c0f157a9064a400d828789191a96da7f4ce889969a588b87ec80de9b14772", size = 2162227, upload-time = "2025-10-24T07:18:46.112Z" }, - { url = "https://files.pythonhosted.org/packages/59/2f/33ecb5bedf4f3bc297ceacb7f016ff951331d352f58e7e791589609ea306/lupa-2.6-cp313-cp313-win32.whl", hash = "sha256:ee9523941ae0a87b5b703417720c5d78f72d2f5bc23883a2ea80a949a3ed9e75", size = 1419558, upload-time = "2025-10-24T07:18:48.371Z" }, - { url = "https://files.pythonhosted.org/packages/f9/b4/55e885834c847ea610e111d87b9ed4768f0afdaeebc00cd46810f25029f6/lupa-2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b1335a5835b0a25ebdbc75cf0bda195e54d133e4d994877ef025e218c2e59db9", size = 1683424, upload-time = "2025-10-24T07:18:50.976Z" }, - { url = "https://files.pythonhosted.org/packages/66/9d/d9427394e54d22a35d1139ef12e845fd700d4872a67a34db32516170b746/lupa-2.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dcb6d0a3264873e1653bc188499f48c1fb4b41a779e315eba45256cfe7bc33c1", size = 953818, upload-time = "2025-10-24T07:18:53.378Z" }, - { url = "https://files.pythonhosted.org/packages/10/41/27bbe81953fb2f9ecfced5d9c99f85b37964cfaf6aa8453bb11283983721/lupa-2.6-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:a37e01f2128f8c36106726cb9d360bac087d58c54b4522b033cc5691c584db18", size = 1915850, upload-time = "2025-10-24T07:18:55.259Z" }, - { url = "https://files.pythonhosted.org/packages/a3/98/f9ff60db84a75ba8725506bbf448fb085bc77868a021998ed2a66d920568/lupa-2.6-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:458bd7e9ff3c150b245b0fcfbb9bd2593d1152ea7f0a7b91c1d185846da033fe", size = 982344, upload-time = "2025-10-24T07:18:57.05Z" }, - { url = "https://files.pythonhosted.org/packages/41/f7/f39e0f1c055c3b887d86b404aaf0ca197b5edfd235a8b81b45b25bac7fc3/lupa-2.6-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:052ee82cac5206a02df77119c325339acbc09f5ce66967f66a2e12a0f3211cad", size = 1156543, upload-time = "2025-10-24T07:18:59.251Z" }, - { url = "https://files.pythonhosted.org/packages/9e/9c/59e6cffa0d672d662ae17bd7ac8ecd2c89c9449dee499e3eb13ca9cd10d9/lupa-2.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96594eca3c87dd07938009e95e591e43d554c1dbd0385be03c100367141db5a8", size = 1047974, upload-time = "2025-10-24T07:19:01.449Z" }, - { url = "https://files.pythonhosted.org/packages/23/c6/a04e9cef7c052717fcb28fb63b3824802488f688391895b618e39be0f684/lupa-2.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8faddd9d198688c8884091173a088a8e920ecc96cda2ffed576a23574c4b3f6", size = 2073458, upload-time = "2025-10-24T07:19:03.369Z" }, - { url = "https://files.pythonhosted.org/packages/e6/10/824173d10f38b51fc77785228f01411b6ca28826ce27404c7c912e0e442c/lupa-2.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:daebb3a6b58095c917e76ba727ab37b27477fb926957c825205fbda431552134", size = 1067683, upload-time = "2025-10-24T07:19:06.2Z" }, - { url = "https://files.pythonhosted.org/packages/b6/dc/9692fbcf3c924d9c4ece2d8d2f724451ac2e09af0bd2a782db1cef34e799/lupa-2.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f3154e68972befe0f81564e37d8142b5d5d79931a18309226a04ec92487d4ea3", size = 1171892, upload-time = "2025-10-24T07:19:08.544Z" }, - { url = "https://files.pythonhosted.org/packages/84/ff/e318b628d4643c278c96ab3ddea07fc36b075a57383c837f5b11e537ba9d/lupa-2.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e4dadf77b9fedc0bfa53417cc28dc2278a26d4cbd95c29f8927ad4d8fe0a7ef9", size = 2166641, upload-time = "2025-10-24T07:19:10.485Z" }, - { url = "https://files.pythonhosted.org/packages/12/f7/a6f9ec2806cf2d50826980cdb4b3cffc7691dc6f95e13cc728846d5cb793/lupa-2.6-cp314-cp314-win32.whl", hash = "sha256:cb34169c6fa3bab3e8ac58ca21b8a7102f6a94b6a5d08d3636312f3f02fafd8f", size = 1456857, upload-time = "2025-10-24T07:19:37.989Z" }, - { url = "https://files.pythonhosted.org/packages/c5/de/df71896f25bdc18360fdfa3b802cd7d57d7fede41a0e9724a4625b412c85/lupa-2.6-cp314-cp314-win_amd64.whl", hash = "sha256:b74f944fe46c421e25d0f8692aef1e842192f6f7f68034201382ac440ef9ea67", size = 1731191, upload-time = "2025-10-24T07:19:40.281Z" }, - { url = "https://files.pythonhosted.org/packages/47/3c/a1f23b01c54669465f5f4c4083107d496fbe6fb45998771420e9aadcf145/lupa-2.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0e21b716408a21ab65723f8841cf7f2f37a844b7a965eeabb785e27fca4099cf", size = 999343, upload-time = "2025-10-24T07:19:12.519Z" }, - { url = "https://files.pythonhosted.org/packages/c5/6d/501994291cb640bfa2ccf7f554be4e6914afa21c4026bd01bff9ca8aac57/lupa-2.6-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:589db872a141bfff828340079bbdf3e9a31f2689f4ca0d88f97d9e8c2eae6142", size = 2000730, upload-time = "2025-10-24T07:19:14.869Z" }, - { url = "https://files.pythonhosted.org/packages/53/a5/457ffb4f3f20469956c2d4c4842a7675e884efc895b2f23d126d23e126cc/lupa-2.6-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:cd852a91a4a9d4dcbb9a58100f820a75a425703ec3e3f049055f60b8533b7953", size = 1021553, upload-time = "2025-10-24T07:19:17.123Z" }, - { url = "https://files.pythonhosted.org/packages/51/6b/36bb5a5d0960f2a5c7c700e0819abb76fd9bf9c1d8a66e5106416d6e9b14/lupa-2.6-cp314-cp314t-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:0334753be028358922415ca97a64a3048e4ed155413fc4eaf87dd0a7e2752983", size = 1133275, upload-time = "2025-10-24T07:19:20.51Z" }, - { url = "https://files.pythonhosted.org/packages/19/86/202ff4429f663013f37d2229f6176ca9f83678a50257d70f61a0a97281bf/lupa-2.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:661d895cd38c87658a34780fac54a690ec036ead743e41b74c3fb81a9e65a6aa", size = 1038441, upload-time = "2025-10-24T07:19:22.509Z" }, - { url = "https://files.pythonhosted.org/packages/a7/42/d8125f8e420714e5b52e9c08d88b5329dfb02dcca731b4f21faaee6cc5b5/lupa-2.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aa58454ccc13878cc177c62529a2056be734da16369e451987ff92784994ca7", size = 2058324, upload-time = "2025-10-24T07:19:24.979Z" }, - { url = "https://files.pythonhosted.org/packages/2b/2c/47bf8b84059876e877a339717ddb595a4a7b0e8740bacae78ba527562e1c/lupa-2.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1425017264e470c98022bba8cff5bd46d054a827f5df6b80274f9cc71dafd24f", size = 1060250, upload-time = "2025-10-24T07:19:27.262Z" }, - { url = "https://files.pythonhosted.org/packages/c2/06/d88add2b6406ca1bdec99d11a429222837ca6d03bea42ca75afa169a78cb/lupa-2.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:224af0532d216e3105f0a127410f12320f7c5f1aa0300bdf9646b8d9afb0048c", size = 1151126, upload-time = "2025-10-24T07:19:29.522Z" }, - { url = "https://files.pythonhosted.org/packages/b4/a0/89e6a024c3b4485b89ef86881c9d55e097e7cb0bdb74efb746f2fa6a9a76/lupa-2.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9abb98d5a8fd27c8285302e82199f0e56e463066f88f619d6594a450bf269d80", size = 2153693, upload-time = "2025-10-24T07:19:31.379Z" }, - { url = "https://files.pythonhosted.org/packages/b6/36/a0f007dc58fc1bbf51fb85dcc82fcb1f21b8c4261361de7dab0e3d8521ef/lupa-2.6-cp314-cp314t-win32.whl", hash = "sha256:1849efeba7a8f6fb8aa2c13790bee988fd242ae404bd459509640eeea3d1e291", size = 1590104, upload-time = "2025-10-24T07:19:33.514Z" }, - { url = "https://files.pythonhosted.org/packages/7d/5e/db903ce9cf82c48d6b91bf6d63ae4c8d0d17958939a4e04ba6b9f38b8643/lupa-2.6-cp314-cp314t-win_amd64.whl", hash = "sha256:fc1498d1a4fc028bc521c26d0fad4ca00ed63b952e32fb95949bda76a04bad52", size = 1913818, upload-time = "2025-10-24T07:19:36.039Z" }, -] - [[package]] name = "markdown-it-py" -version = "4.0.0" +version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, ] [[package]] name = "mcp" -version = "1.26.0" +version = "1.27.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -631,9 +590,9 @@ dependencies = [ { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/6d/62e76bbb8144d6ed86e202b5edd8a4cb631e7c8130f3f4893c3f90262b10/mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66", size = 608005, upload-time = "2026-01-24T19:40:32.468Z" } +sdist = { url = "https://files.pythonhosted.org/packages/38/83/d1efe7c2980d8a3afa476f4e3d42d53dd54c0ab94c27bee5d755b45c8b73/mcp-1.27.1.tar.gz", hash = "sha256:0f47e1820f8f8f941466b39749eb1d1839a04caddca2bc60e9d46e8a99914924", size = 608458, upload-time = "2026-05-08T16:50:12.601Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" }, + { url = "https://files.pythonhosted.org/packages/fd/73/42d9596facebdb533b7f0b86c1b0364ef350d1f8ba78b1052e8a58b48b65/mcp-1.27.1-py3-none-any.whl", hash = "sha256:1af3c4203b329430fde7a87b4fcb6392a041f5cb851fd68fc674016ab4e7c06f", size = 216260, upload-time = "2026-05-08T16:50:10.547Z" }, ] [[package]] @@ -647,11 +606,11 @@ wheels = [ [[package]] name = "more-itertools" -version = "10.8.0" +version = "11.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/f7/139d22fef48ac78127d18e01d80cf1be40236ae489769d17f35c3d425293/more_itertools-11.0.2.tar.gz", hash = "sha256:392a9e1e362cbc106a2457d37cabf9b36e5e12efd4ebff1654630e76597df804", size = 144659, upload-time = "2026-04-09T15:01:33.297Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, + { url = "https://files.pythonhosted.org/packages/cb/98/6af411189d9413534c3eb691182bff1f5c6d44ed2f93f2edfe52a1bbceb8/more_itertools-11.0.2-py3-none-any.whl", hash = "sha256:6e35b35f818b01f691643c6c611bc0902f2e92b46c18fffa77ae1e7c46e912e4", size = 71939, upload-time = "2026-04-09T15:01:32.21Z" }, ] [[package]] @@ -668,51 +627,51 @@ wheels = [ [[package]] name = "opentelemetry-api" -version = "1.39.1" +version = "1.41.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/fc/b7564cbef36601aef0d6c9bc01f7badb64be8e862c2e1c3c5c3b43b53e4f/opentelemetry_api-1.41.1.tar.gz", hash = "sha256:0ad1814d73b875f84494387dae86ce0b12c68556331ce6ce8fe789197c949621", size = 71416, upload-time = "2026-04-24T13:15:38.262Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" }, + { url = "https://files.pythonhosted.org/packages/29/59/3e7118ed140f76b0982ba4321bdaed1997a0473f9720de2d10788a577033/opentelemetry_api-1.41.1-py3-none-any.whl", hash = "sha256:a22df900e75c76dc08440710e51f52f1aa6b451b429298896023e60db5b3139f", size = 69007, upload-time = "2026-04-24T13:15:15.662Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] [[package]] name = "pathable" -version = "0.4.4" +version = "0.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/67/93/8f2c2075b180c12c1e9f6a09d1a985bc2036906b13dff1d8917e395f2048/pathable-0.4.4.tar.gz", hash = "sha256:6905a3cd17804edfac7875b5f6c9142a218c7caef78693c2dbbbfbac186d88b2", size = 8124, upload-time = "2025-01-10T18:43:13.247Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/55/b748445cb4ea6b125626f15379be7c96d1035d4fa3e8fee362fa92298abf/pathable-0.5.0.tar.gz", hash = "sha256:d81938348a1cacb525e7c75166270644782c0fb9c8cecc16be033e71427e0ef1", size = 16655, upload-time = "2026-02-20T08:47:00.748Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/eb/b6260b31b1a96386c0a880edebe26f89669098acea8e0318bff6adb378fd/pathable-0.4.4-py3-none-any.whl", hash = "sha256:5ae9e94793b6ef5a4cbe0a7ce9dbbefc1eec38df253763fd0aeeacf2762dbbc2", size = 9592, upload-time = "2025-01-10T18:43:11.88Z" }, -] - -[[package]] -name = "pathvalidate" -version = "3.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fa/2a/52a8da6fe965dea6192eb716b357558e103aea0a1e9a8352ad575a8406ca/pathvalidate-3.3.1.tar.gz", hash = "sha256:b18c07212bfead624345bb8e1d6141cdcf15a39736994ea0b94035ad2b1ba177", size = 63262, upload-time = "2025-06-15T09:07:20.736Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/70/875f4a23bfc4731703a5835487d0d2fb999031bd415e7d17c0ae615c18b7/pathvalidate-3.3.1-py3-none-any.whl", hash = "sha256:5263baab691f8e1af96092fa5137ee17df5bdfbd6cff1fcac4d6ef4bc2e1735f", size = 24305, upload-time = "2025-06-15T09:07:19.117Z" }, + { url = "https://files.pythonhosted.org/packages/52/96/5a770e5c461462575474468e5af931cff9de036e7c2b4fea23c1c58d2cbe/pathable-0.5.0-py3-none-any.whl", hash = "sha256:646e3d09491a6351a0c82632a09c02cdf70a252e73196b36d8a15ba0a114f0a6", size = 16867, upload-time = "2026-02-20T08:46:59.536Z" }, ] [[package]] name = "platformdirs" -version = "4.5.0" +version = "4.9.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" }, + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, ] [[package]] -name = "prometheus-client" -version = "0.24.1" +name = "pluggy" +version = "1.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f0/58/a794d23feb6b00fc0c72787d7e87d872a6730dd9ed7c7b3e954637d8f280/prometheus_client-0.24.1.tar.gz", hash = "sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9", size = 85616, upload-time = "2026-01-14T15:26:26.965Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl", hash = "sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055", size = 64057, upload-time = "2026-01-14T15:26:24.42Z" }, + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] @@ -724,29 +683,43 @@ dependencies = [ { name = "httpx" }, ] +[package.dev-dependencies] +dev = [ + { name = "bandit" }, + { name = "pytest" }, + { name = "vulture" }, +] + [package.metadata] requires-dist = [ - { name = "fastmcp", specifier = "==2.14.0" }, + { name = "fastmcp", specifier = "==3.2.4" }, { name = "httpx", specifier = "==0.28.1" }, ] +[package.metadata.requires-dev] +dev = [ + { name = "bandit", specifier = "==1.8.3" }, + { name = "pytest", specifier = "==9.0.3" }, + { name = "vulture", specifier = "==2.14" }, +] + [[package]] name = "py-key-value-aio" -version = "0.3.0" +version = "0.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "beartype" }, - { name = "py-key-value-shared" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/93/ce/3136b771dddf5ac905cc193b461eb67967cf3979688c6696e1f2cdcde7ea/py_key_value_aio-0.3.0.tar.gz", hash = "sha256:858e852fcf6d696d231266da66042d3355a7f9871650415feef9fca7a6cd4155", size = 50801, upload-time = "2025-11-17T16:50:04.711Z" } +sdist = { url = "https://files.pythonhosted.org/packages/04/3c/0397c072a38d4bc580994b42e0c90c5f44f679303489e4376289534735e5/py_key_value_aio-0.4.4.tar.gz", hash = "sha256:e3012e6243ed7cc09bb05457bd4d03b1ba5c2b1ca8700096b3927db79ffbbe55", size = 92300, upload-time = "2026-02-16T21:21:43.245Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/10/72f6f213b8f0bce36eff21fda0a13271834e9eeff7f9609b01afdc253c79/py_key_value_aio-0.3.0-py3-none-any.whl", hash = "sha256:1c781915766078bfd608daa769fefb97e65d1d73746a3dfb640460e322071b64", size = 96342, upload-time = "2025-11-17T16:50:03.801Z" }, + { url = "https://files.pythonhosted.org/packages/32/69/f1b537ee70b7def42d63124a539ed3026a11a3ffc3086947a1ca6e861868/py_key_value_aio-0.4.4-py3-none-any.whl", hash = "sha256:18e17564ecae61b987f909fc2cd41ee2012c84b4b1dcb8c055cf8b4bc1bf3f5d", size = 152291, upload-time = "2026-02-16T21:21:44.241Z" }, ] [package.optional-dependencies] -disk = [ - { name = "diskcache" }, - { name = "pathvalidate" }, +filetree = [ + { name = "aiofile" }, + { name = "anyio" }, ] keyring = [ { name = "keyring" }, @@ -754,35 +727,19 @@ keyring = [ memory = [ { name = "cachetools" }, ] -redis = [ - { name = "redis" }, -] - -[[package]] -name = "py-key-value-shared" -version = "0.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "beartype" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7b/e4/1971dfc4620a3a15b4579fe99e024f5edd6e0967a71154771a059daff4db/py_key_value_shared-0.3.0.tar.gz", hash = "sha256:8fdd786cf96c3e900102945f92aa1473138ebe960ef49da1c833790160c28a4b", size = 11666, upload-time = "2025-11-17T16:50:06.849Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/e4/b8b0a03ece72f47dce2307d36e1c34725b7223d209fc679315ffe6a4e2c3/py_key_value_shared-0.3.0-py3-none-any.whl", hash = "sha256:5b0efba7ebca08bb158b1e93afc2f07d30b8f40c2fc12ce24a4c0d84f42f9298", size = 19560, upload-time = "2025-11-17T16:50:05.954Z" }, -] [[package]] name = "pycparser" -version = "2.23" +version = "3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] [[package]] name = "pydantic" -version = "2.11.9" +version = "2.13.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -790,9 +747,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ff/5d/09a551ba512d7ca404d785072700d3f6727a02f6f3c24ecfd081c7cf0aa8/pydantic-2.11.9.tar.gz", hash = "sha256:6b8ffda597a14812a7975c90b82a8a2e777d9257aba3453f973acd3c032a18e2", size = 788495, upload-time = "2025-09-13T11:26:39.325Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/d3/108f2006987c58e76691d5ae5d200dd3e0f532cb4e5fa3560751c3a1feba/pydantic-2.11.9-py3-none-any.whl", hash = "sha256:c42dd626f5cfc1c6950ce6205ea58c93efa406da65f479dcb4029d5934857da2", size = 444855, upload-time = "2025-09-13T11:26:36.909Z" }, + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, ] [package.optional-dependencies] @@ -802,79 +759,91 @@ email = [ [[package]] name = "pydantic-core" -version = "2.33.2" +version = "2.46.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, - { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, - { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, - { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, - { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, - { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, - { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, - { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, - { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, - { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, - { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, - { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, - { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, - { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, - { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, - { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, - { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, - { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, - { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, - { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, - { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, - { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, - { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, - { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, - { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, - { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, - { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, - { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, - { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, - { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, ] [[package]] name = "pydantic-settings" -version = "2.10.1" +version = "2.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload-time = "2025-06-24T13:26:46.841Z" } +sdist = { url = "https://files.pythonhosted.org/packages/07/60/1d1e59c9c90d54591469ada7d268251f71c24bdb765f1a8a832cee8c6653/pydantic_settings-2.14.1.tar.gz", hash = "sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa", size = 235551, upload-time = "2026-05-08T13:40:06.542Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" }, -] - -[[package]] -name = "pydocket" -version = "0.17.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cloudpickle" }, - { name = "fakeredis", extra = ["lua"] }, - { name = "opentelemetry-api" }, - { name = "prometheus-client" }, - { name = "py-key-value-aio", extra = ["memory", "redis"] }, - { name = "python-json-logger" }, - { name = "redis" }, - { name = "rich" }, - { name = "typer" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/90/17/1fb6309e40bbee999c5d881b8213a1078968412d855e064a9a94cfb9eeef/pydocket-0.17.2.tar.gz", hash = "sha256:8f02c68952701eb1b3a70d439b76392d15f1eb9568d0bde6a69997ea5c79c89f", size = 329829, upload-time = "2026-01-26T16:07:56.217Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/53/74/4c9b70753d5721165047e6428ac239ca083118474794deaca5a27d0ab212/pydocket-0.17.2-py3-none-any.whl", hash = "sha256:f43743b84b4e3d614d99b0cad2deebab028104c217745406ecf9e1efb8926e04", size = 91628, upload-time = "2026-01-26T16:07:55.018Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" }, ] [[package]] @@ -902,38 +871,45 @@ crypto = [ [[package]] name = "pyperclip" -version = "1.10.0" +version = "1.11.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/15/99/25f4898cf420efb6f45f519de018f4faea5391114a8618b16736ef3029f1/pyperclip-1.10.0.tar.gz", hash = "sha256:180c8346b1186921c75dfd14d9048a6b5d46bfc499778811952c6dd6eb1ca6be", size = 12193, upload-time = "2025-09-18T00:54:00.384Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/52/d87eba7cb129b81563019d1679026e7a112ef76855d6159d24754dbd2a51/pyperclip-1.11.0.tar.gz", hash = "sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6", size = 12185, upload-time = "2025-09-26T14:40:37.245Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/bc/22540e73c5f5ae18f02924cd3954a6c9a4aa6b713c841a94c98335d333a1/pyperclip-1.10.0-py3-none-any.whl", hash = "sha256:596fbe55dc59263bff26e61d2afbe10223e2fccb5210c9c96a28d6887cfcc7ec", size = 11062, upload-time = "2025-09-18T00:53:59.252Z" }, + { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, ] [[package]] name = "python-dotenv" -version = "1.1.1" +version = "1.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, -] - -[[package]] -name = "python-json-logger" -version = "4.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/29/bf/eca6a3d43db1dae7070f70e160ab20b807627ba953663ba07928cdd3dc58/python_json_logger-4.0.0.tar.gz", hash = "sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f", size = 17683, upload-time = "2025-10-06T04:15:18.984Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2", size = 15548, upload-time = "2025-10-06T04:15:17.553Z" }, + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, ] [[package]] name = "python-multipart" -version = "0.0.20" +version = "0.0.28" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/54/a85eb421fbdd5007bc5af39d0f4ed9fa609e0fedbfdc2adcf0b34526870e/python_multipart-0.0.28.tar.gz", hash = "sha256:8550da197eac0f7ab748961fc9509b999fa2662ea25cef857f05249f6893c0f8", size = 45314, upload-time = "2026-05-10T11:05:16.596Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a2/43bbc5860b5034e2af4ef99a0e04d726ff329c43e192ef3abaa8d7ecfce5/python_multipart-0.0.28-py3-none-any.whl", hash = "sha256:10faac07eb966c3f48dc415f9dee46c04cb10d58d30a35677db8027c825ed9b6", size = 29438, upload-time = "2026-05-10T11:05:15.052Z" }, ] [[package]] @@ -963,173 +939,169 @@ wheels = [ [[package]] name = "pyyaml" -version = "6.0.2" +version = "6.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, - { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, - { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, - { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, - { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, - { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, - { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, - { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, - { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, - { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, - { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, - { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, - { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, - { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, - { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, -] - -[[package]] -name = "redis" -version = "7.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/c8/983d5c6579a411d8a99bc5823cc5712768859b5ce2c8afe1a65b37832c81/redis-7.1.0.tar.gz", hash = "sha256:b1cc3cfa5a2cb9c2ab3ba700864fb0ad75617b41f01352ce5779dabf6d5f9c3c", size = 4796669, upload-time = "2025-11-19T15:54:39.961Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b", size = 354159, upload-time = "2025-11-19T15:54:38.064Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] [[package]] name = "referencing" -version = "0.36.2" +version = "0.37.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "rpds-py" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, -] - -[[package]] -name = "requests" -version = "2.32.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, ] [[package]] name = "rich" -version = "14.1.0" +version = "15.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" }, + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, ] [[package]] name = "rich-rst" -version = "1.3.1" +version = "1.3.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docutils" }, { name = "rich" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b0/69/5514c3a87b5f10f09a34bb011bc0927bc12c596c8dae5915604e71abc386/rich_rst-1.3.1.tar.gz", hash = "sha256:fad46e3ba42785ea8c1785e2ceaa56e0ffa32dbe5410dec432f37e4107c4f383", size = 13839, upload-time = "2024-04-30T04:40:38.125Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/6d/a506aaa4a9eaa945ed8ab2b7347859f53593864289853c5d6d62b77246e0/rich_rst-1.3.2.tar.gz", hash = "sha256:a1196fdddf1e364b02ec68a05e8ff8f6914fee10fbca2e6b6735f166bb0da8d4", size = 14936, upload-time = "2025-10-14T16:49:45.332Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/bc/cc4e3dbc5e7992398dcb7a8eda0cbcf4fb792a0cdb93f857b478bf3cf884/rich_rst-1.3.1-py3-none-any.whl", hash = "sha256:498a74e3896507ab04492d326e794c3ef76e7cda078703aa592d1853d91098c1", size = 11621, upload-time = "2024-04-30T04:40:32.619Z" }, + { url = "https://files.pythonhosted.org/packages/13/2f/b4530fbf948867702d0a3f27de4a6aab1d156f406d72852ab902c4d04de9/rich_rst-1.3.2-py3-none-any.whl", hash = "sha256:a99b4907cbe118cf9d18b0b44de272efa61f15117c61e39ebdc431baf5df722a", size = 12567, upload-time = "2025-10-14T16:49:42.953Z" }, ] [[package]] name = "rpds-py" -version = "0.27.1" +version = "0.30.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e9/dd/2c0cbe774744272b0ae725f44032c77bdcab6e8bcf544bffa3b6e70c8dba/rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8", size = 27479, upload-time = "2025-08-27T12:16:36.024Z" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/fe/38de28dee5df58b8198c743fe2bea0c785c6d40941b9950bac4cdb71a014/rpds_py-0.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90", size = 361887, upload-time = "2025-08-27T12:13:10.233Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9a/4b6c7eedc7dd90986bf0fab6ea2a091ec11c01b15f8ba0a14d3f80450468/rpds_py-0.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5", size = 345795, upload-time = "2025-08-27T12:13:11.65Z" }, - { url = "https://files.pythonhosted.org/packages/6f/0e/e650e1b81922847a09cca820237b0edee69416a01268b7754d506ade11ad/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e", size = 385121, upload-time = "2025-08-27T12:13:13.008Z" }, - { url = "https://files.pythonhosted.org/packages/1b/ea/b306067a712988e2bff00dcc7c8f31d26c29b6d5931b461aa4b60a013e33/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881", size = 398976, upload-time = "2025-08-27T12:13:14.368Z" }, - { url = "https://files.pythonhosted.org/packages/2c/0a/26dc43c8840cb8fe239fe12dbc8d8de40f2365e838f3d395835dde72f0e5/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec", size = 525953, upload-time = "2025-08-27T12:13:15.774Z" }, - { url = "https://files.pythonhosted.org/packages/22/14/c85e8127b573aaf3a0cbd7fbb8c9c99e735a4a02180c84da2a463b766e9e/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb", size = 407915, upload-time = "2025-08-27T12:13:17.379Z" }, - { url = "https://files.pythonhosted.org/packages/ed/7b/8f4fee9ba1fb5ec856eb22d725a4efa3deb47f769597c809e03578b0f9d9/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5", size = 386883, upload-time = "2025-08-27T12:13:18.704Z" }, - { url = "https://files.pythonhosted.org/packages/86/47/28fa6d60f8b74fcdceba81b272f8d9836ac0340570f68f5df6b41838547b/rpds_py-0.27.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a", size = 405699, upload-time = "2025-08-27T12:13:20.089Z" }, - { url = "https://files.pythonhosted.org/packages/d0/fd/c5987b5e054548df56953a21fe2ebed51fc1ec7c8f24fd41c067b68c4a0a/rpds_py-0.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444", size = 423713, upload-time = "2025-08-27T12:13:21.436Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ba/3c4978b54a73ed19a7d74531be37a8bcc542d917c770e14d372b8daea186/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a", size = 562324, upload-time = "2025-08-27T12:13:22.789Z" }, - { url = "https://files.pythonhosted.org/packages/b5/6c/6943a91768fec16db09a42b08644b960cff540c66aab89b74be6d4a144ba/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1", size = 593646, upload-time = "2025-08-27T12:13:24.122Z" }, - { url = "https://files.pythonhosted.org/packages/11/73/9d7a8f4be5f4396f011a6bb7a19fe26303a0dac9064462f5651ced2f572f/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998", size = 558137, upload-time = "2025-08-27T12:13:25.557Z" }, - { url = "https://files.pythonhosted.org/packages/6e/96/6772cbfa0e2485bcceef8071de7821f81aeac8bb45fbfd5542a3e8108165/rpds_py-0.27.1-cp312-cp312-win32.whl", hash = "sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39", size = 221343, upload-time = "2025-08-27T12:13:26.967Z" }, - { url = "https://files.pythonhosted.org/packages/67/b6/c82f0faa9af1c6a64669f73a17ee0eeef25aff30bb9a1c318509efe45d84/rpds_py-0.27.1-cp312-cp312-win_amd64.whl", hash = "sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594", size = 232497, upload-time = "2025-08-27T12:13:28.326Z" }, - { url = "https://files.pythonhosted.org/packages/e1/96/2817b44bd2ed11aebacc9251da03689d56109b9aba5e311297b6902136e2/rpds_py-0.27.1-cp312-cp312-win_arm64.whl", hash = "sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502", size = 222790, upload-time = "2025-08-27T12:13:29.71Z" }, - { url = "https://files.pythonhosted.org/packages/cc/77/610aeee8d41e39080c7e14afa5387138e3c9fa9756ab893d09d99e7d8e98/rpds_py-0.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b", size = 361741, upload-time = "2025-08-27T12:13:31.039Z" }, - { url = "https://files.pythonhosted.org/packages/3a/fc/c43765f201c6a1c60be2043cbdb664013def52460a4c7adace89d6682bf4/rpds_py-0.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf", size = 345574, upload-time = "2025-08-27T12:13:32.902Z" }, - { url = "https://files.pythonhosted.org/packages/20/42/ee2b2ca114294cd9847d0ef9c26d2b0851b2e7e00bf14cc4c0b581df0fc3/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83", size = 385051, upload-time = "2025-08-27T12:13:34.228Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e8/1e430fe311e4799e02e2d1af7c765f024e95e17d651612425b226705f910/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf", size = 398395, upload-time = "2025-08-27T12:13:36.132Z" }, - { url = "https://files.pythonhosted.org/packages/82/95/9dc227d441ff2670651c27a739acb2535ccaf8b351a88d78c088965e5996/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2", size = 524334, upload-time = "2025-08-27T12:13:37.562Z" }, - { url = "https://files.pythonhosted.org/packages/87/01/a670c232f401d9ad461d9a332aa4080cd3cb1d1df18213dbd0d2a6a7ab51/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0", size = 407691, upload-time = "2025-08-27T12:13:38.94Z" }, - { url = "https://files.pythonhosted.org/packages/03/36/0a14aebbaa26fe7fab4780c76f2239e76cc95a0090bdb25e31d95c492fcd/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418", size = 386868, upload-time = "2025-08-27T12:13:40.192Z" }, - { url = "https://files.pythonhosted.org/packages/3b/03/8c897fb8b5347ff6c1cc31239b9611c5bf79d78c984430887a353e1409a1/rpds_py-0.27.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d", size = 405469, upload-time = "2025-08-27T12:13:41.496Z" }, - { url = "https://files.pythonhosted.org/packages/da/07/88c60edc2df74850d496d78a1fdcdc7b54360a7f610a4d50008309d41b94/rpds_py-0.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274", size = 422125, upload-time = "2025-08-27T12:13:42.802Z" }, - { url = "https://files.pythonhosted.org/packages/6b/86/5f4c707603e41b05f191a749984f390dabcbc467cf833769b47bf14ba04f/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd", size = 562341, upload-time = "2025-08-27T12:13:44.472Z" }, - { url = "https://files.pythonhosted.org/packages/b2/92/3c0cb2492094e3cd9baf9e49bbb7befeceb584ea0c1a8b5939dca4da12e5/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2", size = 592511, upload-time = "2025-08-27T12:13:45.898Z" }, - { url = "https://files.pythonhosted.org/packages/10/bb/82e64fbb0047c46a168faa28d0d45a7851cd0582f850b966811d30f67ad8/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002", size = 557736, upload-time = "2025-08-27T12:13:47.408Z" }, - { url = "https://files.pythonhosted.org/packages/00/95/3c863973d409210da7fb41958172c6b7dbe7fc34e04d3cc1f10bb85e979f/rpds_py-0.27.1-cp313-cp313-win32.whl", hash = "sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3", size = 221462, upload-time = "2025-08-27T12:13:48.742Z" }, - { url = "https://files.pythonhosted.org/packages/ce/2c/5867b14a81dc217b56d95a9f2a40fdbc56a1ab0181b80132beeecbd4b2d6/rpds_py-0.27.1-cp313-cp313-win_amd64.whl", hash = "sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83", size = 232034, upload-time = "2025-08-27T12:13:50.11Z" }, - { url = "https://files.pythonhosted.org/packages/c7/78/3958f3f018c01923823f1e47f1cc338e398814b92d83cd278364446fac66/rpds_py-0.27.1-cp313-cp313-win_arm64.whl", hash = "sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d", size = 222392, upload-time = "2025-08-27T12:13:52.587Z" }, - { url = "https://files.pythonhosted.org/packages/01/76/1cdf1f91aed5c3a7bf2eba1f1c4e4d6f57832d73003919a20118870ea659/rpds_py-0.27.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228", size = 358355, upload-time = "2025-08-27T12:13:54.012Z" }, - { url = "https://files.pythonhosted.org/packages/c3/6f/bf142541229374287604caf3bb2a4ae17f0a580798fd72d3b009b532db4e/rpds_py-0.27.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92", size = 342138, upload-time = "2025-08-27T12:13:55.791Z" }, - { url = "https://files.pythonhosted.org/packages/1a/77/355b1c041d6be40886c44ff5e798b4e2769e497b790f0f7fd1e78d17e9a8/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2", size = 380247, upload-time = "2025-08-27T12:13:57.683Z" }, - { url = "https://files.pythonhosted.org/packages/d6/a4/d9cef5c3946ea271ce2243c51481971cd6e34f21925af2783dd17b26e815/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723", size = 390699, upload-time = "2025-08-27T12:13:59.137Z" }, - { url = "https://files.pythonhosted.org/packages/3a/06/005106a7b8c6c1a7e91b73169e49870f4af5256119d34a361ae5240a0c1d/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802", size = 521852, upload-time = "2025-08-27T12:14:00.583Z" }, - { url = "https://files.pythonhosted.org/packages/e5/3e/50fb1dac0948e17a02eb05c24510a8fe12d5ce8561c6b7b7d1339ab7ab9c/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f", size = 402582, upload-time = "2025-08-27T12:14:02.034Z" }, - { url = "https://files.pythonhosted.org/packages/cb/b0/f4e224090dc5b0ec15f31a02d746ab24101dd430847c4d99123798661bfc/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2", size = 384126, upload-time = "2025-08-27T12:14:03.437Z" }, - { url = "https://files.pythonhosted.org/packages/54/77/ac339d5f82b6afff1df8f0fe0d2145cc827992cb5f8eeb90fc9f31ef7a63/rpds_py-0.27.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21", size = 399486, upload-time = "2025-08-27T12:14:05.443Z" }, - { url = "https://files.pythonhosted.org/packages/d6/29/3e1c255eee6ac358c056a57d6d6869baa00a62fa32eea5ee0632039c50a3/rpds_py-0.27.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef", size = 414832, upload-time = "2025-08-27T12:14:06.902Z" }, - { url = "https://files.pythonhosted.org/packages/3f/db/6d498b844342deb3fa1d030598db93937a9964fcf5cb4da4feb5f17be34b/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081", size = 557249, upload-time = "2025-08-27T12:14:08.37Z" }, - { url = "https://files.pythonhosted.org/packages/60/f3/690dd38e2310b6f68858a331399b4d6dbb9132c3e8ef8b4333b96caf403d/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd", size = 587356, upload-time = "2025-08-27T12:14:10.034Z" }, - { url = "https://files.pythonhosted.org/packages/86/e3/84507781cccd0145f35b1dc32c72675200c5ce8d5b30f813e49424ef68fc/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7", size = 555300, upload-time = "2025-08-27T12:14:11.783Z" }, - { url = "https://files.pythonhosted.org/packages/e5/ee/375469849e6b429b3516206b4580a79e9ef3eb12920ddbd4492b56eaacbe/rpds_py-0.27.1-cp313-cp313t-win32.whl", hash = "sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688", size = 216714, upload-time = "2025-08-27T12:14:13.629Z" }, - { url = "https://files.pythonhosted.org/packages/21/87/3fc94e47c9bd0742660e84706c311a860dcae4374cf4a03c477e23ce605a/rpds_py-0.27.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797", size = 228943, upload-time = "2025-08-27T12:14:14.937Z" }, - { url = "https://files.pythonhosted.org/packages/70/36/b6e6066520a07cf029d385de869729a895917b411e777ab1cde878100a1d/rpds_py-0.27.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334", size = 362472, upload-time = "2025-08-27T12:14:16.333Z" }, - { url = "https://files.pythonhosted.org/packages/af/07/b4646032e0dcec0df9c73a3bd52f63bc6c5f9cda992f06bd0e73fe3fbebd/rpds_py-0.27.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33", size = 345676, upload-time = "2025-08-27T12:14:17.764Z" }, - { url = "https://files.pythonhosted.org/packages/b0/16/2f1003ee5d0af4bcb13c0cf894957984c32a6751ed7206db2aee7379a55e/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a", size = 385313, upload-time = "2025-08-27T12:14:19.829Z" }, - { url = "https://files.pythonhosted.org/packages/05/cd/7eb6dd7b232e7f2654d03fa07f1414d7dfc980e82ba71e40a7c46fd95484/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b", size = 399080, upload-time = "2025-08-27T12:14:21.531Z" }, - { url = "https://files.pythonhosted.org/packages/20/51/5829afd5000ec1cb60f304711f02572d619040aa3ec033d8226817d1e571/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7", size = 523868, upload-time = "2025-08-27T12:14:23.485Z" }, - { url = "https://files.pythonhosted.org/packages/05/2c/30eebca20d5db95720ab4d2faec1b5e4c1025c473f703738c371241476a2/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136", size = 408750, upload-time = "2025-08-27T12:14:24.924Z" }, - { url = "https://files.pythonhosted.org/packages/90/1a/cdb5083f043597c4d4276eae4e4c70c55ab5accec078da8611f24575a367/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff", size = 387688, upload-time = "2025-08-27T12:14:27.537Z" }, - { url = "https://files.pythonhosted.org/packages/7c/92/cf786a15320e173f945d205ab31585cc43969743bb1a48b6888f7a2b0a2d/rpds_py-0.27.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9", size = 407225, upload-time = "2025-08-27T12:14:28.981Z" }, - { url = "https://files.pythonhosted.org/packages/33/5c/85ee16df5b65063ef26017bef33096557a4c83fbe56218ac7cd8c235f16d/rpds_py-0.27.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60", size = 423361, upload-time = "2025-08-27T12:14:30.469Z" }, - { url = "https://files.pythonhosted.org/packages/4b/8e/1c2741307fcabd1a334ecf008e92c4f47bb6f848712cf15c923becfe82bb/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e", size = 562493, upload-time = "2025-08-27T12:14:31.987Z" }, - { url = "https://files.pythonhosted.org/packages/04/03/5159321baae9b2222442a70c1f988cbbd66b9be0675dd3936461269be360/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212", size = 592623, upload-time = "2025-08-27T12:14:33.543Z" }, - { url = "https://files.pythonhosted.org/packages/ff/39/c09fd1ad28b85bc1d4554a8710233c9f4cefd03d7717a1b8fbfd171d1167/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675", size = 558800, upload-time = "2025-08-27T12:14:35.436Z" }, - { url = "https://files.pythonhosted.org/packages/c5/d6/99228e6bbcf4baa764b18258f519a9035131d91b538d4e0e294313462a98/rpds_py-0.27.1-cp314-cp314-win32.whl", hash = "sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3", size = 221943, upload-time = "2025-08-27T12:14:36.898Z" }, - { url = "https://files.pythonhosted.org/packages/be/07/c802bc6b8e95be83b79bdf23d1aa61d68324cb1006e245d6c58e959e314d/rpds_py-0.27.1-cp314-cp314-win_amd64.whl", hash = "sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456", size = 233739, upload-time = "2025-08-27T12:14:38.386Z" }, - { url = "https://files.pythonhosted.org/packages/c8/89/3e1b1c16d4c2d547c5717377a8df99aee8099ff050f87c45cb4d5fa70891/rpds_py-0.27.1-cp314-cp314-win_arm64.whl", hash = "sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3", size = 223120, upload-time = "2025-08-27T12:14:39.82Z" }, - { url = "https://files.pythonhosted.org/packages/62/7e/dc7931dc2fa4a6e46b2a4fa744a9fe5c548efd70e0ba74f40b39fa4a8c10/rpds_py-0.27.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2", size = 358944, upload-time = "2025-08-27T12:14:41.199Z" }, - { url = "https://files.pythonhosted.org/packages/e6/22/4af76ac4e9f336bfb1a5f240d18a33c6b2fcaadb7472ac7680576512b49a/rpds_py-0.27.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4", size = 342283, upload-time = "2025-08-27T12:14:42.699Z" }, - { url = "https://files.pythonhosted.org/packages/1c/15/2a7c619b3c2272ea9feb9ade67a45c40b3eeb500d503ad4c28c395dc51b4/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e", size = 380320, upload-time = "2025-08-27T12:14:44.157Z" }, - { url = "https://files.pythonhosted.org/packages/a2/7d/4c6d243ba4a3057e994bb5bedd01b5c963c12fe38dde707a52acdb3849e7/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817", size = 391760, upload-time = "2025-08-27T12:14:45.845Z" }, - { url = "https://files.pythonhosted.org/packages/b4/71/b19401a909b83bcd67f90221330bc1ef11bc486fe4e04c24388d28a618ae/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec", size = 522476, upload-time = "2025-08-27T12:14:47.364Z" }, - { url = "https://files.pythonhosted.org/packages/e4/44/1a3b9715c0455d2e2f0f6df5ee6d6f5afdc423d0773a8a682ed2b43c566c/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a", size = 403418, upload-time = "2025-08-27T12:14:49.991Z" }, - { url = "https://files.pythonhosted.org/packages/1c/4b/fb6c4f14984eb56673bc868a66536f53417ddb13ed44b391998100a06a96/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8", size = 384771, upload-time = "2025-08-27T12:14:52.159Z" }, - { url = "https://files.pythonhosted.org/packages/c0/56/d5265d2d28b7420d7b4d4d85cad8ef891760f5135102e60d5c970b976e41/rpds_py-0.27.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48", size = 400022, upload-time = "2025-08-27T12:14:53.859Z" }, - { url = "https://files.pythonhosted.org/packages/8f/e9/9f5fc70164a569bdd6ed9046486c3568d6926e3a49bdefeeccfb18655875/rpds_py-0.27.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb", size = 416787, upload-time = "2025-08-27T12:14:55.673Z" }, - { url = "https://files.pythonhosted.org/packages/d4/64/56dd03430ba491db943a81dcdef115a985aac5f44f565cd39a00c766d45c/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734", size = 557538, upload-time = "2025-08-27T12:14:57.245Z" }, - { url = "https://files.pythonhosted.org/packages/3f/36/92cc885a3129993b1d963a2a42ecf64e6a8e129d2c7cc980dbeba84e55fb/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb", size = 588512, upload-time = "2025-08-27T12:14:58.728Z" }, - { url = "https://files.pythonhosted.org/packages/dd/10/6b283707780a81919f71625351182b4f98932ac89a09023cb61865136244/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0", size = 555813, upload-time = "2025-08-27T12:15:00.334Z" }, - { url = "https://files.pythonhosted.org/packages/04/2e/30b5ea18c01379da6272a92825dd7e53dc9d15c88a19e97932d35d430ef7/rpds_py-0.27.1-cp314-cp314t-win32.whl", hash = "sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a", size = 217385, upload-time = "2025-08-27T12:15:01.937Z" }, - { url = "https://files.pythonhosted.org/packages/32/7d/97119da51cb1dd3f2f3c0805f155a3aa4a95fa44fe7d78ae15e69edf4f34/rpds_py-0.27.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772", size = 230097, upload-time = "2025-08-27T12:15:03.961Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, ] [[package]] @@ -1145,71 +1117,39 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, ] -[[package]] -name = "shellingham" -version = "1.5.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, -] - -[[package]] -name = "sortedcontainers" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, -] - [[package]] name = "sse-starlette" -version = "3.0.2" +version = "3.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, + { name = "starlette" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/6f/22ed6e33f8a9e76ca0a412405f31abb844b779d52c5f96660766edcd737c/sse_starlette-3.0.2.tar.gz", hash = "sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a", size = 20985, upload-time = "2025-07-27T09:07:44.565Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/2b/58abc2d1fd397e7dde08e947e05c884d8ef2f78d5e2588c17a12d42d6994/sse_starlette-3.4.4.tar.gz", hash = "sha256:07e0fa0460138baf25cdd5fb28683472c3995dc1642225191b3832d62526bcb0", size = 31819, upload-time = "2026-05-12T17:37:17.019Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/10/c78f463b4ef22eef8491f218f692be838282cd65480f6e423d7730dfd1fb/sse_starlette-3.0.2-py3-none-any.whl", hash = "sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a", size = 11297, upload-time = "2025-07-27T09:07:43.268Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/805710444ea8cc75fbf70b920ed431a560c4bf9c57f7d5a3117213189399/sse_starlette-3.4.4-py3-none-any.whl", hash = "sha256:3f4dd50d8aed2771a091f3a83000323fc3844541c16b4fe585ae2420cc6df973", size = 16514, upload-time = "2026-05-12T17:37:15.601Z" }, ] [[package]] name = "starlette" -version = "0.48.0" +version = "1.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a7/a5/d6f429d43394057b67a6b5bbe6eae2f77a6bf7459d961fdb224bf206eee6/starlette-0.48.0.tar.gz", hash = "sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46", size = 2652949, upload-time = "2025-09-13T08:41:05.699Z" } +sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/be/72/2db2f49247d0a18b4f1bb9a5a39a0162869acf235f3a96418363947b3d46/starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659", size = 73736, upload-time = "2025-09-13T08:41:03.869Z" }, + { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" }, ] [[package]] -name = "typer" -version = "0.21.1" +name = "stevedore" +version = "5.7.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "rich" }, - { name = "shellingham" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/36/bf/8825b5929afd84d0dabd606c67cd57b8388cb3ec385f7ef19c5cc2202069/typer-0.21.1.tar.gz", hash = "sha256:ea835607cd752343b6b2b7ce676893e5a0324082268b48f27aa058bdb7d2145d", size = 110371, upload-time = "2026-01-06T11:21:10.989Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6d/90764092216fa560f6587f83bb70113a8ba510ba436c6476a2b47359057c/stevedore-5.7.0.tar.gz", hash = "sha256:31dd6fe6b3cbe921e21dcefabc9a5f1cf848cf538a1f27543721b8ca09948aa3", size = 516200, upload-time = "2026-02-20T13:27:06.765Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01", size = 47381, upload-time = "2026-01-06T11:21:09.824Z" }, + { url = "https://files.pythonhosted.org/packages/69/06/36d260a695f383345ab5bbc3fd447249594ae2fa8dfd19c533d5ae23f46b/stevedore-5.7.0-py3-none-any.whl", hash = "sha256:fd25efbb32f1abb4c9e502f385f0018632baac11f9ee5d1b70f88cc5e22ad4ed", size = 54483, upload-time = "2026-02-20T13:27:05.561Z" }, ] [[package]] @@ -1223,74 +1163,167 @@ wheels = [ [[package]] name = "typing-inspection" -version = "0.4.1" +version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] [[package]] -name = "urllib3" -version = "2.5.0" +name = "uncalled-for" +version = "0.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/82/345cc927f7fbdae6065e7768759932fcc827fc20b29b45dfbafa2f1f7da4/uncalled_for-0.3.2.tar.gz", hash = "sha256:89f5dbcd71e2b8f47c030b1fa302e6cce2ec795d1ac565eeb6525c5fe55cb8a2", size = 50032, upload-time = "2026-05-06T13:38:25.204Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, + { url = "https://files.pythonhosted.org/packages/3b/25/2c87754f3a9e692315f7b811244090e68f362979fc8886b3fbd2985a1d8c/uncalled_for-0.3.2-py3-none-any.whl", hash = "sha256:0ff60b142c7d1f8070bde9d42afaa70aedc77dcc10998c227687e9c15713418e", size = 11444, upload-time = "2026-05-06T13:38:24.025Z" }, ] [[package]] name = "uvicorn" -version = "0.36.0" +version = "0.46.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ef/5e/f0cd46063a02fd8515f0e880c37d2657845b7306c16ce6c4ffc44afd9036/uvicorn-0.36.0.tar.gz", hash = "sha256:527dc68d77819919d90a6b267be55f0e76704dca829d34aea9480be831a9b9d9", size = 80032, upload-time = "2025-09-20T01:07:14.418Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/93/041fca8274050e40e6791f267d82e0e2e27dd165627bd640d3e0e378d877/uvicorn-0.46.0.tar.gz", hash = "sha256:fb9da0926999cc6cb22dc7cd71a94a632f078e6ae47ff683c5c420750fb7413d", size = 88758, upload-time = "2026-04-23T07:16:00.151Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/06/5cc0542b47c0338c1cb676b348e24a1c29acabc81000bced518231dded6f/uvicorn-0.36.0-py3-none-any.whl", hash = "sha256:6bb4ba67f16024883af8adf13aba3a9919e415358604ce46780d3f9bdc36d731", size = 67675, upload-time = "2025-09-20T01:07:12.984Z" }, + { url = "https://files.pythonhosted.org/packages/31/a3/5b1562db76a5a488274b2332a97199b32d0442aca0ed193697fd47786316/uvicorn-0.46.0-py3-none-any.whl", hash = "sha256:bbebbcbed972d162afca128605223022bedd345b7bc7855ce66deb31487a9048", size = 70926, upload-time = "2026-04-23T07:15:58.355Z" }, +] + +[[package]] +name = "vulture" +version = "2.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/25/925f35db758a0f9199113aaf61d703de891676b082bd7cf73ea01d6000f7/vulture-2.14.tar.gz", hash = "sha256:cb8277902a1138deeab796ec5bef7076a6e0248ca3607a3f3dee0b6d9e9b8415", size = 58823, upload-time = "2024-12-08T17:39:43.319Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/56/0cc15b8ff2613c1d5c3dc1f3f576ede1c43868c1bc2e5ccaa2d4bcd7974d/vulture-2.14-py2.py3-none-any.whl", hash = "sha256:d9a90dba89607489548a49d557f8bac8112bd25d3cbc8aeef23e860811bd5ed9", size = 28915, upload-time = "2024-12-08T17:39:40.573Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, ] [[package]] name = "websockets" -version = "15.0.1" +version = "16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, - { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, - { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, - { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, - { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, - { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, - { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, - { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, - { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, - { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, - { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, - { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, - { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, - { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, - { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, - { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, - { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, - { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, - { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, - { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, - { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, ] [[package]] name = "zipp" -version = "3.23.0" +version = "3.23.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/21/093488dfc7cc8964ded15ab726fad40f25fd3d788fd741cc1c5a17d78ee8/zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110", size = 25965, upload-time = "2026-04-13T23:21:46.6Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/08/8a/0861bec20485572fbddf3dfba2910e38fe249796cb73ecdeb74e07eeb8d3/zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc", size = 10378, upload-time = "2026-04-13T23:21:45.386Z" }, ] diff --git a/osv-scanner.toml b/osv-scanner.toml new file mode 100644 index 0000000000..5d029efdf6 --- /dev/null +++ b/osv-scanner.toml @@ -0,0 +1,30 @@ +# osv-scanner per-vulnerability ignore list. +# +# Each [[IgnoredVulns]] entry must include a `reason` explaining why the +# finding is accepted and an `ignoreUntil` date so the suppression auto-expires +# and gets re-evaluated. See https://github.com/google/osv-scanner for the +# config schema. + +[[IgnoredVulns]] +id = "PYSEC-2025-183" +ignoreUntil = 2026-08-20T00:00:00Z +reason = """ +CVE-2025-45768 is disputed by the pyjwt maintainers. The advisory describes +weak encryption, but the underlying issue is that callers may pick a short +HMAC secret — key-length enforcement is the application's responsibility, not +a defect in the library. We are on pyjwt 2.12.1 (latest at pin time) and +enforce key strength in our own auth code, so this advisory does not apply. +Re-evaluate when a non-disputed advisory or upstream fix lands. +""" + +[[IgnoredVulns]] +id = "PYSEC-2026-89" +ignoreUntil = 2026-08-20T00:00:00Z +reason = """ +False positive caused by a malformed PYSEC record. The equivalent GitHub +Security Advisory (GHSA-5wmx-573v-2qwq) for CVE-2025-69534 declares the issue +fixed in markdown 3.8.1. We are on markdown==3.10.2 (latest release, includes +the fix), but the PYSEC entry's range is [{introduced: "0"}, {}] with no +closing "fixed" event, so osv-scanner flags every version. There is no newer +release to upgrade to. Re-evaluate once the PYSEC record is corrected upstream. +""" diff --git a/permissions/templates/terraform/README.md b/permissions/templates/terraform/README.md index ec41103e5e..1bb037daa2 100644 --- a/permissions/templates/terraform/README.md +++ b/permissions/templates/terraform/README.md @@ -28,12 +28,12 @@ This Terraform configuration creates the necessary IAM role and policies to allo ### Usage Examples -#### Basic deployment (without S3 integration): +#### Basic deployment (without S3 integration) ```bash terraform apply -var="external_id=your-external-id-here" ``` -#### With S3 integration enabled: +#### With S3 integration enabled ```bash terraform apply \ -var="external_id=your-external-id-here" \ @@ -42,14 +42,14 @@ terraform apply \ -var="s3_integration_bucket_account_id=123456789012" ``` -#### Using terraform.tfvars file (Recommended): +#### Using terraform.tfvars file (Recommended) ```bash cp terraform.tfvars.example terraform.tfvars # Edit the file with your values terraform apply ``` -#### Command line variables (Alternative): +#### Command line variables (Alternative) ```bash terraform apply -var="external_id=your-external-id-here" ``` diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index 06941386e5..0000000000 --- a/poetry.lock +++ /dev/null @@ -1,6738 +0,0 @@ -# This file is automatically @generated by Poetry 2.3.4 and should not be changed by hand. - -[[package]] -name = "about-time" -version = "4.2.1" -description = "Easily measure timing and throughput of code blocks, with beautiful human friendly representations." -optional = false -python-versions = ">=3.7, <4" -groups = ["main"] -files = [ - {file = "about-time-4.2.1.tar.gz", hash = "sha256:6a538862d33ce67d997429d14998310e1dbfda6cb7d9bbfbf799c4709847fece"}, - {file = "about_time-4.2.1-py3-none-any.whl", hash = "sha256:8bbf4c75fe13cbd3d72f49a03b02c5c7dca32169b6d49117c257e7eb3eaee341"}, -] - -[[package]] -name = "aiofiles" -version = "24.1.0" -description = "File support for asyncio." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"}, - {file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"}, -] - -[[package]] -name = "aiohappyeyeballs" -version = "2.6.1" -description = "Happy Eyeballs for asyncio" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}, - {file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"}, -] - -[[package]] -name = "aiohttp" -version = "3.13.5" -description = "Async http client/server framework (asyncio)" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "aiohttp-3.13.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:02222e7e233295f40e011c1b00e3b0bd451f22cf853a0304c3595633ee47da4b"}, - {file = "aiohttp-3.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bace460460ed20614fa6bc8cb09966c0b8517b8c58ad8046828c6078d25333b5"}, - {file = "aiohttp-3.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f546a4dc1e6a5edbb9fd1fd6ad18134550e096a5a43f4ad74acfbd834fc6670"}, - {file = "aiohttp-3.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c86969d012e51b8e415a8c6ce96f7857d6a87d6207303ab02d5d11ef0cad2274"}, - {file = "aiohttp-3.13.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b6f6cd1560c5fa427e3b6074bb24d2c64e225afbb7165008903bd42e4e33e28a"}, - {file = "aiohttp-3.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:636bc362f0c5bbc7372bc3ae49737f9e3030dbce469f0f422c8f38079780363d"}, - {file = "aiohttp-3.13.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6a7cbeb06d1070f1d14895eeeed4dac5913b22d7b456f2eb969f11f4b3993796"}, - {file = "aiohttp-3.13.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca9ef7517fd7874a1a08970ae88f497bf5c984610caa0bf40bd7e8450852b95"}, - {file = "aiohttp-3.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:019a67772e034a0e6b9b17c13d0a8fe56ad9fb150fc724b7f3ffd3724288d9e5"}, - {file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f34ecee82858e41dd217734f0c41a532bd066bcaab636ad830f03a30b2a96f2a"}, - {file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4eac02d9af4813ee289cd63a361576da36dba57f5a1ab36377bc2600db0cbb73"}, - {file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4beac52e9fe46d6abf98b0176a88154b742e878fdf209d2248e99fcdf73cd297"}, - {file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c180f480207a9b2475f2b8d8bd7204e47aec952d084b2a2be58a782ffcf96074"}, - {file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2837fb92951564d6339cedae4a7231692aa9f73cbc4fb2e04263b96844e03b4e"}, - {file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d9010032a0b9710f58012a1e9c222528763d860ba2ee1422c03473eab47703e7"}, - {file = "aiohttp-3.13.5-cp310-cp310-win32.whl", hash = "sha256:7c4b6668b2b2b9027f209ddf647f2a4407784b5d88b8be4efcc72036f365baf9"}, - {file = "aiohttp-3.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:cd3db5927bf9167d5a6157ddb2f036f6b6b0ad001ac82355d43e97a4bde76d76"}, - {file = "aiohttp-3.13.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ab7229b6f9b5c1ba4910d6c41a9eb11f543eadb3f384df1b4c293f4e73d44d6"}, - {file = "aiohttp-3.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f14c50708bb156b3a3ca7230b3d820199d56a48e3af76fa21c2d6087190fe3d"}, - {file = "aiohttp-3.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7d2f8616f0ff60bd332022279011776c3ac0faa0f1b463f7bb12326fbc97a1c"}, - {file = "aiohttp-3.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2567b72e1ffc3ab25510db43f355b29eeada56c0a622e58dcdb19530eb0a3cb"}, - {file = "aiohttp-3.13.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fb0540c854ac9c0c5ad495908fdfd3e332d553ec731698c0e29b1877ba0d2ec6"}, - {file = "aiohttp-3.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9883051c6972f58bfc4ebb2116345ee2aa151178e99c3f2b2bbe2af712abd13"}, - {file = "aiohttp-3.13.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2294172ce08a82fb7c7273485895de1fa1186cc8294cfeb6aef4af42ad261174"}, - {file = "aiohttp-3.13.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a807cabd5115fb55af198b98178997a5e0e57dead43eb74a93d9c07d6d4a7dc"}, - {file = "aiohttp-3.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6d0d932e0f39c02b80744273cd5c388a2d9bc07760a03164f229c8e02662f6"}, - {file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60869c7ac4aaabe7110f26499f3e6e5696eae98144735b12a9c3d9eae2b51a49"}, - {file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:26d2f8546f1dfa75efa50c3488215a903c0168d253b75fba4210f57ab77a0fb8"}, - {file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1162a1492032c82f14271e831c8f4b49f2b6078f4f5fc74de2c912fa225d51d"}, - {file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8b14eb3262fad0dc2f89c1a43b13727e709504972186ff6a99a3ecaa77102b6c"}, - {file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ca9ac61ac6db4eb6c2a0cd1d0f7e1357647b638ccc92f7e9d8d133e71ed3c6ac"}, - {file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7996023b2ed59489ae4762256c8516df9820f751cf2c5da8ed2fb20ee50abab3"}, - {file = "aiohttp-3.13.5-cp311-cp311-win32.whl", hash = "sha256:77dfa48c9f8013271011e51c00f8ada19851f013cde2c48fca1ba5e0caf5bb06"}, - {file = "aiohttp-3.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:d3a4834f221061624b8887090637db9ad4f61752001eae37d56c52fddade2dc8"}, - {file = "aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9"}, - {file = "aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416"}, - {file = "aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2"}, - {file = "aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4"}, - {file = "aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9"}, - {file = "aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5"}, - {file = "aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e"}, - {file = "aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1"}, - {file = "aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286"}, - {file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9"}, - {file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88"}, - {file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3"}, - {file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b"}, - {file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe"}, - {file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14"}, - {file = "aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3"}, - {file = "aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1"}, - {file = "aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61"}, - {file = "aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832"}, - {file = "aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9"}, - {file = "aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090"}, - {file = "aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b"}, - {file = "aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a"}, - {file = "aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8"}, - {file = "aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665"}, - {file = "aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540"}, - {file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb"}, - {file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46"}, - {file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8"}, - {file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d"}, - {file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6"}, - {file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c"}, - {file = "aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc"}, - {file = "aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83"}, - {file = "aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c"}, - {file = "aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be"}, - {file = "aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25"}, - {file = "aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56"}, - {file = "aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2"}, - {file = "aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a"}, - {file = "aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be"}, - {file = "aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b"}, - {file = "aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94"}, - {file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d"}, - {file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7"}, - {file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772"}, - {file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5"}, - {file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1"}, - {file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b"}, - {file = "aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3"}, - {file = "aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162"}, - {file = "aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a"}, - {file = "aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254"}, - {file = "aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36"}, - {file = "aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f"}, - {file = "aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800"}, - {file = "aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf"}, - {file = "aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b"}, - {file = "aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a"}, - {file = "aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8"}, - {file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be"}, - {file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b"}, - {file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6"}, - {file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037"}, - {file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500"}, - {file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9"}, - {file = "aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8"}, - {file = "aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9"}, - {file = "aiohttp-3.13.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:347542f0ea3f95b2a955ee6656461fa1c776e401ac50ebce055a6c38454a0adf"}, - {file = "aiohttp-3.13.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:178c7b5e62b454c2bc790786e6058c3cc968613b4419251b478c153a4aec32b1"}, - {file = "aiohttp-3.13.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af545c2cffdb0967a96b6249e6f5f7b0d92cdfd267f9d5238d5b9ca63e8edb10"}, - {file = "aiohttp-3.13.5-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:206b7b3ef96e4ce211754f0cd003feb28b7d81f0ad26b8d077a5d5161436067f"}, - {file = "aiohttp-3.13.5-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ee5e86776273de1795947d17bddd6bb19e0365fd2af4289c0d2c5454b6b1d36b"}, - {file = "aiohttp-3.13.5-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:95d14ca7abefde230f7639ec136ade282655431fd5db03c343b19dda72dd1643"}, - {file = "aiohttp-3.13.5-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:912d4b6af530ddb1338a66229dac3a25ff11d4448be3ec3d6340583995f56031"}, - {file = "aiohttp-3.13.5-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e999f0c88a458c836d5fb521814e92ed2172c649200336a6df514987c1488258"}, - {file = "aiohttp-3.13.5-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39380e12bd1f2fdab4285b6e055ad48efbaed5c836433b142ed4f5b9be71036a"}, - {file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9efcc0f11d850cefcafdd9275b9576ad3bfb539bed96807663b32ad99c4d4b88"}, - {file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:147b4f501d0292077f29d5268c16bb7c864a1f054d7001c4c1812c0421ea1ed0"}, - {file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d147004fede1b12f6013a6dbb2a26a986a671a03c6ea740ddc76500e5f1c399f"}, - {file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:9277145d36a01653863899c665243871434694bcc3431922c3b35c978061bdb8"}, - {file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4e704c52438f66fdd89588346183d898bb42167cf88f8b7ff1c0f9fc957c348f"}, - {file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a8a4d3427e8de1312ddf309cc482186466c79895b3a139fed3259fc01dfa9a5b"}, - {file = "aiohttp-3.13.5-cp39-cp39-win32.whl", hash = "sha256:6f497a6876aa4b1a102b04996ce4c1170c7040d83faa9387dd921c16e30d5c83"}, - {file = "aiohttp-3.13.5-cp39-cp39-win_amd64.whl", hash = "sha256:cb979826071c0986a5f08333a36104153478ce6018c58cba7f9caddaf63d5d67"}, - {file = "aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1"}, -] - -[package.dependencies] -aiohappyeyeballs = ">=2.5.0" -aiosignal = ">=1.4.0" -async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""} -attrs = ">=17.3.0" -frozenlist = ">=1.1.1" -multidict = ">=4.5,<7.0" -propcache = ">=0.2.0" -yarl = ">=1.17.0,<2.0" - -[package.extras] -speedups = ["Brotli (>=1.2) ; platform_python_implementation == \"CPython\"", "aiodns (>=3.3.0)", "backports.zstd ; platform_python_implementation == \"CPython\" and python_version < \"3.14\"", "brotlicffi (>=1.2) ; platform_python_implementation != \"CPython\""] - -[[package]] -name = "aiosignal" -version = "1.4.0" -description = "aiosignal: a list of registered asynchronous callbacks" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e"}, - {file = "aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7"}, -] - -[package.dependencies] -frozenlist = ">=1.1.0" -typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} - -[[package]] -name = "alibabacloud-actiontrail20200706" -version = "2.4.1" -description = "Alibaba Cloud ActionTrail (20200706) SDK Library for Python" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "alibabacloud_actiontrail20200706-2.4.1-py3-none-any.whl", hash = "sha256:5dee0009db9b7cba182fbac742820f6a949287a8faafb843b5107f7dc89136da"}, - {file = "alibabacloud_actiontrail20200706-2.4.1.tar.gz", hash = "sha256:b65c6b37a96443fbe625dd5a4dd1be52a7476006a411db75206908b11588ffa8"}, -] - -[package.dependencies] -alibabacloud-endpoint-util = ">=0.0.4,<1.0.0" -alibabacloud-openapi-util = ">=0.2.2,<1.0.0" -alibabacloud-tea-openapi = ">=0.3.16,<1.0.0" -alibabacloud-tea-util = ">=0.3.13,<1.0.0" - -[[package]] -name = "alibabacloud-credentials" -version = "1.0.3" -description = "The alibabacloud credentials module of alibabaCloud Python SDK." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "alibabacloud-credentials-1.0.3.tar.gz", hash = "sha256:9d8707e96afc6f348e23f5677ed15a21c2dfce7cfe6669776548ee4c80e1dfaf"}, - {file = "alibabacloud_credentials-1.0.3-py3-none-any.whl", hash = "sha256:30c8302f204b663c655d97e1c283ee9f9f84a6257d7901b931477d6cf34445a8"}, -] - -[package.dependencies] -aiofiles = ">=22.1.0,<25.0.0" -alibabacloud-credentials-api = ">=1.0.0,<2.0.0" -alibabacloud-tea = ">=0.4.0" -APScheduler = ">=3.10.0,<4.0.0" - -[[package]] -name = "alibabacloud-credentials-api" -version = "1.0.0" -description = "Alibaba Cloud Gateway SPI SDK Library for Python" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "alibabacloud-credentials-api-1.0.0.tar.gz", hash = "sha256:8c340038d904f0218d7214a8f4088c31912bfcf279af2cbc7d9be4897a97dd2f"}, -] - -[[package]] -name = "alibabacloud-cs20151215" -version = "6.1.0" -description = "Alibaba Cloud CS (20151215) SDK Library for Python" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "alibabacloud_cs20151215-6.1.0-py3-none-any.whl", hash = "sha256:75e90b1bb9acca2236244bb0e44234ca4805d456ea4303ba4225ac15152a458e"}, - {file = "alibabacloud_cs20151215-6.1.0.tar.gz", hash = "sha256:5b3d99306701bf499ddd57cd9f2905b7721cb1bb4bb38ffe4d051f7b4e80e355"}, -] - -[package.dependencies] -alibabacloud-endpoint-util = ">=0.0.4,<1.0.0" -alibabacloud-openapi-util = ">=0.2.2,<1.0.0" -alibabacloud-tea-openapi = ">=0.3.16,<1.0.0" -alibabacloud-tea-util = ">=0.3.13,<1.0.0" - -[[package]] -name = "alibabacloud-darabonba-array" -version = "0.1.0" -description = "Alibaba Cloud Darabonba Array SDK Library for Python" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "alibabacloud_darabonba_array-0.1.0.tar.gz", hash = "sha256:7f9a7c632518ff4f0cebb0d4e825a48c12e7cf0b9016ea25054dd73732e155aa"}, -] - -[[package]] -name = "alibabacloud-darabonba-encode-util" -version = "0.0.2" -description = "Darabonba Util Library for Alibaba Cloud Python SDK" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "alibabacloud_darabonba_encode_util-0.0.2.tar.gz", hash = "sha256:f1c484f276d60450fa49b4b2987194e741fcb2f7faae7f287c0ae65abc85fd4d"}, -] - -[[package]] -name = "alibabacloud-darabonba-map" -version = "0.0.1" -description = "Alibaba Cloud Darabonba Map SDK Library for Python" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "alibabacloud_darabonba_map-0.0.1.tar.gz", hash = "sha256:adb17384658a1a8f72418f1838d4b6a5fd2566bfd392a3ef06d9dbb0a595a23f"}, -] - -[[package]] -name = "alibabacloud-darabonba-signature-util" -version = "0.0.4" -description = "Darabonba Util Library for Alibaba Cloud Python SDK" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "alibabacloud_darabonba_signature_util-0.0.4.tar.gz", hash = "sha256:71d79b2ae65957bcfbf699ced894fda782b32f9635f1616635533e5a90d5feb0"}, -] - -[package.dependencies] -cryptography = ">=3.0.0" - -[[package]] -name = "alibabacloud-darabonba-string" -version = "0.0.4" -description = "Alibaba Cloud Darabonba String Library for Python" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "alibabacloud-darabonba-string-0.0.4.tar.gz", hash = "sha256:ec6614c0448dadcbc5e466485838a1f8cfdd911135bea739e20b14511270c6f7"}, -] - -[[package]] -name = "alibabacloud-darabonba-time" -version = "0.0.1" -description = "Alibaba Cloud Darabonba Time SDK Library for Python" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "alibabacloud_darabonba_time-0.0.1.tar.gz", hash = "sha256:0ad9c7b0696570d1a3f40106cc7777f755fd92baa0d1dcab5b7df78dde5b922d"}, -] - -[[package]] -name = "alibabacloud-ecs20140526" -version = "7.2.5" -description = "Alibaba Cloud Elastic Compute Service (20140526) SDK Library for Python" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "alibabacloud_ecs20140526-7.2.5-py3-none-any.whl", hash = "sha256:10bda5e185f6ba899e7d51477373595c629d66db7530a8a37433fb4e9034a96f"}, - {file = "alibabacloud_ecs20140526-7.2.5.tar.gz", hash = "sha256:2abbe630ce42d69061821f38950b938c5982cc31902ccd7132d05be328765a55"}, -] - -[package.dependencies] -alibabacloud-endpoint-util = ">=0.0.4,<1.0.0" -alibabacloud-openapi-util = ">=0.2.2,<1.0.0" -alibabacloud-tea-openapi = ">=0.3.16,<1.0.0" -alibabacloud-tea-util = ">=0.3.13,<1.0.0" - -[[package]] -name = "alibabacloud-endpoint-util" -version = "0.0.4" -description = "The endpoint-util module of alibabaCloud Python SDK." -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "alibabacloud_endpoint_util-0.0.4.tar.gz", hash = "sha256:a593eb8ddd8168d5dc2216cd33111b144f9189fcd6e9ca20e48f358a739bbf90"}, -] - -[[package]] -name = "alibabacloud-gateway-oss" -version = "0.0.17" -description = "Alibaba Cloud OSS SDK Library for Python" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "alibabacloud_gateway_oss-0.0.17.tar.gz", hash = "sha256:8c4b66c8c7dd285fc210ee232ab3f062b5573258752804d19382000746531e29"}, -] - -[package.dependencies] -alibabacloud_credentials = ">=0.3.5" -alibabacloud_darabonba_array = ">=0.1.0,<1.0.0" -alibabacloud_darabonba_encode_util = ">=0.0.2,<1.0.0" -alibabacloud_darabonba_map = ">=0.0.1,<1.0.0" -alibabacloud_darabonba_signature_util = ">=0.0.4,<1.0.0" -alibabacloud_darabonba_string = ">=0.0.4,<1.0.0" -alibabacloud_darabonba_time = ">=0.0.1,<1.0.0" -alibabacloud_gateway_oss_util = ">=0.0.3,<1.0.0" -alibabacloud_gateway_spi = ">=0.0.1,<1.0.0" -alibabacloud_openapi_util = ">=0.2.1,<1.0.0" -alibabacloud_oss_util = ">=0.0.5,<1.0.0" -alibabacloud_tea_util = ">=0.3.11,<1.0.0" -alibabacloud_tea_xml = ">=0.0.2,<1.0.0" - -[[package]] -name = "alibabacloud-gateway-oss-util" -version = "0.0.3" -description = "Alibaba Cloud OSS Util Library for Python" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "alibabacloud_gateway_oss_util-0.0.3.tar.gz", hash = "sha256:5eb7fa450dc7350d5c71577974b9d7f489479e5c5ec7efc1c5376385e8c1c0a5"}, -] - -[[package]] -name = "alibabacloud-gateway-sls" -version = "0.4.0" -description = "Alibaba Cloud SLS Gateway Library for Python" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "alibabacloud_gateway_sls-0.4.0-py3-none-any.whl", hash = "sha256:a0299a83a5528025983b42b7533a28028461bced5e180a66f97999e0134760a6"}, - {file = "alibabacloud_gateway_sls-0.4.0.tar.gz", hash = "sha256:9d2aceb377c9b3ed0558149fda16fe39fa114cc0a22e22a88dc76efdda34633b"}, -] - -[package.dependencies] -alibabacloud-credentials = ">=1.0.2,<2.0.0" -alibabacloud-darabonba-array = ">=0.1.0,<1.0.0" -alibabacloud-darabonba-encode-util = ">=0.0.2,<1.0.0" -alibabacloud-darabonba-map = ">=0.0.1,<1.0.0" -alibabacloud-darabonba-signature-util = ">=0.0.4,<1.0.0" -alibabacloud-darabonba-string = ">=0.0.4,<1.0.0" -alibabacloud-gateway-sls-util = ">=0.4.0,<1.0.0" -alibabacloud-gateway-spi = ">=0.0.2,<1.0.0" -alibabacloud-openapi-util = ">=0.2.2,<1.0.0" -alibabacloud-tea-util = ">=0.3.13,<1.0.0" - -[[package]] -name = "alibabacloud-gateway-sls-util" -version = "0.4.0" -description = "Alibaba Cloud SLS Util Library for Python" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "alibabacloud_gateway_sls_util-0.4.0-py3-none-any.whl", hash = "sha256:c91ab7fe55af526a01d25b0d431088c4d241b160db055da3d8cb7330bd74595a"}, - {file = "alibabacloud_gateway_sls_util-0.4.0.tar.gz", hash = "sha256:f8b683a36a2ae3fe9a8225d3d97773ea769bdf9cdf4f4d033eab2eb6062ddd1f"}, -] - -[package.dependencies] -aliyun-log-fastpb = ">=0.2.0" -lz4 = ">=4.3.2" -zstd = ">=1.5.5.1" - -[[package]] -name = "alibabacloud-gateway-spi" -version = "0.0.3" -description = "Alibaba Cloud Gateway SPI SDK Library for Python" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "alibabacloud_gateway_spi-0.0.3.tar.gz", hash = "sha256:10d1c53a3fc5f87915fbd6b4985b98338a776e9b44a0263f56643c5048223b8b"}, -] - -[package.dependencies] -alibabacloud_credentials = ">=0.3.4" - -[[package]] -name = "alibabacloud-openapi-util" -version = "0.2.2" -description = "Aliyun Tea OpenApi Library for Python" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "alibabacloud_openapi_util-0.2.2.tar.gz", hash = "sha256:ebbc3906f554cb4bf8f513e43e8a33e8b6a3d4a0ef13617a0e14c3dda8ef52a8"}, -] - -[package.dependencies] -alibabacloud_tea_util = ">=0.0.2" -cryptography = ">=3.0.0" - -[[package]] -name = "alibabacloud-oss-util" -version = "0.0.6" -description = "The oss util module of alibabaCloud Python SDK." -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "alibabacloud_oss_util-0.0.6.tar.gz", hash = "sha256:d3ecec36632434bd509a113e8cf327dc23e830ac8d9dd6949926f4e334c8b5d6"}, -] - -[package.dependencies] -alibabacloud-tea = "*" - -[[package]] -name = "alibabacloud-oss20190517" -version = "1.0.6" -description = "Alibaba Cloud Object Storage Service (20190517) SDK Library for Python" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "alibabacloud_oss20190517-1.0.6-py3-none-any.whl", hash = "sha256:365fda353de6658a1a289f4d70dcd0394e2a8e2921b6b5834ba6d9772121d2f6"}, - {file = "alibabacloud_oss20190517-1.0.6.tar.gz", hash = "sha256:7cd0fb16af613ceb38d2e0e529aa1f58038c7cf59eb67c8c8775ae44ea717852"}, -] - -[package.dependencies] -alibabacloud-gateway-oss = ">=0.0.9,<1.0.0" -alibabacloud-gateway-spi = ">=0.0.1,<1.0.0" -alibabacloud-openapi-util = ">=0.2.1,<1.0.0" -alibabacloud-tea-openapi = ">=0.3.6,<1.0.0" -alibabacloud-tea-util = ">=0.3.11,<1.0.0" - -[[package]] -name = "alibabacloud-ram20150501" -version = "1.2.0" -description = "Alibaba Cloud Resource Access Management (20150501) SDK Library for Python" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "alibabacloud_ram20150501-1.2.0-py3-none-any.whl", hash = "sha256:03a0f2a0259848787c1f74e802b486184a88e04183486bd9398766971e5eb00a"}, - {file = "alibabacloud_ram20150501-1.2.0.tar.gz", hash = "sha256:6253513c8880769f4fd5b36fedddb362a9ca628ad9ae9c05c0eeacf5fbc95b42"}, -] - -[package.dependencies] -alibabacloud-endpoint-util = ">=0.0.4,<1.0.0" -alibabacloud-openapi-util = ">=0.2.2,<1.0.0" -alibabacloud-tea-openapi = ">=0.3.15,<1.0.0" -alibabacloud-tea-util = ">=0.3.13,<1.0.0" - -[[package]] -name = "alibabacloud-rds20140815" -version = "12.0.0" -description = "Alibaba Cloud rds (20140815) SDK Library for Python" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "alibabacloud_rds20140815-12.0.0-py3-none-any.whl", hash = "sha256:0bd7e2018a428d86b1b0681087336e74665b48fc3eb0a13c4f4377ed5eab2b08"}, - {file = "alibabacloud_rds20140815-12.0.0.tar.gz", hash = "sha256:e7421d94f18a914c0a06b0e7fad0daff557713f1c97d415d463a78c1270e9b98"}, -] - -[package.dependencies] -alibabacloud-endpoint-util = ">=0.0.4,<1.0.0" -alibabacloud-openapi-util = ">=0.2.2,<1.0.0" -alibabacloud-tea-openapi = ">=0.3.15,<1.0.0" -alibabacloud-tea-util = ">=0.3.13,<1.0.0" - -[[package]] -name = "alibabacloud-sas20181203" -version = "6.1.0" -description = "Alibaba Cloud Threat Detection (20181203) SDK Library for Python" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "alibabacloud_sas20181203-6.1.0-py3-none-any.whl", hash = "sha256:1ad735332c50c7961be036b17420d56b5ec3b5557e3aea1daa19491e8b75da20"}, - {file = "alibabacloud_sas20181203-6.1.0.tar.gz", hash = "sha256:e49ffd53e630274a8bf5a8299ca753023ad118510c80f6d9c6fb018b7479bf37"}, -] - -[package.dependencies] -alibabacloud-endpoint-util = ">=0.0.4,<1.0.0" -alibabacloud-openapi-util = ">=0.2.2,<1.0.0" -alibabacloud-tea-openapi = ">=0.3.16,<1.0.0" -alibabacloud-tea-util = ">=0.3.13,<1.0.0" - -[[package]] -name = "alibabacloud-sls20201230" -version = "5.9.0" -description = "Alibaba Cloud Log Service (20201230) SDK Library for Python" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "alibabacloud_sls20201230-5.9.0-py3-none-any.whl", hash = "sha256:c4ae14096817a9686af5a0ae2389f1f6a8781e60b9edb8643445250cf15c26f1"}, - {file = "alibabacloud_sls20201230-5.9.0.tar.gz", hash = "sha256:bea830b64fbc7ed1719ba386ceeefb120f08d705f03eb0e02409dc6f12a291da"}, -] - -[package.dependencies] -alibabacloud-gateway-sls = ">=0.3.0,<1.0.0" -alibabacloud-openapi-util = ">=0.2.2,<1.0.0" -alibabacloud-tea-openapi = ">=0.3.16,<1.0.0" -alibabacloud-tea-util = ">=0.3.13,<1.0.0" - -[[package]] -name = "alibabacloud-sts20150401" -version = "1.1.6" -description = "Alibaba Cloud Sts (20150401) SDK Library for Python" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "alibabacloud_sts20150401-1.1.6-py3-none-any.whl", hash = "sha256:627f5ca1f86e19b0bf8ce0e99071a36fb65579fad9256fbee38fdc8d500598e9"}, - {file = "alibabacloud_sts20150401-1.1.6.tar.gz", hash = "sha256:c2529b41e0e4531e21cb393e4df346e19fd6d54cc6337d1138dbcd2191438d4c"}, -] - -[package.dependencies] -alibabacloud-endpoint-util = ">=0.0.4,<1.0.0" -alibabacloud-openapi-util = ">=0.2.2,<1.0.0" -alibabacloud-tea-openapi = ">=0.3.15,<1.0.0" -alibabacloud-tea-util = ">=0.3.13,<1.0.0" - -[[package]] -name = "alibabacloud-tea" -version = "0.4.3" -description = "The tea module of alibabaCloud Python SDK." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "alibabacloud-tea-0.4.3.tar.gz", hash = "sha256:ec8053d0aa8d43ebe1deb632d5c5404339b39ec9a18a0707d57765838418504a"}, -] - -[package.dependencies] -aiohttp = ">=3.7.0,<4.0.0" -requests = ">=2.21.0,<3.0.0" - -[[package]] -name = "alibabacloud-tea-openapi" -version = "0.4.4" -description = "Alibaba Cloud openapi SDK Library for Python" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "alibabacloud_tea_openapi-0.4.4-py3-none-any.whl", hash = "sha256:cea6bc1fe35b0319a8752cb99eb0ecb0dab7ca1a71b99c12970ba0867410995f"}, - {file = "alibabacloud_tea_openapi-0.4.4.tar.gz", hash = "sha256:1b0917bc03cd49417da64945e92731716d53e2eb8707b235f54e45b7473221ce"}, -] - -[package.dependencies] -alibabacloud-credentials = ">=1.0.2,<2.0.0" -alibabacloud-gateway-spi = ">=0.0.2,<1.0.0" -alibabacloud-tea-util = ">=0.3.13,<1.0.0" -cryptography = {version = ">=3.0.0,<47.0.0", markers = "python_version >= \"3.8\""} -darabonba-core = ">=1.0.3,<2.0.0" - -[[package]] -name = "alibabacloud-tea-util" -version = "0.3.14" -description = "The tea-util module of alibabaCloud Python SDK." -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "alibabacloud_tea_util-0.3.14-py3-none-any.whl", hash = "sha256:10d3e5c340d8f7ec69dd27345eb2fc5a1dab07875742525edf07bbe86db93bfe"}, - {file = "alibabacloud_tea_util-0.3.14.tar.gz", hash = "sha256:708e7c9f64641a3c9e0e566365d2f23675f8d7c2a3e2971d9402ceede0408cdb"}, -] - -[package.dependencies] -alibabacloud-tea = ">=0.3.3" - -[[package]] -name = "alibabacloud-tea-xml" -version = "0.0.3" -description = "The tea-xml module of alibabaCloud Python SDK." -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "alibabacloud_tea_xml-0.0.3.tar.gz", hash = "sha256:979cb51fadf43de77f41c69fc69c12529728919f849723eb0cd24eb7b048a90c"}, -] - -[package.dependencies] -alibabacloud-tea = ">=0.4.0" - -[[package]] -name = "alibabacloud-vpc20160428" -version = "6.13.0" -description = "Alibaba Cloud Virtual Private Cloud (20160428) SDK Library for Python" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "alibabacloud_vpc20160428-6.13.0-py3-none-any.whl", hash = "sha256:933cf1e74322a20a2df27ca6323760d857744a4246eeadc9fb3eae01322fb1c6"}, - {file = "alibabacloud_vpc20160428-6.13.0.tar.gz", hash = "sha256:daf00679a83d422799f9fcf263739fe1f360641675843cbfbe623833fc8b1681"}, -] - -[package.dependencies] -alibabacloud-endpoint-util = ">=0.0.4,<1.0.0" -alibabacloud-openapi-util = ">=0.2.2,<1.0.0" -alibabacloud-tea-openapi = ">=0.3.16,<1.0.0" -alibabacloud-tea-util = ">=0.3.13,<1.0.0" - -[[package]] -name = "alive-progress" -version = "3.3.0" -description = "A new kind of Progress Bar, with real-time throughput, ETA, and very cool animations!" -optional = false -python-versions = "<4,>=3.9" -groups = ["main"] -files = [ - {file = "alive-progress-3.3.0.tar.gz", hash = "sha256:457dd2428b48dacd49854022a46448d236a48f1b7277874071c39395307e830c"}, - {file = "alive_progress-3.3.0-py3-none-any.whl", hash = "sha256:63dd33bb94cde15ad9e5b666dbba8fedf71b72a4935d6fb9a92931e69402c9ff"}, -] - -[package.dependencies] -about-time = "4.2.1" -graphemeu = "0.7.2" - -[[package]] -name = "aliyun-log-fastpb" -version = "0.2.0" -description = "Fast protobuf serialization for Aliyun Log using PyO3 and quick-protobuf" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "aliyun_log_fastpb-0.2.0-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:51633d92d2b349aed4843c0b503454fb4f7d73eeaaa54f82aa5a36c10c064ef5"}, - {file = "aliyun_log_fastpb-0.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d2984aafc61ccbbf1db2589ce90b6d5a26e72dba137fb1fdf7f61ce3faa967c0"}, - {file = "aliyun_log_fastpb-0.2.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:181fc61ac9934f58b0880fa5617a4a4dc709dba09f8be95b5a71e828f2e48053"}, - {file = "aliyun_log_fastpb-0.2.0-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12b8bfddf0bc5450f16f1954c6387a73da124fae10d1205a17a0117e66bb56db"}, - {file = "aliyun_log_fastpb-0.2.0-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8fbc83cbaa51d332e5e68871c1200014f1f3de54a8cba4fb55a634ee145cd4e4"}, - {file = "aliyun_log_fastpb-0.2.0-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42a86a6e11dd227d595fa23f69d30588446af19d045d1003bd1b66b5c9a55485"}, - {file = "aliyun_log_fastpb-0.2.0-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd92c0b84ba300c1d1c227204c5f2fff243cea80bc3f9399293385e87c82ee3e"}, - {file = "aliyun_log_fastpb-0.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7c07a6d81a3eab6666949240da305236ed2350c305154d7e39fcc121fc52291"}, - {file = "aliyun_log_fastpb-0.2.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2cff4fbdd0edff94adcee1dcabf16daacb5d336a12fc897887aa6e4f0ad25152"}, - {file = "aliyun_log_fastpb-0.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5a451809e2a062accbb8dae8750e507e58806e4a8da48d69215cdeef428e9d63"}, - {file = "aliyun_log_fastpb-0.2.0-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:61f09df30232f1f5628d13310cf0e175171399ea1c75a8470e9f9d97b045bfb5"}, - {file = "aliyun_log_fastpb-0.2.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:a5fbf0d41d8c0c964a3dc8dd0ee2e732f876b803e0ed3432550ef3b84dde84f1"}, - {file = "aliyun_log_fastpb-0.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ae2f84ed0777e00045791044a56413f370afbd5b061505f5ded540c04b19c58e"}, - {file = "aliyun_log_fastpb-0.2.0-cp37-abi3-win32.whl", hash = "sha256:967f9656c805602fd9be07d8c2756ad89204c852c99689c3c71aa035416ef42a"}, - {file = "aliyun_log_fastpb-0.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:bbdcf7b85f0f3437c2a8e8a1db0ef5584d21468b7c7a358269a4c651c84f4a54"}, - {file = "aliyun_log_fastpb-0.2.0.tar.gz", hash = "sha256:91c714e76fb941c9a0db6b1aa1f4c56cb1626254ff5444c1179860f5e5b63d93"}, -] - -[[package]] -name = "annotated-types" -version = "0.7.0" -description = "Reusable constraint types to use with typing.Annotated" -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, - {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, -] - -[[package]] -name = "antlr4-python3-runtime" -version = "4.13.2" -description = "ANTLR 4.13.2 runtime for Python 3" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "antlr4_python3_runtime-4.13.2-py3-none-any.whl", hash = "sha256:fe3835eb8d33daece0e799090eda89719dbccee7aa39ef94eed3818cafa5a7e8"}, - {file = "antlr4_python3_runtime-4.13.2.tar.gz", hash = "sha256:909b647e1d2fc2b70180ac586df3933e38919c85f98ccc656a96cd3f25ef3916"}, -] - -[[package]] -name = "anyio" -version = "4.9.0" -description = "High level compatibility layer for multiple asynchronous event loop implementations" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"}, - {file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"}, -] - -[package.dependencies] -exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} -idna = ">=2.8" -sniffio = ">=1.1" -typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} - -[package.extras] -doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] -test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""] -trio = ["trio (>=0.26.1)"] - -[[package]] -name = "apscheduler" -version = "3.11.1" -description = "In-process task scheduler with Cron-like capabilities" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "apscheduler-3.11.1-py3-none-any.whl", hash = "sha256:6162cb5683cb09923654fa9bdd3130c4be4bfda6ad8990971c9597ecd52965d2"}, - {file = "apscheduler-3.11.1.tar.gz", hash = "sha256:0db77af6400c84d1747fe98a04b8b58f0080c77d11d338c4f507a9752880f221"}, -] - -[package.dependencies] -tzlocal = ">=3.0" - -[package.extras] -doc = ["packaging", "sphinx", "sphinx-rtd-theme (>=1.3.0)"] -etcd = ["etcd3", "protobuf (<=3.21.0)"] -gevent = ["gevent"] -mongodb = ["pymongo (>=3.0)"] -redis = ["redis (>=3.0)"] -rethinkdb = ["rethinkdb (>=2.4.0)"] -sqlalchemy = ["sqlalchemy (>=1.4)"] -test = ["APScheduler[etcd,mongodb,redis,rethinkdb,sqlalchemy,tornado,zookeeper]", "PySide6 ; platform_python_implementation == \"CPython\" and python_version < \"3.14\"", "anyio (>=4.5.2)", "gevent ; python_version < \"3.14\"", "pytest", "pytz", "twisted ; python_version < \"3.14\""] -tornado = ["tornado (>=4.3)"] -twisted = ["twisted"] -zookeeper = ["kazoo"] - -[[package]] -name = "astroid" -version = "3.3.11" -description = "An abstract syntax tree for Python with inference support." -optional = false -python-versions = ">=3.9.0" -groups = ["dev"] -files = [ - {file = "astroid-3.3.11-py3-none-any.whl", hash = "sha256:54c760ae8322ece1abd213057c4b5bba7c49818853fc901ef09719a60dbf9dec"}, - {file = "astroid-3.3.11.tar.gz", hash = "sha256:1e5a5011af2920c7c67a53f65d536d65bfa7116feeaf2354d8b94f29573bb0ce"}, -] - -[package.dependencies] -typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} - -[[package]] -name = "async-timeout" -version = "5.0.1" -description = "Timeout context manager for asyncio programs" -optional = false -python-versions = ">=3.8" -groups = ["main"] -markers = "python_version == \"3.10\"" -files = [ - {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, - {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, -] - -[[package]] -name = "attrs" -version = "25.3.0" -description = "Classes Without Boilerplate" -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, - {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, -] - -[package.extras] -benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] -tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] - -[[package]] -name = "authlib" -version = "1.6.9" -description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "authlib-1.6.9-py2.py3-none-any.whl", hash = "sha256:f08b4c14e08f0861dc18a32357b33fbcfd2ea86cfe3fe149484b4d764c4a0ac3"}, - {file = "authlib-1.6.9.tar.gz", hash = "sha256:d8f2421e7e5980cc1ddb4e32d3f5fa659cfaf60d8eaf3281ebed192e4ab74f04"}, -] - -[package.dependencies] -cryptography = "*" - -[[package]] -name = "aws-sam-translator" -version = "1.99.0" -description = "AWS SAM Translator is a library that transform SAM templates into AWS CloudFormation templates" -optional = false -python-versions = "!=4.0,<=4.0,>=3.8" -groups = ["dev"] -files = [ - {file = "aws_sam_translator-1.99.0-py3-none-any.whl", hash = "sha256:b1997e09da876342655eb568e66098280ffd137213009f0136b57f4e7694c98c"}, - {file = "aws_sam_translator-1.99.0.tar.gz", hash = "sha256:be326054a7ee2f535fcd914db85e5d50bdf4054313c14888af69b6de3187cdf8"}, -] - -[package.dependencies] -boto3 = ">=1.34.0,<2.0.0" -jsonschema = ">=3.2,<5" -pydantic = ">=1.8,<1.10.15 || >1.10.15,<1.10.17 || >1.10.17,<3" -typing_extensions = ">=4.4" - -[package.extras] -dev = ["black (==24.3.0)", "boto3 (>=1.34.0,<2.0.0)", "boto3-stubs[appconfig,serverlessrepo] (>=1.34.0,<2.0.0)", "cloudformation-cli (>=0.2.39,<0.3.0)", "coverage (>=5.3,<8)", "dateparser (>=1.1,<2.0)", "mypy (>=1.3.0,<1.4.0)", "parameterized (>=0.7,<1.0)", "pytest (>=6.2,<8)", "pytest-cov (>=2.10,<5)", "pytest-env (>=0.6,<1)", "pytest-rerunfailures (>=9.1,<12)", "pytest-xdist (>=2.5,<4)", "pyyaml (>=6.0,<7.0)", "requests (>=2.28,<3.0)", "ruamel.yaml (==0.17.21)", "ruff (>=0.4.5,<0.5.0)", "tenacity (>=9.0,<10.0)", "types-PyYAML (>=6.0,<7.0)", "types-jsonschema (>=3.2,<4.0)"] - -[[package]] -name = "aws-xray-sdk" -version = "2.14.0" -description = "The AWS X-Ray SDK for Python (the SDK) enables Python developers to record and emit information from within their applications to the AWS X-Ray service." -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "aws_xray_sdk-2.14.0-py2.py3-none-any.whl", hash = "sha256:cfbe6feea3d26613a2a869d14c9246a844285c97087ad8f296f901633554ad94"}, - {file = "aws_xray_sdk-2.14.0.tar.gz", hash = "sha256:aab843c331af9ab9ba5cefb3a303832a19db186140894a523edafc024cc0493c"}, -] - -[package.dependencies] -botocore = ">=1.11.3" -wrapt = "*" - -[[package]] -name = "awsipranges" -version = "0.3.3" -description = "Work with the AWS IP address ranges in native Python." -optional = false -python-versions = ">=3.7,<4.0" -groups = ["main"] -files = [ - {file = "awsipranges-0.3.3-py3-none-any.whl", hash = "sha256:f3d7a54aeaf7fe310beb5d377a4034a63a51b72677ae6af3e0967bc4de7eedaf"}, - {file = "awsipranges-0.3.3.tar.gz", hash = "sha256:4f0b3f22a9dc1163c85b513bed812b6c92bdacd674e6a7b68252a3c25b99e2c0"}, -] - -[[package]] -name = "azure-common" -version = "1.1.28" -description = "Microsoft Azure Client Library for Python (Common)" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "azure-common-1.1.28.zip", hash = "sha256:4ac0cd3214e36b6a1b6a442686722a5d8cc449603aa833f3f0f40bda836704a3"}, - {file = "azure_common-1.1.28-py2.py3-none-any.whl", hash = "sha256:5c12d3dcf4ec20599ca6b0d3e09e86e146353d443e7fcc050c9a19c1f9df20ad"}, -] - -[[package]] -name = "azure-core" -version = "1.38.0" -description = "Microsoft Azure Core Library for Python" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "azure_core-1.38.0-py3-none-any.whl", hash = "sha256:ab0c9b2cd71fecb1842d52c965c95285d3cfb38902f6766e4a471f1cd8905335"}, - {file = "azure_core-1.38.0.tar.gz", hash = "sha256:8194d2682245a3e4e3151a667c686464c3786fed7918b394d035bdcd61bb5993"}, -] - -[package.dependencies] -requests = ">=2.21.0" -typing-extensions = ">=4.6.0" - -[package.extras] -aio = ["aiohttp (>=3.0)"] -tracing = ["opentelemetry-api (>=1.26,<2.0)"] - -[[package]] -name = "azure-identity" -version = "1.21.0" -description = "Microsoft Azure Identity Library for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "azure_identity-1.21.0-py3-none-any.whl", hash = "sha256:258ea6325537352440f71b35c3dffe9d240eae4a5126c1b7ce5efd5766bd9fd9"}, - {file = "azure_identity-1.21.0.tar.gz", hash = "sha256:ea22ce6e6b0f429bc1b8d9212d5b9f9877bd4c82f1724bfa910760612c07a9a6"}, -] - -[package.dependencies] -azure-core = ">=1.31.0" -cryptography = ">=2.5" -msal = ">=1.30.0" -msal-extensions = ">=1.2.0" -typing-extensions = ">=4.0.0" - -[[package]] -name = "azure-keyvault-keys" -version = "4.10.0" -description = "Microsoft Azure Key Vault Keys Client Library for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "azure_keyvault_keys-4.10.0-py3-none-any.whl", hash = "sha256:210227e0061f641a79755f0e0bcbcf27bbfb4df630a933c43a99a29962283d0d"}, - {file = "azure_keyvault_keys-4.10.0.tar.gz", hash = "sha256:511206ae90aec1726a4d6ff5a92d754bd0c0f1e8751891368d30fb70b62955f1"}, -] - -[package.dependencies] -azure-core = ">=1.31.0" -cryptography = ">=2.1.4" -isodate = ">=0.6.1" -typing-extensions = ">=4.0.1" - -[[package]] -name = "azure-mgmt-apimanagement" -version = "5.0.0" -description = "Microsoft Azure API Management Client Library for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "azure_mgmt_apimanagement-5.0.0-py3-none-any.whl", hash = "sha256:b88c42a392333b60722fb86f15d092dfc19a8d67510dccd15c217381dff4e6ec"}, - {file = "azure_mgmt_apimanagement-5.0.0.tar.gz", hash = "sha256:0ab7fe17e70fe3154cd840ff47d19d7a4610217003eaa7c21acf3511a6e57999"}, -] - -[package.dependencies] -azure-common = ">=1.1" -azure-mgmt-core = ">=1.3.2" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[[package]] -name = "azure-mgmt-applicationinsights" -version = "4.1.0" -description = "Microsoft Azure Application Insights Management Client Library for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "azure_mgmt_applicationinsights-4.1.0-py3-none-any.whl", hash = "sha256:9e71f29b01e505a773501451d12fd6a10482cf4b13e9ac2bff72f5380496d979"}, - {file = "azure_mgmt_applicationinsights-4.1.0.tar.gz", hash = "sha256:15531390f12ce3d767cd3f1949af36aa39077c145c952fec4d80303c86ec7b6c"}, -] - -[package.dependencies] -azure-common = ">=1.1" -azure-mgmt-core = ">=1.3.2" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[[package]] -name = "azure-mgmt-authorization" -version = "4.0.0" -description = "Microsoft Azure Authorization Management Client Library for Python" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "azure-mgmt-authorization-4.0.0.zip", hash = "sha256:69b85abc09ae64fc72975bd43431170d8c7eb5d166754b98aac5f3845de57dc4"}, - {file = "azure_mgmt_authorization-4.0.0-py3-none-any.whl", hash = "sha256:d8feeb3842e6ddf1a370963ca4f61fb6edc124e8997b807dd025bc9b2379cd1a"}, -] - -[package.dependencies] -azure-common = ">=1.1,<2.0" -azure-mgmt-core = ">=1.3.2,<2.0.0" -isodate = ">=0.6.1,<1.0.0" - -[[package]] -name = "azure-mgmt-compute" -version = "34.0.0" -description = "Microsoft Azure Compute Management Client Library for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "azure_mgmt_compute-34.0.0-py3-none-any.whl", hash = "sha256:f8f7b1c5c187a26fae4d1f099adf93561244242f28899484d9a42747bf0d5af4"}, - {file = "azure_mgmt_compute-34.0.0.tar.gz", hash = "sha256:58cd01d025efa02870b84dbfb69834a3b23501a135658c03854d2434e8dfee1e"}, -] - -[package.dependencies] -azure-common = ">=1.1" -azure-mgmt-core = ">=1.3.2" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[[package]] -name = "azure-mgmt-containerregistry" -version = "12.0.0" -description = "Microsoft Azure Container Registry Client Library for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "azure_mgmt_containerregistry-12.0.0-py3-none-any.whl", hash = "sha256:464abd4d3d9ecc0456ed8f63a6b9b93afc2e3e194f2d34f26a758afb67ad3b5c"}, - {file = "azure_mgmt_containerregistry-12.0.0.tar.gz", hash = "sha256:f19f8faa7881deaf2b5015c0eb050a92e2380cd9d18dee33cdb5f27d44a06c03"}, -] - -[package.dependencies] -azure-common = ">=1.1" -azure-mgmt-core = ">=1.3.2" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[[package]] -name = "azure-mgmt-containerservice" -version = "34.1.0" -description = "Microsoft Azure Container Service Management Client Library for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "azure_mgmt_containerservice-34.1.0-py3-none-any.whl", hash = "sha256:1faa1714e0100c6ee4cfb8d2eadb1c270b548a84b0070c74e9fe646056a5cb12"}, - {file = "azure_mgmt_containerservice-34.1.0.tar.gz", hash = "sha256:637a6cf8f06636c016ad151d76f9c7ba75bd05d4334b3dd7837eb8b517f30dbe"}, -] - -[package.dependencies] -azure-common = ">=1.1" -azure-mgmt-core = ">=1.3.2" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[[package]] -name = "azure-mgmt-core" -version = "1.6.0" -description = "Microsoft Azure Management Core Library for Python" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "azure_mgmt_core-1.6.0-py3-none-any.whl", hash = "sha256:0460d11e85c408b71c727ee1981f74432bc641bb25dfcf1bb4e90a49e776dbc4"}, - {file = "azure_mgmt_core-1.6.0.tar.gz", hash = "sha256:b26232af857b021e61d813d9f4ae530465255cb10b3dde945ad3743f7a58e79c"}, -] - -[package.dependencies] -azure-core = ">=1.32.0" - -[[package]] -name = "azure-mgmt-cosmosdb" -version = "9.7.0" -description = "Microsoft Azure Cosmos DB Management Client Library for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "azure_mgmt_cosmosdb-9.7.0-py3-none-any.whl", hash = "sha256:be735a554d16995c8cefe413e62119985f8fabae1cb45a6f6ad2c3958bed14da"}, - {file = "azure_mgmt_cosmosdb-9.7.0.tar.gz", hash = "sha256:b5072d319f11953d8f12e22459aded1912d5f27e442e1d8b49596a85005410a1"}, -] - -[package.dependencies] -azure-common = ">=1.1" -azure-mgmt-core = ">=1.3.2" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[[package]] -name = "azure-mgmt-databricks" -version = "2.0.0" -description = "Microsoft Azure Data Bricks Management Client Library for Python" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "azure-mgmt-databricks-2.0.0.zip", hash = "sha256:70d11362dc2d17f5fb1db0cfe65c1af55b8f136f1a0db9a5b51e7acf760cf5b9"}, - {file = "azure_mgmt_databricks-2.0.0-py3-none-any.whl", hash = "sha256:0c29434a7339e74231bd171a6c08dcdf8153abaebd332658d7f66b8ea143fa17"}, -] - -[package.dependencies] -azure-common = ">=1.1,<2.0" -azure-mgmt-core = ">=1.3.2,<2.0.0" -isodate = ">=0.6.1,<1.0.0" - -[[package]] -name = "azure-mgmt-keyvault" -version = "10.3.1" -description = "Microsoft Azure Key Vault Management Client Library for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "azure-mgmt-keyvault-10.3.1.tar.gz", hash = "sha256:34b92956aefbdd571cae5a03f7078e037d8087b2c00cfa6748835dc73abb5a30"}, - {file = "azure_mgmt_keyvault-10.3.1-py3-none-any.whl", hash = "sha256:a18a27a06551482d31f92bc43ac8b0846af02cd69511f80090865b4c5caa3c21"}, -] - -[package.dependencies] -azure-common = ">=1.1" -azure-mgmt-core = ">=1.3.2" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[[package]] -name = "azure-mgmt-loganalytics" -version = "12.0.0" -description = "Microsoft Azure Log Analytics Management Client Library for Python" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "azure-mgmt-loganalytics-12.0.0.zip", hash = "sha256:da128a7e0291be7fa2063848df92a9180cf5c16d42adc09d2bc2efd711536bfb"}, - {file = "azure_mgmt_loganalytics-12.0.0-py2.py3-none-any.whl", hash = "sha256:75ac1d47dd81179905c40765be8834643d8994acff31056ddc1863017f3faa02"}, -] - -[package.dependencies] -azure-common = ">=1.1,<2.0" -azure-mgmt-core = ">=1.2.0,<2.0.0" -msrest = ">=0.6.21" - -[[package]] -name = "azure-mgmt-monitor" -version = "6.0.2" -description = "Microsoft Azure Monitor Client Library for Python" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "azure-mgmt-monitor-6.0.2.tar.gz", hash = "sha256:5ffbf500e499ab7912b1ba6d26cef26480d9ae411532019bb78d72562196e07b"}, - {file = "azure_mgmt_monitor-6.0.2-py3-none-any.whl", hash = "sha256:fe4cf41e6680b74a228f81451dc5522656d599c6f343ecf702fc790fda9a357b"}, -] - -[package.dependencies] -azure-common = ">=1.1,<2.0" -azure-mgmt-core = ">=1.3.2,<2.0.0" -isodate = ">=0.6.1,<1.0.0" - -[[package]] -name = "azure-mgmt-network" -version = "28.1.0" -description = "Microsoft Azure Network Management Client Library for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "azure_mgmt_network-28.1.0-py3-none-any.whl", hash = "sha256:8ddb0e9ec8f10c9c152d60fc945908d113e4591f397ea3e40b92290ec2b01658"}, - {file = "azure_mgmt_network-28.1.0.tar.gz", hash = "sha256:8c84bffb5ec75c6e0244e58ecf07c00d5fc421d616b0cb369c6fe585af33cf87"}, -] - -[package.dependencies] -azure-common = ">=1.1" -azure-mgmt-core = ">=1.3.2" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[[package]] -name = "azure-mgmt-postgresqlflexibleservers" -version = "1.1.0" -description = "Microsoft Azure Postgresqlflexibleservers Management Client Library for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "azure_mgmt_postgresqlflexibleservers-1.1.0-py3-none-any.whl", hash = "sha256:87ddb5a5e6d12c45769485d234cfe0322140e3a0a7636d0e61fb00ac544b5d20"}, - {file = "azure_mgmt_postgresqlflexibleservers-1.1.0.tar.gz", hash = "sha256:9ede9d8ba63e9d2879cb74adc903c649af3bc5460a02787287b0cd18d754af14"}, -] - -[package.dependencies] -azure-common = ">=1.1" -azure-mgmt-core = ">=1.3.2" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[[package]] -name = "azure-mgmt-rdbms" -version = "10.1.0" -description = "Microsoft Azure RDBMS Management Client Library for Python" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "azure-mgmt-rdbms-10.1.0.zip", hash = "sha256:a87d401c876c84734cdd4888af551e4a1461b4b328d9816af60cb8ac5979f035"}, - {file = "azure_mgmt_rdbms-10.1.0-py3-none-any.whl", hash = "sha256:8eac17d1341a91d7ed914435941ba917b5ef1568acabc3e65653603966a7cc88"}, -] - -[package.dependencies] -azure-common = ">=1.1,<2.0" -azure-mgmt-core = ">=1.3.0,<2.0.0" -msrest = ">=0.6.21" - -[[package]] -name = "azure-mgmt-recoveryservices" -version = "3.1.0" -description = "Microsoft Azure Recovery Services Client Library for Python" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "azure_mgmt_recoveryservices-3.1.0-py3-none-any.whl", hash = "sha256:21c58afdf4ae66806783e95f8cd17e3bec31be7178c48784db21f0b05de7fa66"}, - {file = "azure_mgmt_recoveryservices-3.1.0.tar.gz", hash = "sha256:7f2db98401708cf145322f50bc491caf7967bec4af3bf7b0984b9f07d3092687"}, -] - -[package.dependencies] -azure-common = ">=1.1" -azure-mgmt-core = ">=1.5.0" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[[package]] -name = "azure-mgmt-recoveryservicesbackup" -version = "9.2.0" -description = "Microsoft Azure Recovery Services Backup Management Client Library for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "azure_mgmt_recoveryservicesbackup-9.2.0-py3-none-any.whl", hash = "sha256:c0002858d0166b6a10189a1fd580a49c83dc31b111e98010a5b2ea0f767dfff1"}, - {file = "azure_mgmt_recoveryservicesbackup-9.2.0.tar.gz", hash = "sha256:c402b3e22a6c3879df56bc37e0063142c3352c5102599ff102d19824f1b32b29"}, -] - -[package.dependencies] -azure-common = ">=1.1" -azure-mgmt-core = ">=1.3.2" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[[package]] -name = "azure-mgmt-resource" -version = "24.0.0" -description = "Microsoft Azure Resource Management Client Library for Python" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "azure_mgmt_resource-24.0.0-py3-none-any.whl", hash = "sha256:27b32cd223e2784269f5a0db3c282042886ee4072d79cedc638438ece7cd0df4"}, - {file = "azure_mgmt_resource-24.0.0.tar.gz", hash = "sha256:cf6b8995fcdd407ac9ff1dd474087129429a1d90dbb1ac77f97c19b96237b265"}, -] - -[package.dependencies] -azure-common = ">=1.1" -azure-mgmt-core = ">=1.5.0" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[[package]] -name = "azure-mgmt-search" -version = "9.1.0" -description = "Microsoft Azure Search Management Client Library for Python" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "azure-mgmt-search-9.1.0.tar.gz", hash = "sha256:53bc6eeadb0974d21f120bb21bb5e6827df6d650e17347460fd83e2d68883599"}, - {file = "azure_mgmt_search-9.1.0-py3-none-any.whl", hash = "sha256:488ff81477e980e2b7abf0b857387c74ebbad419e6f6126044e3e6fad2da72b6"}, -] - -[package.dependencies] -azure-common = ">=1.1,<2.0" -azure-mgmt-core = ">=1.3.2,<2.0.0" -isodate = ">=0.6.1,<1.0.0" - -[[package]] -name = "azure-mgmt-security" -version = "7.0.0" -description = "Microsoft Azure Security Center Management Client Library for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "azure-mgmt-security-7.0.0.tar.gz", hash = "sha256:5912eed7e9d3758fdca8d26e1dc26b41943dc4703208a1184266e2c252e1ad66"}, - {file = "azure_mgmt_security-7.0.0-py3-none-any.whl", hash = "sha256:85a6d8b7a5cd74884a548ed53fed034449f54a9989edd64e9020c5837db96933"}, -] - -[package.dependencies] -azure-common = ">=1.1" -azure-mgmt-core = ">=1.3.2" -isodate = ">=0.6.1" - -[[package]] -name = "azure-mgmt-sql" -version = "3.0.1" -description = "Microsoft Azure SQL Management Client Library for Python" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "azure-mgmt-sql-3.0.1.zip", hash = "sha256:129042cc011225e27aee6ef2697d585fa5722e5d1aeb0038af6ad2451a285457"}, - {file = "azure_mgmt_sql-3.0.1-py2.py3-none-any.whl", hash = "sha256:1d1dd940d4d41be4ee319aad626341251572a5bf4a2addec71779432d9a1381f"}, -] - -[package.dependencies] -azure-common = ">=1.1,<2.0" -azure-mgmt-core = ">=1.2.0,<2.0.0" -msrest = ">=0.6.21" - -[[package]] -name = "azure-mgmt-storage" -version = "22.1.1" -description = "Microsoft Azure Storage Management Client Library for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "azure_mgmt_storage-22.1.1-py3-none-any.whl", hash = "sha256:a4a4064918dcfa4f1cbebada5bf064935d66f2a3647a2f46a1f1c9348736f5d9"}, - {file = "azure_mgmt_storage-22.1.1.tar.gz", hash = "sha256:25aaa5ae8c40c30e2f91f8aae6f52906b0557e947d5c1b9817d4ff9decc11340"}, -] - -[package.dependencies] -azure-common = ">=1.1" -azure-mgmt-core = ">=1.3.2" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[[package]] -name = "azure-mgmt-subscription" -version = "3.1.1" -description = "Microsoft Azure Subscription Management Client Library for Python" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "azure-mgmt-subscription-3.1.1.zip", hash = "sha256:4e255b4ce9b924357bb8c5009b3c88a2014d3203b2495e2256fa027bf84e800e"}, - {file = "azure_mgmt_subscription-3.1.1-py3-none-any.whl", hash = "sha256:38d4574a8d47fa17e3587d756e296cb63b82ad8fb21cd8543bcee443a502bf48"}, -] - -[package.dependencies] -azure-common = ">=1.1,<2.0" -azure-mgmt-core = ">=1.3.2,<2.0.0" -msrest = ">=0.7.1" - -[[package]] -name = "azure-mgmt-web" -version = "8.0.0" -description = "Microsoft Azure Web Apps Management Client Library for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "azure_mgmt_web-8.0.0-py3-none-any.whl", hash = "sha256:0536aac05bfc673b56ed930f2966b77856e84df675d376e782a7af6bb92449af"}, - {file = "azure_mgmt_web-8.0.0.tar.gz", hash = "sha256:c8d9c042c09db7aacb20270a9effed4d4e651e365af32d80897b84dc7bf35098"}, -] - -[package.dependencies] -azure-common = ">=1.1" -azure-mgmt-core = ">=1.3.2" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[[package]] -name = "azure-monitor-query" -version = "2.0.0" -description = "Microsoft Corporation Azure Monitor Query Client Library for Python" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "azure_monitor_query-2.0.0-py3-none-any.whl", hash = "sha256:8f52d581271d785e12f49cd5aaa144b8910fb843db2373855a7ef94c7fc462ea"}, - {file = "azure_monitor_query-2.0.0.tar.gz", hash = "sha256:7b05f2fcac4fb67fc9f77a7d4c5d98a0f3099fb73b57c69ec1b080773994671b"}, -] - -[package.dependencies] -azure-core = ">=1.30.0" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[[package]] -name = "azure-storage-blob" -version = "12.24.1" -description = "Microsoft Azure Blob Storage Client Library for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "azure_storage_blob-12.24.1-py3-none-any.whl", hash = "sha256:77fb823fdbac7f3c11f7d86a5892e2f85e161e8440a7489babe2195bf248f09e"}, - {file = "azure_storage_blob-12.24.1.tar.gz", hash = "sha256:052b2a1ea41725ba12e2f4f17be85a54df1129e13ea0321f5a2fcc851cbf47d4"}, -] - -[package.dependencies] -azure-core = ">=1.30.0" -cryptography = ">=2.1.4" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[package.extras] -aio = ["azure-core[aio] (>=1.30.0)"] - -[[package]] -name = "backports-datetime-fromisoformat" -version = "2.0.3" -description = "Backport of Python 3.11's datetime.fromisoformat" -optional = false -python-versions = ">3" -groups = ["dev"] -markers = "python_version == \"3.10\"" -files = [ - {file = "backports_datetime_fromisoformat-2.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f681f638f10588fa3c101ee9ae2b63d3734713202ddfcfb6ec6cea0778a29d4"}, - {file = "backports_datetime_fromisoformat-2.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:cd681460e9142f1249408e5aee6d178c6d89b49e06d44913c8fdfb6defda8d1c"}, - {file = "backports_datetime_fromisoformat-2.0.3-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:ee68bc8735ae5058695b76d3bb2aee1d137c052a11c8303f1e966aa23b72b65b"}, - {file = "backports_datetime_fromisoformat-2.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8273fe7932db65d952a43e238318966eab9e49e8dd546550a41df12175cc2be4"}, - {file = "backports_datetime_fromisoformat-2.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39d57ea50aa5a524bb239688adc1d1d824c31b6094ebd39aa164d6cadb85de22"}, - {file = "backports_datetime_fromisoformat-2.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ac6272f87693e78209dc72e84cf9ab58052027733cd0721c55356d3c881791cf"}, - {file = "backports_datetime_fromisoformat-2.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:44c497a71f80cd2bcfc26faae8857cf8e79388e3d5fbf79d2354b8c360547d58"}, - {file = "backports_datetime_fromisoformat-2.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:6335a4c9e8af329cb1ded5ab41a666e1448116161905a94e054f205aa6d263bc"}, - {file = "backports_datetime_fromisoformat-2.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2e4b66e017253cdbe5a1de49e0eecff3f66cd72bcb1229d7db6e6b1832c0443"}, - {file = "backports_datetime_fromisoformat-2.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:43e2d648e150777e13bbc2549cc960373e37bf65bd8a5d2e0cef40e16e5d8dd0"}, - {file = "backports_datetime_fromisoformat-2.0.3-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:4ce6326fd86d5bae37813c7bf1543bae9e4c215ec6f5afe4c518be2635e2e005"}, - {file = "backports_datetime_fromisoformat-2.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7c8fac333bf860208fd522a5394369ee3c790d0aa4311f515fcc4b6c5ef8d75"}, - {file = "backports_datetime_fromisoformat-2.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24a4da5ab3aa0cc293dc0662a0c6d1da1a011dc1edcbc3122a288cfed13a0b45"}, - {file = "backports_datetime_fromisoformat-2.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:58ea11e3bf912bd0a36b0519eae2c5b560b3cb972ea756e66b73fb9be460af01"}, - {file = "backports_datetime_fromisoformat-2.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8a375c7dbee4734318714a799b6c697223e4bbb57232af37fbfff88fb48a14c6"}, - {file = "backports_datetime_fromisoformat-2.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:ac677b1664c4585c2e014739f6678137c8336815406052349c85898206ec7061"}, - {file = "backports_datetime_fromisoformat-2.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66ce47ee1ba91e146149cf40565c3d750ea1be94faf660ca733d8601e0848147"}, - {file = "backports_datetime_fromisoformat-2.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:8b7e069910a66b3bba61df35b5f879e5253ff0821a70375b9daf06444d046fa4"}, - {file = "backports_datetime_fromisoformat-2.0.3-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:a3b5d1d04a9e0f7b15aa1e647c750631a873b298cdd1255687bb68779fe8eb35"}, - {file = "backports_datetime_fromisoformat-2.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec1b95986430e789c076610aea704db20874f0781b8624f648ca9fb6ef67c6e1"}, - {file = "backports_datetime_fromisoformat-2.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffe5f793db59e2f1d45ec35a1cf51404fdd69df9f6952a0c87c3060af4c00e32"}, - {file = "backports_datetime_fromisoformat-2.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:620e8e73bd2595dfff1b4d256a12b67fce90ece3de87b38e1dde46b910f46f4d"}, - {file = "backports_datetime_fromisoformat-2.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4cf9c0a985d68476c1cabd6385c691201dda2337d7453fb4da9679ce9f23f4e7"}, - {file = "backports_datetime_fromisoformat-2.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:d144868a73002e6e2e6fef72333e7b0129cecdd121aa8f1edba7107fd067255d"}, - {file = "backports_datetime_fromisoformat-2.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e81b26497a17c29595bc7df20bc6a872ceea5f8c9d6537283945d4b6396aec10"}, - {file = "backports_datetime_fromisoformat-2.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:5ba00ead8d9d82fd6123eb4891c566d30a293454e54e32ff7ead7644f5f7e575"}, - {file = "backports_datetime_fromisoformat-2.0.3-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:24d574cb4072e1640b00864e94c4c89858033936ece3fc0e1c6f7179f120d0a8"}, - {file = "backports_datetime_fromisoformat-2.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9735695a66aad654500b0193525e590c693ab3368478ce07b34b443a1ea5e824"}, - {file = "backports_datetime_fromisoformat-2.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63d39709e17eb72685d052ac82acf0763e047f57c86af1b791505b1fec96915d"}, - {file = "backports_datetime_fromisoformat-2.0.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:1ea2cc84224937d6b9b4c07f5cb7c667f2bde28c255645ba27f8a675a7af8234"}, - {file = "backports_datetime_fromisoformat-2.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4024e6d35a9fdc1b3fd6ac7a673bd16cb176c7e0b952af6428b7129a70f72cce"}, - {file = "backports_datetime_fromisoformat-2.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5e2dcc94dc9c9ab8704409d86fcb5236316e9dcef6feed8162287634e3568f4c"}, - {file = "backports_datetime_fromisoformat-2.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fa2de871801d824c255fac7e5e7e50f2be6c9c376fd9268b40c54b5e9da91f42"}, - {file = "backports_datetime_fromisoformat-2.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:1314d4923c1509aa9696712a7bc0c7160d3b7acf72adafbbe6c558d523f5d491"}, - {file = "backports_datetime_fromisoformat-2.0.3-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:b750ecba3a8815ad8bc48311552f3f8ab99dd2326d29df7ff670d9c49321f48f"}, - {file = "backports_datetime_fromisoformat-2.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d5117dce805d8a2f78baeddc8c6127281fa0a5e2c40c6dd992ba6b2b367876"}, - {file = "backports_datetime_fromisoformat-2.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb35f607bd1cbe37b896379d5f5ed4dc298b536f4b959cb63180e05cacc0539d"}, - {file = "backports_datetime_fromisoformat-2.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:61c74710900602637d2d145dda9720c94e303380803bf68811b2a151deec75c2"}, - {file = "backports_datetime_fromisoformat-2.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ece59af54ebf67ecbfbbf3ca9066f5687879e36527ad69d8b6e3ac565d565a62"}, - {file = "backports_datetime_fromisoformat-2.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:d0a7c5f875068efe106f62233bc712d50db4d07c13c7db570175c7857a7b5dbd"}, - {file = "backports_datetime_fromisoformat-2.0.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90e202e72a3d5aae673fcc8c9a4267d56b2f532beeb9173361293625fe4d2039"}, - {file = "backports_datetime_fromisoformat-2.0.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2df98ef1b76f5a58bb493dda552259ba60c3a37557d848e039524203951c9f06"}, - {file = "backports_datetime_fromisoformat-2.0.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7100adcda5e818b5a894ad0626e38118bb896a347f40ebed8981155675b9ba7b"}, - {file = "backports_datetime_fromisoformat-2.0.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e410383f5d6a449a529d074e88af8bc80020bb42b402265f9c02c8358c11da5"}, - {file = "backports_datetime_fromisoformat-2.0.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2797593760da6bcc32c4a13fa825af183cd4bfd333c60b3dbf84711afca26ef"}, - {file = "backports_datetime_fromisoformat-2.0.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35a144fd681a0bea1013ccc4cd3fd4dc758ea17ee23dca019c02b82ec46fc0c4"}, - {file = "backports_datetime_fromisoformat-2.0.3.tar.gz", hash = "sha256:b58edc8f517b66b397abc250ecc737969486703a66eb97e01e6d51291b1a139d"}, -] - -[[package]] -name = "bandit" -version = "1.8.3" -description = "Security oriented static analyser for python code." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "bandit-1.8.3-py3-none-any.whl", hash = "sha256:28f04dc0d258e1dd0f99dee8eefa13d1cb5e3fde1a5ab0c523971f97b289bcd8"}, - {file = "bandit-1.8.3.tar.gz", hash = "sha256:f5847beb654d309422985c36644649924e0ea4425c76dec2e89110b87506193a"}, -] - -[package.dependencies] -colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} -PyYAML = ">=5.3.1" -rich = "*" -stevedore = ">=1.20.0" - -[package.extras] -baseline = ["GitPython (>=3.1.30)"] -sarif = ["jschema-to-python (>=1.2.3)", "sarif-om (>=1.0.4)"] -test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "pylint (==1.9.4)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)"] -toml = ["tomli (>=1.1.0) ; python_version < \"3.11\""] -yaml = ["PyYAML"] - -[[package]] -name = "black" -version = "25.1.0" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32"}, - {file = "black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da"}, - {file = "black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7"}, - {file = "black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9"}, - {file = "black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0"}, - {file = "black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299"}, - {file = "black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096"}, - {file = "black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2"}, - {file = "black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b"}, - {file = "black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc"}, - {file = "black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f"}, - {file = "black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba"}, - {file = "black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f"}, - {file = "black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3"}, - {file = "black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171"}, - {file = "black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18"}, - {file = "black-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0"}, - {file = "black-25.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f"}, - {file = "black-25.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e"}, - {file = "black-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355"}, - {file = "black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717"}, - {file = "black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.10)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - -[[package]] -name = "blinker" -version = "1.9.0" -description = "Fast, simple object-to-object and broadcast signaling" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"}, - {file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"}, -] - -[[package]] -name = "boto3" -version = "1.40.61" -description = "The AWS SDK for Python" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "boto3-1.40.61-py3-none-any.whl", hash = "sha256:6b9c57b2a922b5d8c17766e29ed792586a818098efe84def27c8f582b33f898c"}, - {file = "boto3-1.40.61.tar.gz", hash = "sha256:d6c56277251adf6c2bdd25249feae625abe4966831676689ff23b4694dea5b12"}, -] - -[package.dependencies] -botocore = ">=1.40.61,<1.41.0" -jmespath = ">=0.7.1,<2.0.0" -s3transfer = ">=0.14.0,<0.15.0" - -[package.extras] -crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] - -[[package]] -name = "botocore" -version = "1.40.61" -description = "Low-level, data-driven core of boto 3." -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "botocore-1.40.61-py3-none-any.whl", hash = "sha256:17ebae412692fd4824f99cde0f08d50126dc97954008e5ba2b522eb049238aa7"}, - {file = "botocore-1.40.61.tar.gz", hash = "sha256:a2487ad69b090f9cccd64cf07c7021cd80ee9c0655ad974f87045b02f3ef52cd"}, -] - -[package.dependencies] -jmespath = ">=0.7.1,<2.0.0" -python-dateutil = ">=2.1,<3.0.0" -urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""} - -[package.extras] -crt = ["awscrt (==0.27.6)"] - -[[package]] -name = "cachetools" -version = "5.5.2" -description = "Extensible memoizing collections and decorators" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a"}, - {file = "cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4"}, -] - -[[package]] -name = "certifi" -version = "2025.7.14" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -files = [ - {file = "certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2"}, - {file = "certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995"}, -] - -[[package]] -name = "cffi" -version = "2.0.0" -description = "Foreign Function Interface for Python calling C code." -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -markers = "platform_python_implementation != \"PyPy\"" -files = [ - {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, - {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, - {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, - {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, - {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, - {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, - {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, - {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, - {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, - {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, - {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, - {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, - {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, - {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, - {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, - {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, - {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, - {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, - {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, - {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, - {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, - {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, - {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, - {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, - {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, - {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, - {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, - {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, - {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, - {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, - {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, - {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, - {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, - {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, - {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, - {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, - {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, - {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, - {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, - {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, - {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, - {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, - {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, - {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, - {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, - {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, - {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, - {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, - {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, - {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, - {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, -] - -[package.dependencies] -pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} - -[[package]] -name = "cfn-lint" -version = "1.38.0" -description = "Checks CloudFormation templates for practices and behaviour that could potentially be improved" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "cfn_lint-1.38.0-py3-none-any.whl", hash = "sha256:336753eb5259022f6581e26cece84ef729ef3d06ca1445d02ade9a966474d915"}, - {file = "cfn_lint-1.38.0.tar.gz", hash = "sha256:356275ec13a1f9cd20f87ef4ff7396a34aefad633f4783126d8f5507400b925d"}, -] - -[package.dependencies] -aws-sam-translator = ">=1.97.0" -jsonpatch = "*" -networkx = ">=2.4,<4" -pyyaml = ">5.4" -regex = "*" -sympy = ">=1.0.0" -typing_extensions = "*" - -[package.extras] -full = ["jschema_to_python (>=1.2.3,<1.3.0)", "junit-xml (>=1.9,<2.0)", "pydot", "sarif-om (>=1.0.4,<1.1.0)"] -graph = ["pydot"] -junit = ["junit-xml (>=1.9,<2.0)"] -sarif = ["jschema_to_python (>=1.2.3,<1.3.0)", "sarif-om (>=1.0.4,<1.1.0)"] - -[[package]] -name = "charset-normalizer" -version = "3.4.2" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -files = [ - {file = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-win32.whl", hash = "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-win32.whl", hash = "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e"}, - {file = "charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0"}, - {file = "charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63"}, -] - -[[package]] -name = "circuitbreaker" -version = "2.1.3" -description = "Python Circuit Breaker pattern implementation" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "circuitbreaker-2.1.3-py3-none-any.whl", hash = "sha256:87ba6a3ed03fdc7032bc175561c2b04d52ade9d5faf94ca2b035fbdc5e6b1dd1"}, - {file = "circuitbreaker-2.1.3.tar.gz", hash = "sha256:1a4baee510f7bea3c91b194dcce7c07805fe96c4423ed5594b75af438531d084"}, -] - -[[package]] -name = "click" -version = "8.2.1" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.10" -groups = ["main", "dev"] -files = [ - {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, - {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "click-plugins" -version = "1.1.1.2" -description = "An extension module for click to enable registering CLI commands via setuptools entry-points." -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "click_plugins-1.1.1.2-py2.py3-none-any.whl", hash = "sha256:008d65743833ffc1f5417bf0e78e8d2c23aab04d9745ba817bd3e71b0feb6aa6"}, - {file = "click_plugins-1.1.1.2.tar.gz", hash = "sha256:d7af3984a99d243c131aa1a828331e7630f4a88a9741fd05c927b204bcf92261"}, -] - -[package.dependencies] -click = ">=4.0" - -[package.extras] -dev = ["coveralls", "pytest (>=3.6)", "pytest-cov", "wheel"] - -[[package]] -name = "cloudflare" -version = "4.3.1" -description = "The official Python library for the cloudflare API" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "cloudflare-4.3.1-py3-none-any.whl", hash = "sha256:6927135a5ee5633d6e2e1952ca0484745e933727aeeb189996d2ad9d292071c6"}, - {file = "cloudflare-4.3.1.tar.gz", hash = "sha256:b1e1c6beeb8d98f63bfe0a1cba874fc4e22e000bcc490544f956c689b3b5b258"}, -] - -[package.dependencies] -anyio = ">=3.5.0,<5" -distro = ">=1.7.0,<2" -httpx = ">=0.23.0,<1" -pydantic = ">=1.9.0,<3" -sniffio = "*" -typing-extensions = ">=4.10,<5" - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["main", "dev"] -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] -markers = {dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} - -[[package]] -name = "contextlib2" -version = "21.6.0" -description = "Backports and enhancements for the contextlib module" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "contextlib2-21.6.0-py2.py3-none-any.whl", hash = "sha256:3fbdb64466afd23abaf6c977627b75b6139a5a3e8ce38405c5b413aed7a0471f"}, - {file = "contextlib2-21.6.0.tar.gz", hash = "sha256:ab1e2bfe1d01d968e1b7e8d9023bc51ef3509bba217bb730cee3827e1ee82869"}, -] - -[[package]] -name = "coverage" -version = "7.6.12" -description = "Code coverage measurement for Python" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "coverage-7.6.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:704c8c8c6ce6569286ae9622e534b4f5b9759b6f2cd643f1c1a61f666d534fe8"}, - {file = "coverage-7.6.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ad7525bf0241e5502168ae9c643a2f6c219fa0a283001cee4cf23a9b7da75879"}, - {file = "coverage-7.6.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06097c7abfa611c91edb9e6920264e5be1d6ceb374efb4986f38b09eed4cb2fe"}, - {file = "coverage-7.6.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:220fa6c0ad7d9caef57f2c8771918324563ef0d8272c94974717c3909664e674"}, - {file = "coverage-7.6.12-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3688b99604a24492bcfe1c106278c45586eb819bf66a654d8a9a1433022fb2eb"}, - {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d1a987778b9c71da2fc8948e6f2656da6ef68f59298b7e9786849634c35d2c3c"}, - {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cec6b9ce3bd2b7853d4a4563801292bfee40b030c05a3d29555fd2a8ee9bd68c"}, - {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ace9048de91293e467b44bce0f0381345078389814ff6e18dbac8fdbf896360e"}, - {file = "coverage-7.6.12-cp310-cp310-win32.whl", hash = "sha256:ea31689f05043d520113e0552f039603c4dd71fa4c287b64cb3606140c66f425"}, - {file = "coverage-7.6.12-cp310-cp310-win_amd64.whl", hash = "sha256:676f92141e3c5492d2a1596d52287d0d963df21bf5e55c8b03075a60e1ddf8aa"}, - {file = "coverage-7.6.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e18aafdfb3e9ec0d261c942d35bd7c28d031c5855dadb491d2723ba54f4c3015"}, - {file = "coverage-7.6.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66fe626fd7aa5982cdebad23e49e78ef7dbb3e3c2a5960a2b53632f1f703ea45"}, - {file = "coverage-7.6.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ef01d70198431719af0b1f5dcbefc557d44a190e749004042927b2a3fed0702"}, - {file = "coverage-7.6.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e92ae5a289a4bc4c0aae710c0948d3c7892e20fd3588224ebe242039573bf0"}, - {file = "coverage-7.6.12-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e695df2c58ce526eeab11a2e915448d3eb76f75dffe338ea613c1201b33bab2f"}, - {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d74c08e9aaef995f8c4ef6d202dbd219c318450fe2a76da624f2ebb9c8ec5d9f"}, - {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e995b3b76ccedc27fe4f477b349b7d64597e53a43fc2961db9d3fbace085d69d"}, - {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b1f097878d74fe51e1ddd1be62d8e3682748875b461232cf4b52ddc6e6db0bba"}, - {file = "coverage-7.6.12-cp311-cp311-win32.whl", hash = "sha256:1f7ffa05da41754e20512202c866d0ebfc440bba3b0ed15133070e20bf5aeb5f"}, - {file = "coverage-7.6.12-cp311-cp311-win_amd64.whl", hash = "sha256:e216c5c45f89ef8971373fd1c5d8d1164b81f7f5f06bbf23c37e7908d19e8558"}, - {file = "coverage-7.6.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b172f8e030e8ef247b3104902cc671e20df80163b60a203653150d2fc204d1ad"}, - {file = "coverage-7.6.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:641dfe0ab73deb7069fb972d4d9725bf11c239c309ce694dd50b1473c0f641c3"}, - {file = "coverage-7.6.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e549f54ac5f301e8e04c569dfdb907f7be71b06b88b5063ce9d6953d2d58574"}, - {file = "coverage-7.6.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959244a17184515f8c52dcb65fb662808767c0bd233c1d8a166e7cf74c9ea985"}, - {file = "coverage-7.6.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda1c5f347550c359f841d6614fb8ca42ae5cb0b74d39f8a1e204815ebe25750"}, - {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ceeb90c3eda1f2d8c4c578c14167dbd8c674ecd7d38e45647543f19839dd6ea"}, - {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f16f44025c06792e0fb09571ae454bcc7a3ec75eeb3c36b025eccf501b1a4c3"}, - {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b076e625396e787448d27a411aefff867db2bffac8ed04e8f7056b07024eed5a"}, - {file = "coverage-7.6.12-cp312-cp312-win32.whl", hash = "sha256:00b2086892cf06c7c2d74983c9595dc511acca00665480b3ddff749ec4fb2a95"}, - {file = "coverage-7.6.12-cp312-cp312-win_amd64.whl", hash = "sha256:7ae6eabf519bc7871ce117fb18bf14e0e343eeb96c377667e3e5dd12095e0288"}, - {file = "coverage-7.6.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:488c27b3db0ebee97a830e6b5a3ea930c4a6e2c07f27a5e67e1b3532e76b9ef1"}, - {file = "coverage-7.6.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d1095bbee1851269f79fd8e0c9b5544e4c00c0c24965e66d8cba2eb5bb535fd"}, - {file = "coverage-7.6.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0533adc29adf6a69c1baa88c3d7dbcaadcffa21afbed3ca7a225a440e4744bf9"}, - {file = "coverage-7.6.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53c56358d470fa507a2b6e67a68fd002364d23c83741dbc4c2e0680d80ca227e"}, - {file = "coverage-7.6.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cbb1a3027c79ca6310bf101014614f6e6e18c226474606cf725238cf5bc2d4"}, - {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79cac3390bfa9836bb795be377395f28410811c9066bc4eefd8015258a7578c6"}, - {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b148068e881faa26d878ff63e79650e208e95cf1c22bd3f77c3ca7b1d9821a3"}, - {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8bec2ac5da793c2685ce5319ca9bcf4eee683b8a1679051f8e6ec04c4f2fd7dc"}, - {file = "coverage-7.6.12-cp313-cp313-win32.whl", hash = "sha256:200e10beb6ddd7c3ded322a4186313d5ca9e63e33d8fab4faa67ef46d3460af3"}, - {file = "coverage-7.6.12-cp313-cp313-win_amd64.whl", hash = "sha256:2b996819ced9f7dbb812c701485d58f261bef08f9b85304d41219b1496b591ef"}, - {file = "coverage-7.6.12-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:299cf973a7abff87a30609879c10df0b3bfc33d021e1adabc29138a48888841e"}, - {file = "coverage-7.6.12-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b467a8c56974bf06e543e69ad803c6865249d7a5ccf6980457ed2bc50312703"}, - {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2458f275944db8129f95d91aee32c828a408481ecde3b30af31d552c2ce284a0"}, - {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a9d8be07fb0832636a0f72b80d2a652fe665e80e720301fb22b191c3434d924"}, - {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d47376a4f445e9743f6c83291e60adb1b127607a3618e3185bbc8091f0467b"}, - {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b95574d06aa9d2bd6e5cc35a5bbe35696342c96760b69dc4287dbd5abd4ad51d"}, - {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ecea0c38c9079570163d663c0433a9af4094a60aafdca491c6a3d248c7432827"}, - {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2251fabcfee0a55a8578a9d29cecfee5f2de02f11530e7d5c5a05859aa85aee9"}, - {file = "coverage-7.6.12-cp313-cp313t-win32.whl", hash = "sha256:eb5507795caabd9b2ae3f1adc95f67b1104971c22c624bb354232d65c4fc90b3"}, - {file = "coverage-7.6.12-cp313-cp313t-win_amd64.whl", hash = "sha256:f60a297c3987c6c02ffb29effc70eadcbb412fe76947d394a1091a3615948e2f"}, - {file = "coverage-7.6.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e7575ab65ca8399c8c4f9a7d61bbd2d204c8b8e447aab9d355682205c9dd948d"}, - {file = "coverage-7.6.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8161d9fbc7e9fe2326de89cd0abb9f3599bccc1287db0aba285cb68d204ce929"}, - {file = "coverage-7.6.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a1e465f398c713f1b212400b4e79a09829cd42aebd360362cd89c5bdc44eb87"}, - {file = "coverage-7.6.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f25d8b92a4e31ff1bd873654ec367ae811b3a943583e05432ea29264782dc32c"}, - {file = "coverage-7.6.12-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a936309a65cc5ca80fa9f20a442ff9e2d06927ec9a4f54bcba9c14c066323f2"}, - {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aa6f302a3a0b5f240ee201297fff0bbfe2fa0d415a94aeb257d8b461032389bd"}, - {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f973643ef532d4f9be71dd88cf7588936685fdb576d93a79fe9f65bc337d9d73"}, - {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:78f5243bb6b1060aed6213d5107744c19f9571ec76d54c99cc15938eb69e0e86"}, - {file = "coverage-7.6.12-cp39-cp39-win32.whl", hash = "sha256:69e62c5034291c845fc4df7f8155e8544178b6c774f97a99e2734b05eb5bed31"}, - {file = "coverage-7.6.12-cp39-cp39-win_amd64.whl", hash = "sha256:b01a840ecc25dce235ae4c1b6a0daefb2a203dba0e6e980637ee9c2f6ee0df57"}, - {file = "coverage-7.6.12-pp39.pp310-none-any.whl", hash = "sha256:7e39e845c4d764208e7b8f6a21c541ade741e2c41afabdfa1caa28687a3c98cf"}, - {file = "coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953"}, - {file = "coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2"}, -] - -[package.dependencies] -tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} - -[package.extras] -toml = ["tomli ; python_full_version <= \"3.11.0a6\""] - -[[package]] -name = "cryptography" -version = "46.0.6" -description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -optional = false -python-versions = "!=3.9.0,!=3.9.1,>=3.8" -groups = ["main", "dev"] -files = [ - {file = "cryptography-46.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8"}, - {file = "cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30"}, - {file = "cryptography-46.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a"}, - {file = "cryptography-46.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175"}, - {file = "cryptography-46.0.6-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463"}, - {file = "cryptography-46.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97"}, - {file = "cryptography-46.0.6-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c"}, - {file = "cryptography-46.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507"}, - {file = "cryptography-46.0.6-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19"}, - {file = "cryptography-46.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738"}, - {file = "cryptography-46.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c"}, - {file = "cryptography-46.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f"}, - {file = "cryptography-46.0.6-cp311-abi3-win32.whl", hash = "sha256:bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2"}, - {file = "cryptography-46.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124"}, - {file = "cryptography-46.0.6-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:2ef9e69886cbb137c2aef9772c2e7138dc581fad4fcbcf13cc181eb5a3ab6275"}, - {file = "cryptography-46.0.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7f417f034f91dcec1cb6c5c35b07cdbb2ef262557f701b4ecd803ee8cefed4f4"}, - {file = "cryptography-46.0.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d24c13369e856b94892a89ddf70b332e0b70ad4a5c43cf3e9cb71d6d7ffa1f7b"}, - {file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:aad75154a7ac9039936d50cf431719a2f8d4ed3d3c277ac03f3339ded1a5e707"}, - {file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3c21d92ed15e9cfc6eb64c1f5a0326db22ca9c2566ca46d845119b45b4400361"}, - {file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:4668298aef7cddeaf5c6ecc244c2302a2b8e40f384255505c22875eebb47888b"}, - {file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8ce35b77aaf02f3b59c90b2c8a05c73bac12cea5b4e8f3fbece1f5fddea5f0ca"}, - {file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c89eb37fae9216985d8734c1afd172ba4927f5a05cfd9bf0e4863c6d5465b013"}, - {file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:ed418c37d095aeddf5336898a132fba01091f0ac5844e3e8018506f014b6d2c4"}, - {file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:69cf0056d6947edc6e6760e5f17afe4bea06b56a9ac8a06de9d2bd6b532d4f3a"}, - {file = "cryptography-46.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e7304c4f4e9490e11efe56af6713983460ee0780f16c63f219984dab3af9d2d"}, - {file = "cryptography-46.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b928a3ca837c77a10e81a814a693f2295200adb3352395fad024559b7be7a736"}, - {file = "cryptography-46.0.6-cp314-cp314t-win32.whl", hash = "sha256:97c8115b27e19e592a05c45d0dd89c57f81f841cc9880e353e0d3bf25b2139ed"}, - {file = "cryptography-46.0.6-cp314-cp314t-win_amd64.whl", hash = "sha256:c797e2517cb7880f8297e2c0f43bb910e91381339336f75d2c1c2cbf811b70b4"}, - {file = "cryptography-46.0.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a"}, - {file = "cryptography-46.0.6-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8"}, - {file = "cryptography-46.0.6-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77"}, - {file = "cryptography-46.0.6-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290"}, - {file = "cryptography-46.0.6-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410"}, - {file = "cryptography-46.0.6-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d"}, - {file = "cryptography-46.0.6-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70"}, - {file = "cryptography-46.0.6-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d"}, - {file = "cryptography-46.0.6-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa"}, - {file = "cryptography-46.0.6-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58"}, - {file = "cryptography-46.0.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb"}, - {file = "cryptography-46.0.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72"}, - {file = "cryptography-46.0.6-cp38-abi3-win32.whl", hash = "sha256:7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c"}, - {file = "cryptography-46.0.6-cp38-abi3-win_amd64.whl", hash = "sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f"}, - {file = "cryptography-46.0.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:2ea0f37e9a9cf0df2952893ad145fd9627d326a59daec9b0802480fa3bcd2ead"}, - {file = "cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a3e84d5ec9ba01f8fd03802b2147ba77f0c8f2617b2aff254cedd551844209c8"}, - {file = "cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:12f0fa16cc247b13c43d56d7b35287ff1569b5b1f4c5e87e92cc4fcc00cd10c0"}, - {file = "cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:50575a76e2951fe7dbd1f56d181f8c5ceeeb075e9ff88e7ad997d2f42af06e7b"}, - {file = "cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:90e5f0a7b3be5f40c3a0a0eafb32c681d8d2c181fc2a1bdabe9b3f611d9f6b1a"}, - {file = "cryptography-46.0.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6728c49e3b2c180ef26f8e9f0a883a2c585638db64cf265b49c9ba10652d430e"}, - {file = "cryptography-46.0.6.tar.gz", hash = "sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759"}, -] - -[package.dependencies] -cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""} -typing-extensions = {version = ">=4.13.2", markers = "python_full_version < \"3.11.0\""} - -[package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"] -docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] -nox = ["nox[uv] (>=2024.4.15)"] -pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"] -sdist = ["build (>=1.0.0)"] -ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi (>=2024)", "cryptography-vectors (==46.0.6)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] -test-randomorder = ["pytest-randomly"] - -[[package]] -name = "darabonba-core" -version = "1.0.4" -description = "The darabonba module of alibabaCloud Python SDK." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "darabonba_core-1.0.4-py3-none-any.whl", hash = "sha256:4c3bc1d76d5af1087297b6afde8e960ea2f54f93e725e2df8453f0b4bb27dd24"}, - {file = "darabonba_core-1.0.4.tar.gz", hash = "sha256:6ede4e9bfd458148bab19ab2331716ae9b5c226ba5f6d221de6f88ee65704137"}, -] - -[package.dependencies] -aiohttp = ">=3.7.0,<4.0.0" -alibabacloud-tea = "*" -requests = ">=2.21.0,<3.0.0" - -[[package]] -name = "dash" -version = "3.1.1" -description = "A Python framework for building reactive web-apps. Developed by Plotly." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "dash-3.1.1-py3-none-any.whl", hash = "sha256:66fff37e79c6aa114cd55aea13683d1e9afe0e3f96b35388baca95ff6cfdad23"}, - {file = "dash-3.1.1.tar.gz", hash = "sha256:916b31cec46da0a3339da0e9df9f446126aa7f293c0544e07adf9fe4ba060b18"}, -] - -[package.dependencies] -Flask = ">=1.0.4,<3.2" -importlib-metadata = "*" -nest-asyncio = "*" -plotly = ">=5.0.0" -requests = "*" -retrying = "*" -setuptools = "*" -typing-extensions = ">=4.1.1" -Werkzeug = "<3.2" - -[package.extras] -async = ["flask[async]"] -celery = ["celery[redis] (>=5.1.2,<5.4.0)", "kombu (<5.4.0)", "redis (>=3.5.3,<=5.0.4)"] -ci = ["black (==22.3.0)", "flake8 (==7.0.0)", "flaky (==3.8.1)", "flask-talisman (==1.0.0)", "ipython (<9.0.0)", "jupyterlab (<4.0.0)", "mimesis (<=11.1.0)", "mock (==4.0.3)", "mypy (==1.15.0) ; python_version >= \"3.12\"", "numpy (<=1.26.3)", "openpyxl", "orjson (==3.10.3)", "pandas (>=1.4.0)", "pyarrow", "pylint (==3.0.3)", "pyright (==1.1.398) ; python_version >= \"3.7\"", "pytest-mock", "pytest-rerunfailures", "pytest-sugar (==0.9.6)", "pyzmq (==25.1.2)", "xlrd (>=2.0.1)"] -compress = ["flask-compress"] -dev = ["PyYAML (>=5.4.1)", "coloredlogs (>=15.0.1)", "fire (>=0.4.0)"] -diskcache = ["diskcache (>=5.2.1)", "multiprocess (>=0.70.12)", "psutil (>=5.8.0)"] -testing = ["beautifulsoup4 (>=4.8.2)", "cryptography", "dash-testing-stub (>=0.0.2)", "lxml (>=4.6.2)", "multiprocess (>=0.70.12)", "percy (>=2.0.2)", "psutil (>=5.8.0)", "pytest (>=6.0.2)", "requests[security] (>=2.21.0)", "selenium (>=3.141.0,<=4.2.0)", "waitress (>=1.4.4)"] - -[[package]] -name = "dash-bootstrap-components" -version = "2.0.3" -description = "Bootstrap themed components for use in Plotly Dash" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "dash_bootstrap_components-2.0.3-py3-none-any.whl", hash = "sha256:82754d3d001ad5482b8a82b496c7bf98a1c68d2669d607a89dda7ec627304af5"}, - {file = "dash_bootstrap_components-2.0.3.tar.gz", hash = "sha256:5c161b04a6e7ed19a7d54e42f070c29fd6c385d5a7797e7a82999aa2fc15b1de"}, -] - -[package.dependencies] -dash = ">=3.0.4" - -[package.extras] -pandas = ["numpy (>=2.0.2)", "pandas (>=2.2.3)"] - -[[package]] -name = "decorator" -version = "5.2.1" -description = "Decorators for Humans" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a"}, - {file = "decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360"}, -] - -[[package]] -name = "defusedxml" -version = "0.7.1" -description = "XML bomb protection for Python stdlib modules" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -groups = ["main"] -files = [ - {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, - {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, -] - -[[package]] -name = "detect-secrets" -version = "1.5.0" -description = "Tool for detecting secrets in the codebase" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "detect_secrets-1.5.0-py3-none-any.whl", hash = "sha256:e24e7b9b5a35048c313e983f76c4bd09dad89f045ff059e354f9943bf45aa060"}, - {file = "detect_secrets-1.5.0.tar.gz", hash = "sha256:6bb46dcc553c10df51475641bb30fd69d25645cc12339e46c824c1e0c388898a"}, -] - -[package.dependencies] -pyyaml = "*" -requests = "*" - -[package.extras] -gibberish = ["gibberish-detector"] -word-list = ["pyahocorasick"] - -[[package]] -name = "dill" -version = "0.4.0" -description = "serialize all of Python" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049"}, - {file = "dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0"}, -] - -[package.extras] -graph = ["objgraph (>=1.7.2)"] -profile = ["gprof2dot (>=2022.7.29)"] - -[[package]] -name = "distro" -version = "1.9.0" -description = "Distro - an OS platform information API" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, - {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, -] - -[[package]] -name = "dnspython" -version = "2.7.0" -description = "DNS toolkit" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86"}, - {file = "dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1"}, -] - -[package.extras] -dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "hypercorn (>=0.16.0)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "quart-trio (>=0.11.0)", "sphinx (>=7.2.0)", "sphinx-rtd-theme (>=2.0.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"] -dnssec = ["cryptography (>=43)"] -doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"] -doq = ["aioquic (>=1.0.0)"] -idna = ["idna (>=3.7)"] -trio = ["trio (>=0.23)"] -wmi = ["wmi (>=1.5.1)"] - -[[package]] -name = "docker" -version = "7.1.0" -description = "A Python library for the Docker Engine API." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0"}, - {file = "docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c"}, -] - -[package.dependencies] -pywin32 = {version = ">=304", markers = "sys_platform == \"win32\""} -requests = ">=2.26.0" -urllib3 = ">=1.26.0" - -[package.extras] -dev = ["coverage (==7.2.7)", "pytest (==7.4.2)", "pytest-cov (==4.1.0)", "pytest-timeout (==2.1.0)", "ruff (==0.1.8)"] -docs = ["myst-parser (==0.18.0)", "sphinx (==5.1.1)"] -ssh = ["paramiko (>=2.4.3)"] -websockets = ["websocket-client (>=1.3.0)"] - -[[package]] -name = "dogpile-cache" -version = "1.5.0" -description = "A caching front-end based on the Dogpile lock." -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "dogpile_cache-1.5.0-py3-none-any.whl", hash = "sha256:dc7b47d37844db15e8fdc0243c1b58857a2ddc52a5118237a97127bac200e18d"}, - {file = "dogpile_cache-1.5.0.tar.gz", hash = "sha256:849c5573c9a38f155cd4173103c702b637ede0361c12e864876877d0cd125eec"}, -] - -[package.dependencies] -decorator = ">=4.0.0" -stevedore = ">=3.0.0" -typing_extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} - -[package.extras] -bmemcached = ["python-binary-memcached"] -memcached = ["python-memcached"] -pifpaf = ["pifpaf (>=3.3.0)"] -pylibmc = ["pylibmc"] -pymemcache = ["pymemcache"] -redis = ["redis"] -valkey = ["valkey"] - -[[package]] -name = "dparse" -version = "0.6.4" -description = "A parser for Python dependency files" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "dparse-0.6.4-py3-none-any.whl", hash = "sha256:fbab4d50d54d0e739fbb4dedfc3d92771003a5b9aa8545ca7a7045e3b174af57"}, - {file = "dparse-0.6.4.tar.gz", hash = "sha256:90b29c39e3edc36c6284c82c4132648eaf28a01863eb3c231c2512196132201a"}, -] - -[package.dependencies] -packaging = "*" -tomli = {version = "*", markers = "python_version < \"3.11\""} - -[package.extras] -all = ["pipenv", "poetry", "pyyaml"] -conda = ["pyyaml"] -pipenv = ["pipenv"] -poetry = ["poetry"] - -[[package]] -name = "dulwich" -version = "0.23.0" -description = "Python Git Library" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "dulwich-0.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c13b0d5a9009cde23ecb8cb201df6e23e2a7a82c5e2d6ba6443fbb322c9befc6"}, - {file = "dulwich-0.23.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:a68faf8612bf93de1285048d6ad13160f0fb3c5596a86e694e78f4e212886fa5"}, - {file = "dulwich-0.23.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:d971566826f16ec67c70641c1fbdb337323aa5b533799bc5a4641f4750e73b36"}, - {file = "dulwich-0.23.0-cp310-cp310-win32.whl", hash = "sha256:27d970adf539806dfc4fe3e4c9e8dc6ebf0318977a56e24d22f13413535a51ba"}, - {file = "dulwich-0.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:025178533e884ffdb0d9d8db4b8870745d438cbfecb782fd1b56c3b6438e86cf"}, - {file = "dulwich-0.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d68498fdda13ab00791b483daab3bcfe9f9721c037aa458695e6ad81640c57cc"}, - {file = "dulwich-0.23.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:cb7bb930b12471a1cfcea4b3d25a671dc0ad32573f0ad25684684298959a1527"}, - {file = "dulwich-0.23.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a2abbce32fd2bc7902bcc5f69b10bf22576810de21651baaa864b78fd7aec261"}, - {file = "dulwich-0.23.0-cp311-cp311-win32.whl", hash = "sha256:9e3151f10ce2a9ff91bca64c74345217f53bdd947dc958032343822009832f7a"}, - {file = "dulwich-0.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:3ae9f1d9dc92d4e9a3f89ba2c55221f7b6442c5dd93b3f6f539a3c9eb3f37bdd"}, - {file = "dulwich-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:52cdef66a7994d29528ca79ca59452518bbba3fd56a9c61c61f6c467c1c7956e"}, - {file = "dulwich-0.23.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:d473888a6ab9ed5d4a4c3f053cbe5b77f72d54b6efdf5688fed76094316e571e"}, - {file = "dulwich-0.23.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:19fcf20224c641a61c774da92f098fbaae9938c7e17a52841e64092adf7e78f9"}, - {file = "dulwich-0.23.0-cp312-cp312-win32.whl", hash = "sha256:7fc8b76b704ef35cd001e993e3aa4e1d666a2064bf467c07c560f12b2959dcaf"}, - {file = "dulwich-0.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:cb0566b888b578325350b4d67c61a0de35d417e9877560e3a6df88cae4576a59"}, - {file = "dulwich-0.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:624e2223c8b705b3a217f9c8d3bfed3a573093be0b0ba033c46cba8411fb9630"}, - {file = "dulwich-0.23.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:b4eaf326d15bb3fc5316c777b0312f0fe02f6f82a4368cd971d0ce2167b7ec34"}, - {file = "dulwich-0.23.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:d754afaf7c133a015c75cc2be11703138b4be932e0eeeb2c70add56083f31109"}, - {file = "dulwich-0.23.0-cp313-cp313-win32.whl", hash = "sha256:ac53ec438bde3c1f479782c34240479b36cd47230d091979137b7ecc12c0242e"}, - {file = "dulwich-0.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:50d3b4ba45671fb8b7d2afbd02c10b4edbc3290a1f92260e64098b409e9ca35c"}, - {file = "dulwich-0.23.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d8e18ea3fa49f10932077f39c0b960b5045870c550c3d7c74f3cfaac09457cd6"}, - {file = "dulwich-0.23.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:3e6df0eb8cca21f210e3ddce2ccb64482646893dbec2fee9f3411d037595bf7b"}, - {file = "dulwich-0.23.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:90c0064d7df8e7fe83d3a03c7d60b9e07a92698b18442f926199b2c3f0bf34d4"}, - {file = "dulwich-0.23.0-cp39-cp39-win32.whl", hash = "sha256:84eef513aba501cbc1f223863f3b4b351fe732d3fb590cab9bdf5d33eb1a1248"}, - {file = "dulwich-0.23.0-cp39-cp39-win_amd64.whl", hash = "sha256:dce943da48217c26e15790fd6df62d27a7f1d067102780351ebf2635fc0ba482"}, - {file = "dulwich-0.23.0-py3-none-any.whl", hash = "sha256:d8da6694ca332bb48775e35ee2215aa4673821164a91b83062f699c69f7cd135"}, - {file = "dulwich-0.23.0.tar.gz", hash = "sha256:0aa6c2489dd5e978b27e9b75983b7331a66c999f0efc54ebe37cab808ed322ae"}, -] - -[package.dependencies] -urllib3 = ">=1.25" - -[package.extras] -dev = ["dissolve (>=0.1.1)", "mypy (==1.16.0)", "ruff (==0.11.13)"] -fastimport = ["fastimport"] -https = ["urllib3 (>=1.24.1)"] -merge = ["merge3"] -paramiko = ["paramiko"] -pgp = ["gpg"] - -[[package]] -name = "durationpy" -version = "0.10" -description = "Module for converting between datetime.timedelta and Go's Duration strings." -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286"}, - {file = "durationpy-0.10.tar.gz", hash = "sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba"}, -] - -[[package]] -name = "email-validator" -version = "2.2.0" -description = "A robust email address syntax and deliverability validation library." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631"}, - {file = "email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7"}, -] - -[package.dependencies] -dnspython = ">=2.0.0" -idna = ">=2.0.0" - -[[package]] -name = "exceptiongroup" -version = "1.3.0" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -markers = "python_version == \"3.10\"" -files = [ - {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, - {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, -] - -[package.dependencies] -typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "execnet" -version = "2.1.1" -description = "execnet: rapid multi-Python deployment" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"}, - {file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"}, -] - -[package.extras] -testing = ["hatch", "pre-commit", "pytest", "tox"] - -[[package]] -name = "filelock" -version = "3.20.3" -description = "A platform independent file lock." -optional = false -python-versions = ">=3.10" -groups = ["main", "dev"] -files = [ - {file = "filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1"}, - {file = "filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1"}, -] - -[[package]] -name = "flake8" -version = "7.1.2" -description = "the modular source code checker: pep8 pyflakes and co" -optional = false -python-versions = ">=3.8.1" -groups = ["dev"] -files = [ - {file = "flake8-7.1.2-py2.py3-none-any.whl", hash = "sha256:1cbc62e65536f65e6d754dfe6f1bada7f5cf392d6f5db3c2b85892466c3e7c1a"}, - {file = "flake8-7.1.2.tar.gz", hash = "sha256:c586ffd0b41540951ae41af572e6790dbd49fc12b3aa2541685d253d9bd504bd"}, -] - -[package.dependencies] -mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.12.0,<2.13.0" -pyflakes = ">=3.2.0,<3.3.0" - -[[package]] -name = "flask" -version = "3.1.1" -description = "A simple framework for building complex web applications." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "flask-3.1.1-py3-none-any.whl", hash = "sha256:07aae2bb5eaf77993ef57e357491839f5fd9f4dc281593a81a9e4d79a24f295c"}, - {file = "flask-3.1.1.tar.gz", hash = "sha256:284c7b8f2f58cb737f0cf1c30fd7eaf0ccfcde196099d24ecede3fc2005aa59e"}, -] - -[package.dependencies] -blinker = ">=1.9.0" -click = ">=8.1.3" -itsdangerous = ">=2.2.0" -jinja2 = ">=3.1.2" -markupsafe = ">=2.1.1" -werkzeug = ">=3.1.0" - -[package.extras] -async = ["asgiref (>=3.2)"] -dotenv = ["python-dotenv"] - -[[package]] -name = "freezegun" -version = "1.5.1" -description = "Let your Python tests travel through time" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "freezegun-1.5.1-py3-none-any.whl", hash = "sha256:bf111d7138a8abe55ab48a71755673dbaa4ab87f4cff5634a4442dfec34c15f1"}, - {file = "freezegun-1.5.1.tar.gz", hash = "sha256:b29dedfcda6d5e8e083ce71b2b542753ad48cfec44037b3fc79702e2980a89e9"}, -] - -[package.dependencies] -python-dateutil = ">=2.7" - -[[package]] -name = "frozenlist" -version = "1.7.0" -description = "A list-like structure which implements collections.abc.MutableSequence" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "frozenlist-1.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cc4df77d638aa2ed703b878dd093725b72a824c3c546c076e8fdf276f78ee84a"}, - {file = "frozenlist-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:716a9973a2cc963160394f701964fe25012600f3d311f60c790400b00e568b61"}, - {file = "frozenlist-1.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0fd1bad056a3600047fb9462cff4c5322cebc59ebf5d0a3725e0ee78955001d"}, - {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3789ebc19cb811163e70fe2bd354cea097254ce6e707ae42e56f45e31e96cb8e"}, - {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af369aa35ee34f132fcfad5be45fbfcde0e3a5f6a1ec0712857f286b7d20cca9"}, - {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac64b6478722eeb7a3313d494f8342ef3478dff539d17002f849101b212ef97c"}, - {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f89f65d85774f1797239693cef07ad4c97fdd0639544bad9ac4b869782eb1981"}, - {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1073557c941395fdfcfac13eb2456cb8aad89f9de27bae29fabca8e563b12615"}, - {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed8d2fa095aae4bdc7fdd80351009a48d286635edffee66bf865e37a9125c50"}, - {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:24c34bea555fe42d9f928ba0a740c553088500377448febecaa82cc3e88aa1fa"}, - {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:69cac419ac6a6baad202c85aaf467b65ac860ac2e7f2ac1686dc40dbb52f6577"}, - {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:960d67d0611f4c87da7e2ae2eacf7ea81a5be967861e0c63cf205215afbfac59"}, - {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:41be2964bd4b15bf575e5daee5a5ce7ed3115320fb3c2b71fca05582ffa4dc9e"}, - {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:46d84d49e00c9429238a7ce02dc0be8f6d7cd0cd405abd1bebdc991bf27c15bd"}, - {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:15900082e886edb37480335d9d518cec978afc69ccbc30bd18610b7c1b22a718"}, - {file = "frozenlist-1.7.0-cp310-cp310-win32.whl", hash = "sha256:400ddd24ab4e55014bba442d917203c73b2846391dd42ca5e38ff52bb18c3c5e"}, - {file = "frozenlist-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:6eb93efb8101ef39d32d50bce242c84bcbddb4f7e9febfa7b524532a239b4464"}, - {file = "frozenlist-1.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aa51e147a66b2d74de1e6e2cf5921890de6b0f4820b257465101d7f37b49fb5a"}, - {file = "frozenlist-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9b35db7ce1cd71d36ba24f80f0c9e7cff73a28d7a74e91fe83e23d27c7828750"}, - {file = "frozenlist-1.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34a69a85e34ff37791e94542065c8416c1afbf820b68f720452f636d5fb990cd"}, - {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a646531fa8d82c87fe4bb2e596f23173caec9185bfbca5d583b4ccfb95183e2"}, - {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:79b2ffbba483f4ed36a0f236ccb85fbb16e670c9238313709638167670ba235f"}, - {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a26f205c9ca5829cbf82bb2a84b5c36f7184c4316617d7ef1b271a56720d6b30"}, - {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bcacfad3185a623fa11ea0e0634aac7b691aa925d50a440f39b458e41c561d98"}, - {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72c1b0fe8fe451b34f12dce46445ddf14bd2a5bcad7e324987194dc8e3a74c86"}, - {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61d1a5baeaac6c0798ff6edfaeaa00e0e412d49946c53fae8d4b8e8b3566c4ae"}, - {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7edf5c043c062462f09b6820de9854bf28cc6cc5b6714b383149745e287181a8"}, - {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d50ac7627b3a1bd2dcef6f9da89a772694ec04d9a61b66cf87f7d9446b4a0c31"}, - {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ce48b2fece5aeb45265bb7a58259f45027db0abff478e3077e12b05b17fb9da7"}, - {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:fe2365ae915a1fafd982c146754e1de6ab3478def8a59c86e1f7242d794f97d5"}, - {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:45a6f2fdbd10e074e8814eb98b05292f27bad7d1883afbe009d96abdcf3bc898"}, - {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:21884e23cffabb157a9dd7e353779077bf5b8f9a58e9b262c6caad2ef5f80a56"}, - {file = "frozenlist-1.7.0-cp311-cp311-win32.whl", hash = "sha256:284d233a8953d7b24f9159b8a3496fc1ddc00f4db99c324bd5fb5f22d8698ea7"}, - {file = "frozenlist-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:387cbfdcde2f2353f19c2f66bbb52406d06ed77519ac7ee21be0232147c2592d"}, - {file = "frozenlist-1.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3dbf9952c4bb0e90e98aec1bd992b3318685005702656bc6f67c1a32b76787f2"}, - {file = "frozenlist-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f5906d3359300b8a9bb194239491122e6cf1444c2efb88865426f170c262cdb"}, - {file = "frozenlist-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3dabd5a8f84573c8d10d8859a50ea2dec01eea372031929871368c09fa103478"}, - {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa57daa5917f1738064f302bf2626281a1cb01920c32f711fbc7bc36111058a8"}, - {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c193dda2b6d49f4c4398962810fa7d7c78f032bf45572b3e04dd5249dff27e08"}, - {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe2b675cf0aaa6d61bf8fbffd3c274b3c9b7b1623beb3809df8a81399a4a9c4"}, - {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fc5d5cda37f62b262405cf9652cf0856839c4be8ee41be0afe8858f17f4c94b"}, - {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d5ce521d1dd7d620198829b87ea002956e4319002ef0bc8d3e6d045cb4646e"}, - {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:488d0a7d6a0008ca0db273c542098a0fa9e7dfaa7e57f70acef43f32b3f69dca"}, - {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:15a7eaba63983d22c54d255b854e8108e7e5f3e89f647fc854bd77a237e767df"}, - {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1eaa7e9c6d15df825bf255649e05bd8a74b04a4d2baa1ae46d9c2d00b2ca2cb5"}, - {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4389e06714cfa9d47ab87f784a7c5be91d3934cd6e9a7b85beef808297cc025"}, - {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:73bd45e1488c40b63fe5a7df892baf9e2a4d4bb6409a2b3b78ac1c6236178e01"}, - {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99886d98e1643269760e5fe0df31e5ae7050788dd288947f7f007209b8c33f08"}, - {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:290a172aae5a4c278c6da8a96222e6337744cd9c77313efe33d5670b9f65fc43"}, - {file = "frozenlist-1.7.0-cp312-cp312-win32.whl", hash = "sha256:426c7bc70e07cfebc178bc4c2bf2d861d720c4fff172181eeb4a4c41d4ca2ad3"}, - {file = "frozenlist-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:563b72efe5da92e02eb68c59cb37205457c977aa7a449ed1b37e6939e5c47c6a"}, - {file = "frozenlist-1.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee80eeda5e2a4e660651370ebffd1286542b67e268aa1ac8d6dbe973120ef7ee"}, - {file = "frozenlist-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d1a81c85417b914139e3a9b995d4a1c84559afc839a93cf2cb7f15e6e5f6ed2d"}, - {file = "frozenlist-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbb65198a9132ebc334f237d7b0df163e4de83fb4f2bdfe46c1e654bdb0c5d43"}, - {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dab46c723eeb2c255a64f9dc05b8dd601fde66d6b19cdb82b2e09cc6ff8d8b5d"}, - {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6aeac207a759d0dedd2e40745575ae32ab30926ff4fa49b1635def65806fddee"}, - {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd8c4e58ad14b4fa7802b8be49d47993182fdd4023393899632c88fd8cd994eb"}, - {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04fb24d104f425da3540ed83cbfc31388a586a7696142004c577fa61c6298c3f"}, - {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a5c505156368e4ea6b53b5ac23c92d7edc864537ff911d2fb24c140bb175e60"}, - {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bd7eb96a675f18aa5c553eb7ddc24a43c8c18f22e1f9925528128c052cdbe00"}, - {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:05579bf020096fe05a764f1f84cd104a12f78eaab68842d036772dc6d4870b4b"}, - {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:376b6222d114e97eeec13d46c486facd41d4f43bab626b7c3f6a8b4e81a5192c"}, - {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0aa7e176ebe115379b5b1c95b4096fb1c17cce0847402e227e712c27bdb5a949"}, - {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3fbba20e662b9c2130dc771e332a99eff5da078b2b2648153a40669a6d0e36ca"}, - {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f4410a0a601d349dd406b5713fec59b4cee7e71678d5b17edda7f4655a940b"}, - {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cdfaaec6a2f9327bf43c933c0319a7c429058e8537c508964a133dffee412e"}, - {file = "frozenlist-1.7.0-cp313-cp313-win32.whl", hash = "sha256:5fc4df05a6591c7768459caba1b342d9ec23fa16195e744939ba5914596ae3e1"}, - {file = "frozenlist-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:52109052b9791a3e6b5d1b65f4b909703984b770694d3eb64fad124c835d7cba"}, - {file = "frozenlist-1.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a6f86e4193bb0e235ef6ce3dde5cbabed887e0b11f516ce8a0f4d3b33078ec2d"}, - {file = "frozenlist-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:82d664628865abeb32d90ae497fb93df398a69bb3434463d172b80fc25b0dd7d"}, - {file = "frozenlist-1.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:912a7e8375a1c9a68325a902f3953191b7b292aa3c3fb0d71a216221deca460b"}, - {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9537c2777167488d539bc5de2ad262efc44388230e5118868e172dd4a552b146"}, - {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f34560fb1b4c3e30ba35fa9a13894ba39e5acfc5f60f57d8accde65f46cc5e74"}, - {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acd03d224b0175f5a850edc104ac19040d35419eddad04e7cf2d5986d98427f1"}, - {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2038310bc582f3d6a09b3816ab01737d60bf7b1ec70f5356b09e84fb7408ab1"}, - {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c05e4c8e5f36e5e088caa1bf78a687528f83c043706640a92cb76cd6999384"}, - {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:765bb588c86e47d0b68f23c1bee323d4b703218037765dcf3f25c838c6fecceb"}, - {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:32dc2e08c67d86d0969714dd484fd60ff08ff81d1a1e40a77dd34a387e6ebc0c"}, - {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:c0303e597eb5a5321b4de9c68e9845ac8f290d2ab3f3e2c864437d3c5a30cd65"}, - {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a47f2abb4e29b3a8d0b530f7c3598badc6b134562b1a5caee867f7c62fee51e3"}, - {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3d688126c242a6fabbd92e02633414d40f50bb6002fa4cf995a1d18051525657"}, - {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4e7e9652b3d367c7bd449a727dc79d5043f48b88d0cbfd4f9f1060cf2b414104"}, - {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1a85e345b4c43db8b842cab1feb41be5cc0b10a1830e6295b69d7310f99becaf"}, - {file = "frozenlist-1.7.0-cp313-cp313t-win32.whl", hash = "sha256:3a14027124ddb70dfcee5148979998066897e79f89f64b13328595c4bdf77c81"}, - {file = "frozenlist-1.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3bf8010d71d4507775f658e9823210b7427be36625b387221642725b515dcf3e"}, - {file = "frozenlist-1.7.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cea3dbd15aea1341ea2de490574a4a37ca080b2ae24e4b4f4b51b9057b4c3630"}, - {file = "frozenlist-1.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7d536ee086b23fecc36c2073c371572374ff50ef4db515e4e503925361c24f71"}, - {file = "frozenlist-1.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dfcebf56f703cb2e346315431699f00db126d158455e513bd14089d992101e44"}, - {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:974c5336e61d6e7eb1ea5b929cb645e882aadab0095c5a6974a111e6479f8878"}, - {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c70db4a0ab5ab20878432c40563573229a7ed9241506181bba12f6b7d0dc41cb"}, - {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1137b78384eebaf70560a36b7b229f752fb64d463d38d1304939984d5cb887b6"}, - {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e793a9f01b3e8b5c0bc646fb59140ce0efcc580d22a3468d70766091beb81b35"}, - {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74739ba8e4e38221d2c5c03d90a7e542cb8ad681915f4ca8f68d04f810ee0a87"}, - {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e63344c4e929b1a01e29bc184bbb5fd82954869033765bfe8d65d09e336a677"}, - {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2ea2a7369eb76de2217a842f22087913cdf75f63cf1307b9024ab82dfb525938"}, - {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:836b42f472a0e006e02499cef9352ce8097f33df43baaba3e0a28a964c26c7d2"}, - {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e22b9a99741294b2571667c07d9f8cceec07cb92aae5ccda39ea1b6052ed4319"}, - {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:9a19e85cc503d958abe5218953df722748d87172f71b73cf3c9257a91b999890"}, - {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f22dac33bb3ee8fe3e013aa7b91dc12f60d61d05b7fe32191ffa84c3aafe77bd"}, - {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9ccec739a99e4ccf664ea0775149f2749b8a6418eb5b8384b4dc0a7d15d304cb"}, - {file = "frozenlist-1.7.0-cp39-cp39-win32.whl", hash = "sha256:b3950f11058310008a87757f3eee16a8e1ca97979833239439586857bc25482e"}, - {file = "frozenlist-1.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:43a82fce6769c70f2f5a06248b614a7d268080a9d20f7457ef10ecee5af82b63"}, - {file = "frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e"}, - {file = "frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f"}, -] - -[[package]] -name = "google-api-core" -version = "2.25.1" -description = "Google API client core library" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "google_api_core-2.25.1-py3-none-any.whl", hash = "sha256:8a2a56c1fef82987a524371f99f3bd0143702fecc670c72e600c1cda6bf8dbb7"}, - {file = "google_api_core-2.25.1.tar.gz", hash = "sha256:d2aaa0b13c78c61cb3f4282c464c046e45fbd75755683c9c525e6e8f7ed0a5e8"}, -] - -[package.dependencies] -google-auth = ">=2.14.1,<3.0.0" -googleapis-common-protos = ">=1.56.2,<2.0.0" -proto-plus = ">=1.22.3,<2.0.0" -protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" -requests = ">=2.18.0,<3.0.0" - -[package.extras] -async-rest = ["google-auth[aiohttp] (>=2.35.0,<3.0.0)"] -grpc = ["grpcio (>=1.33.2,<2.0.0)", "grpcio (>=1.49.1,<2.0.0) ; python_version >= \"3.11\"", "grpcio-status (>=1.33.2,<2.0.0)", "grpcio-status (>=1.49.1,<2.0.0) ; python_version >= \"3.11\""] -grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.0)"] -grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.0)"] - -[[package]] -name = "google-api-python-client" -version = "2.163.0" -description = "Google API Client Library for Python" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "google_api_python_client-2.163.0-py2.py3-none-any.whl", hash = "sha256:080e8bc0669cb4c1fb8efb8da2f5b91a2625d8f0e7796cfad978f33f7016c6c4"}, - {file = "google_api_python_client-2.163.0.tar.gz", hash = "sha256:88dee87553a2d82176e2224648bf89272d536c8f04dcdda37ef0a71473886dd7"}, -] - -[package.dependencies] -google-api-core = ">=1.31.5,<2.0.dev0 || >2.3.0,<3.0.0.dev0" -google-auth = ">=1.32.0,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0.dev0" -google-auth-httplib2 = ">=0.2.0,<1.0.0" -httplib2 = ">=0.19.0,<1.dev0" -uritemplate = ">=3.0.1,<5" - -[[package]] -name = "google-auth" -version = "2.40.3" -description = "Google Authentication Library" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "google_auth-2.40.3-py2.py3-none-any.whl", hash = "sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca"}, - {file = "google_auth-2.40.3.tar.gz", hash = "sha256:500c3a29adedeb36ea9cf24b8d10858e152f2412e3ca37829b3fa18e33d63b77"}, -] - -[package.dependencies] -cachetools = ">=2.0.0,<6.0" -pyasn1-modules = ">=0.2.1" -rsa = ">=3.1.4,<5" - -[package.extras] -aiohttp = ["aiohttp (>=3.6.2,<4.0.0)", "requests (>=2.20.0,<3.0.0)"] -enterprise-cert = ["cryptography", "pyopenssl"] -pyjwt = ["cryptography (<39.0.0) ; python_version < \"3.8\"", "cryptography (>=38.0.3)", "pyjwt (>=2.0)"] -pyopenssl = ["cryptography (<39.0.0) ; python_version < \"3.8\"", "cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] -reauth = ["pyu2f (>=0.1.5)"] -requests = ["requests (>=2.20.0,<3.0.0)"] -testing = ["aiohttp (<3.10.0)", "aiohttp (>=3.6.2,<4.0.0)", "aioresponses", "cryptography (<39.0.0) ; python_version < \"3.8\"", "cryptography (>=38.0.3)", "flask", "freezegun", "grpcio", "mock", "oauth2client", "packaging", "pyjwt (>=2.0)", "pyopenssl (<24.3.0)", "pyopenssl (>=20.0.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-localserver", "pyu2f (>=0.1.5)", "requests (>=2.20.0,<3.0.0)", "responses", "urllib3"] -urllib3 = ["packaging", "urllib3"] - -[[package]] -name = "google-auth-httplib2" -version = "0.2.0" -description = "Google Authentication Library: httplib2 transport" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "google-auth-httplib2-0.2.0.tar.gz", hash = "sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05"}, - {file = "google_auth_httplib2-0.2.0-py2.py3-none-any.whl", hash = "sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d"}, -] - -[package.dependencies] -google-auth = "*" -httplib2 = ">=0.19.0" - -[[package]] -name = "googleapis-common-protos" -version = "1.70.0" -description = "Common protobufs used in Google APIs" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "googleapis_common_protos-1.70.0-py3-none-any.whl", hash = "sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8"}, - {file = "googleapis_common_protos-1.70.0.tar.gz", hash = "sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257"}, -] - -[package.dependencies] -protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" - -[package.extras] -grpc = ["grpcio (>=1.44.0,<2.0.0)"] - -[[package]] -name = "graphemeu" -version = "0.7.2" -description = "Unicode grapheme helpers" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "graphemeu-0.7.2-py3-none-any.whl", hash = "sha256:1444520f6899fd30114fc2a39f297d86d10fa0f23bf7579f772f8bc7efaa2542"}, - {file = "graphemeu-0.7.2.tar.gz", hash = "sha256:42bbe373d7c146160f286cd5f76b1a8ad29172d7333ce10705c5cc282462a4f8"}, -] - -[package.extras] -dev = ["pytest"] -docs = ["sphinx", "sphinx-autobuild"] - -[[package]] -name = "graphql-core" -version = "3.2.6" -description = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL." -optional = false -python-versions = "<4,>=3.6" -groups = ["dev"] -files = [ - {file = "graphql_core-3.2.6-py3-none-any.whl", hash = "sha256:78b016718c161a6fb20a7d97bbf107f331cd1afe53e45566c59f776ed7f0b45f"}, - {file = "graphql_core-3.2.6.tar.gz", hash = "sha256:c08eec22f9e40f0bd61d805907e3b3b1b9a320bc606e23dc145eebca07c8fbab"}, -] - -[[package]] -name = "h11" -version = "0.16.0" -description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, - {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, -] - -[[package]] -name = "h2" -version = "4.3.0" -description = "Pure-Python HTTP/2 protocol implementation" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd"}, - {file = "h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1"}, -] - -[package.dependencies] -hpack = ">=4.1,<5" -hyperframe = ">=6.1,<7" - -[[package]] -name = "hpack" -version = "4.1.0" -description = "Pure-Python HPACK header encoding" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496"}, - {file = "hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca"}, -] - -[[package]] -name = "httpcore" -version = "1.0.9" -description = "A minimal low-level HTTP client." -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, - {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, -] - -[package.dependencies] -certifi = "*" -h11 = ">=0.16" - -[package.extras] -asyncio = ["anyio (>=4.0,<5.0)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] -trio = ["trio (>=0.22.0,<1.0)"] - -[[package]] -name = "httplib2" -version = "0.22.0" -description = "A comprehensive HTTP client library." -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -groups = ["main"] -files = [ - {file = "httplib2-0.22.0-py3-none-any.whl", hash = "sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc"}, - {file = "httplib2-0.22.0.tar.gz", hash = "sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81"}, -] - -[package.dependencies] -pyparsing = {version = ">=2.4.2,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.0.2 || >3.0.2,<3.0.3 || >3.0.3,<4", markers = "python_version > \"3.0\""} - -[[package]] -name = "httpx" -version = "0.28.1" -description = "The next generation HTTP client." -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, - {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, -] - -[package.dependencies] -anyio = "*" -certifi = "*" -h2 = {version = ">=3,<5", optional = true, markers = "extra == \"http2\""} -httpcore = "==1.*" -idna = "*" - -[package.extras] -brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] -cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "hyperframe" -version = "6.1.0" -description = "Pure-Python HTTP/2 framing" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5"}, - {file = "hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08"}, -] - -[[package]] -name = "iamdata" -version = "0.1.202507281" -description = "IAM data for AWS actions, resources, and conditions based on IAM policy documents. Checked for updates daily." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "iamdata-0.1.202507281-py3-none-any.whl", hash = "sha256:654b8deacf757cd16b9a5b7fc2175714aaff8343d4ed551194ed63242ae6c421"}, - {file = "iamdata-0.1.202507281.tar.gz", hash = "sha256:4050870068ca2fb044d03c46229bc8dbafb4f99db2f50c77297aafd437154ddd"}, -] - -[[package]] -name = "idna" -version = "3.10" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.6" -groups = ["main", "dev"] -files = [ - {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, - {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, -] - -[package.extras] -all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] - -[[package]] -name = "importlib-metadata" -version = "8.7.0" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"}, - {file = "importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"}, -] - -[package.dependencies] -zipp = ">=3.20" - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -perf = ["ipython"] -test = ["flufl.flake8", "importlib_resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] -type = ["pytest-mypy"] - -[[package]] -name = "iniconfig" -version = "2.1.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, - {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, -] - -[[package]] -name = "iso8601" -version = "2.1.0" -description = "Simple module to parse ISO 8601 dates" -optional = false -python-versions = ">=3.7,<4.0" -groups = ["main"] -files = [ - {file = "iso8601-2.1.0-py3-none-any.whl", hash = "sha256:aac4145c4dcb66ad8b648a02830f5e2ff6c24af20f4f482689be402db2429242"}, - {file = "iso8601-2.1.0.tar.gz", hash = "sha256:6b1d3829ee8921c4301998c909f7829fa9ed3cbdac0d3b16af2d743aed1ba8df"}, -] - -[[package]] -name = "isodate" -version = "0.7.2" -description = "An ISO 8601 date/time/duration parser and formatter" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15"}, - {file = "isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6"}, -] - -[[package]] -name = "isort" -version = "6.0.1" -description = "A Python utility / library to sort Python imports." -optional = false -python-versions = ">=3.9.0" -groups = ["dev"] -files = [ - {file = "isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615"}, - {file = "isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450"}, -] - -[package.extras] -colors = ["colorama"] -plugins = ["setuptools"] - -[[package]] -name = "itsdangerous" -version = "2.2.0" -description = "Safely pass data to untrusted environments and back." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, - {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, -] - -[[package]] -name = "jinja2" -version = "3.1.6" -description = "A very fast and expressive template engine." -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -files = [ - {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, - {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, -] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "jmespath" -version = "1.0.1" -description = "JSON Matching Expressions" -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -files = [ - {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, - {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, -] - -[[package]] -name = "joblib" -version = "1.5.3" -description = "Lightweight pipelining with Python functions" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713"}, - {file = "joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3"}, -] - -[[package]] -name = "joserfc" -version = "1.2.2" -description = "The ultimate Python library for JOSE RFCs, including JWS, JWE, JWK, JWA, JWT" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "joserfc-1.2.2-py3-none-any.whl", hash = "sha256:630cc36b2f11f749980401b0cd7305fab5735ee11d830d919bc207305d011358"}, - {file = "joserfc-1.2.2.tar.gz", hash = "sha256:0d2a84feecef96168635fd9bf288363fc75b4afef3d99691f77833c8e025d200"}, -] - -[package.dependencies] -cryptography = "*" - -[package.extras] -drafts = ["pycryptodome"] - -[[package]] -name = "jsonpatch" -version = "1.33" -description = "Apply JSON-Patches (RFC 6902)" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" -groups = ["main", "dev"] -files = [ - {file = "jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade"}, - {file = "jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c"}, -] - -[package.dependencies] -jsonpointer = ">=1.9" - -[[package]] -name = "jsonpath-ng" -version = "1.7.0" -description = "A final implementation of JSONPath for Python that aims to be standard compliant, including arithmetic and binary comparison operators and providing clear AST for metaprogramming." -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "jsonpath-ng-1.7.0.tar.gz", hash = "sha256:f6f5f7fd4e5ff79c785f1573b394043b39849fb2bb47bcead935d12b00beab3c"}, - {file = "jsonpath_ng-1.7.0-py2-none-any.whl", hash = "sha256:898c93fc173f0c336784a3fa63d7434297544b7198124a68f9a3ef9597b0ae6e"}, - {file = "jsonpath_ng-1.7.0-py3-none-any.whl", hash = "sha256:f3d7f9e848cba1b6da28c55b1c26ff915dc9e0b1ba7e752a53d6da8d5cbd00b6"}, -] - -[package.dependencies] -ply = "*" - -[[package]] -name = "jsonpointer" -version = "3.0.0" -description = "Identify specific nodes in a JSON document (RFC 6901)" -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -files = [ - {file = "jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942"}, - {file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"}, -] - -[[package]] -name = "jsonschema" -version = "4.23.0" -description = "An implementation of JSON Schema validation for Python" -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566"}, - {file = "jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4"}, -] - -[package.dependencies] -attrs = ">=22.2.0" -jsonschema-specifications = ">=2023.3.6" -referencing = ">=0.28.4" -rpds-py = ">=0.7.1" - -[package.extras] -format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] -format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=24.6.0)"] - -[[package]] -name = "jsonschema-path" -version = "0.3.4" -description = "JSONSchema Spec with object-oriented paths" -optional = false -python-versions = "<4.0.0,>=3.8.0" -groups = ["dev"] -files = [ - {file = "jsonschema_path-0.3.4-py3-none-any.whl", hash = "sha256:f502191fdc2b22050f9a81c9237be9d27145b9001c55842bece5e94e382e52f8"}, - {file = "jsonschema_path-0.3.4.tar.gz", hash = "sha256:8365356039f16cc65fddffafda5f58766e34bebab7d6d105616ab52bc4297001"}, -] - -[package.dependencies] -pathable = ">=0.4.1,<0.5.0" -PyYAML = ">=5.1" -referencing = "<0.37.0" -requests = ">=2.31.0,<3.0.0" - -[[package]] -name = "jsonschema-specifications" -version = "2025.4.1" -description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af"}, - {file = "jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608"}, -] - -[package.dependencies] -referencing = ">=0.31.0" - -[[package]] -name = "keystoneauth1" -version = "5.13.0" -description = "Authentication Library for OpenStack Identity" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "keystoneauth1-5.13.0-py3-none-any.whl", hash = "sha256:5ab81412eb0923ceb9c602cc3decce514b399523cb83d16b409ed3b0f9b03d41"}, - {file = "keystoneauth1-5.13.0.tar.gz", hash = "sha256:57c9ca407207899b50d8ff1ca8abb4a4e7427461bfc1877eb8519c3989ce63ec"}, -] - -[package.dependencies] -iso8601 = ">=2.0.0" -os-service-types = ">=1.2.0" -pbr = ">=2.0.0" -requests = ">=2.14.2" -stevedore = ">=1.20.0" -typing-extensions = ">=4.12" - -[package.extras] -betamax = ["PyYAML (>=3.13)", "betamax (>=0.7.0)", "fixtures (>=3.0.0)"] -kerberos = ["requests-kerberos (>=0.8.0)"] -oauth1 = ["oauthlib (>=0.6.2)"] -saml2 = ["lxml (>=4.2.0)"] - -[[package]] -name = "kubernetes" -version = "32.0.1" -description = "Kubernetes python client" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "kubernetes-32.0.1-py2.py3-none-any.whl", hash = "sha256:35282ab8493b938b08ab5526c7ce66588232df00ef5e1dbe88a419107dc10998"}, - {file = "kubernetes-32.0.1.tar.gz", hash = "sha256:42f43d49abd437ada79a79a16bd48a604d3471a117a8347e87db693f2ba0ba28"}, -] - -[package.dependencies] -certifi = ">=14.5.14" -durationpy = ">=0.7" -google-auth = ">=1.0.1" -oauthlib = ">=3.2.2" -python-dateutil = ">=2.5.3" -pyyaml = ">=5.4.1" -requests = "*" -requests-oauthlib = "*" -six = ">=1.9.0" -urllib3 = ">=1.24.2" -websocket-client = ">=0.32.0,<0.40.0 || >0.40.0,<0.41.dev0 || >=0.43.dev0" - -[package.extras] -adal = ["adal (>=1.0.2)"] - -[[package]] -name = "lazy-object-proxy" -version = "1.11.0" -description = "A fast and thorough lazy object proxy." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "lazy_object_proxy-1.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:132bc8a34f2f2d662a851acfd1b93df769992ed1b81e2b1fda7db3e73b0d5a18"}, - {file = "lazy_object_proxy-1.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:01261a3afd8621a1accb5682df2593dc7ec7d21d38f411011a5712dcd418fbed"}, - {file = "lazy_object_proxy-1.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:090935756cc041e191f22f4f9c7fd4fe9a454717067adf5b1bbd2ce3046b556e"}, - {file = "lazy_object_proxy-1.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:76ec715017f06410f57df442c1a8d66e6b5f7035077785b129817f5ae58810a4"}, - {file = "lazy_object_proxy-1.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9a9f39098e93a63618a79eef2889ae3cf0605f676cd4797fdfd49fcd7ddc318b"}, - {file = "lazy_object_proxy-1.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:ee13f67f4fcd044ef27bfccb1c93d39c100046fec1fad6e9a1fcdfd17492aeb3"}, - {file = "lazy_object_proxy-1.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd4c84eafd8dd15ea16f7d580758bc5c2ce1f752faec877bb2b1f9f827c329cd"}, - {file = "lazy_object_proxy-1.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:d2503427bda552d3aefcac92f81d9e7ca631e680a2268cbe62cd6a58de6409b7"}, - {file = "lazy_object_proxy-1.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0613116156801ab3fccb9e2b05ed83b08ea08c2517fdc6c6bc0d4697a1a376e3"}, - {file = "lazy_object_proxy-1.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bb03c507d96b65f617a6337dedd604399d35face2cdf01526b913fb50c4cb6e8"}, - {file = "lazy_object_proxy-1.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28c174db37946f94b97a97b579932ff88f07b8d73a46b6b93322b9ac06794a3b"}, - {file = "lazy_object_proxy-1.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:d662f0669e27704495ff1f647070eb8816931231c44e583f4d0701b7adf6272f"}, - {file = "lazy_object_proxy-1.11.0-py3-none-any.whl", hash = "sha256:a56a5093d433341ff7da0e89f9b486031ccd222ec8e52ec84d0ec1cdc819674b"}, - {file = "lazy_object_proxy-1.11.0.tar.gz", hash = "sha256:18874411864c9fbbbaa47f9fc1dd7aea754c86cfde21278ef427639d1dd78e9c"}, -] - -[[package]] -name = "lz4" -version = "4.4.5" -description = "LZ4 Bindings for Python" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "lz4-4.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d221fa421b389ab2345640a508db57da36947a437dfe31aeddb8d5c7b646c22d"}, - {file = "lz4-4.4.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7dc1e1e2dbd872f8fae529acd5e4839efd0b141eaa8ae7ce835a9fe80fbad89f"}, - {file = "lz4-4.4.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e928ec2d84dc8d13285b4a9288fd6246c5cde4f5f935b479f50d986911f085e3"}, - {file = "lz4-4.4.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daffa4807ef54b927451208f5f85750c545a4abbff03d740835fc444cd97f758"}, - {file = "lz4-4.4.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a2b7504d2dffed3fd19d4085fe1cc30cf221263fd01030819bdd8d2bb101cf1"}, - {file = "lz4-4.4.5-cp310-cp310-win32.whl", hash = "sha256:0846e6e78f374156ccf21c631de80967e03cc3c01c373c665789dc0c5431e7fc"}, - {file = "lz4-4.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:7c4e7c44b6a31de77d4dc9772b7d2561937c9588a734681f70ec547cfbc51ecd"}, - {file = "lz4-4.4.5-cp310-cp310-win_arm64.whl", hash = "sha256:15551280f5656d2206b9b43262799c89b25a25460416ec554075a8dc568e4397"}, - {file = "lz4-4.4.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d6da84a26b3aa5da13a62e4b89ab36a396e9327de8cd48b436a3467077f8ccd4"}, - {file = "lz4-4.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61d0ee03e6c616f4a8b69987d03d514e8896c8b1b7cc7598ad029e5c6aedfd43"}, - {file = "lz4-4.4.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:33dd86cea8375d8e5dd001e41f321d0a4b1eb7985f39be1b6a4f466cd480b8a7"}, - {file = "lz4-4.4.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:609a69c68e7cfcfa9d894dc06be13f2e00761485b62df4e2472f1b66f7b405fb"}, - {file = "lz4-4.4.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:75419bb1a559af00250b8f1360d508444e80ed4b26d9d40ec5b09fe7875cb989"}, - {file = "lz4-4.4.5-cp311-cp311-win32.whl", hash = "sha256:12233624f1bc2cebc414f9efb3113a03e89acce3ab6f72035577bc61b270d24d"}, - {file = "lz4-4.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:8a842ead8ca7c0ee2f396ca5d878c4c40439a527ebad2b996b0444f0074ed004"}, - {file = "lz4-4.4.5-cp311-cp311-win_arm64.whl", hash = "sha256:83bc23ef65b6ae44f3287c38cbf82c269e2e96a26e560aa551735883388dcc4b"}, - {file = "lz4-4.4.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:df5aa4cead2044bab83e0ebae56e0944cc7fcc1505c7787e9e1057d6d549897e"}, - {file = "lz4-4.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d0bf51e7745484d2092b3a51ae6eb58c3bd3ce0300cf2b2c14f76c536d5697a"}, - {file = "lz4-4.4.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7b62f94b523c251cf32aa4ab555f14d39bd1a9df385b72443fd76d7c7fb051f5"}, - {file = "lz4-4.4.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c3ea562c3af274264444819ae9b14dbbf1ab070aff214a05e97db6896c7597e"}, - {file = "lz4-4.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24092635f47538b392c4eaeff14c7270d2c8e806bf4be2a6446a378591c5e69e"}, - {file = "lz4-4.4.5-cp312-cp312-win32.whl", hash = "sha256:214e37cfe270948ea7eb777229e211c601a3e0875541c1035ab408fbceaddf50"}, - {file = "lz4-4.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:713a777de88a73425cf08eb11f742cd2c98628e79a8673d6a52e3c5f0c116f33"}, - {file = "lz4-4.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:a88cbb729cc333334ccfb52f070463c21560fca63afcf636a9f160a55fac3301"}, - {file = "lz4-4.4.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6bb05416444fafea170b07181bc70640975ecc2a8c92b3b658c554119519716c"}, - {file = "lz4-4.4.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b424df1076e40d4e884cfcc4c77d815368b7fb9ebcd7e634f937725cd9a8a72a"}, - {file = "lz4-4.4.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:216ca0c6c90719731c64f41cfbd6f27a736d7e50a10b70fad2a9c9b262ec923d"}, - {file = "lz4-4.4.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:533298d208b58b651662dd972f52d807d48915176e5b032fb4f8c3b6f5fe535c"}, - {file = "lz4-4.4.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:451039b609b9a88a934800b5fc6ee401c89ad9c175abf2f4d9f8b2e4ef1afc64"}, - {file = "lz4-4.4.5-cp313-cp313-win32.whl", hash = "sha256:a5f197ffa6fc0e93207b0af71b302e0a2f6f29982e5de0fbda61606dd3a55832"}, - {file = "lz4-4.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:da68497f78953017deb20edff0dba95641cc86e7423dfadf7c0264e1ac60dc22"}, - {file = "lz4-4.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:c1cfa663468a189dab510ab231aad030970593f997746d7a324d40104db0d0a9"}, - {file = "lz4-4.4.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67531da3b62f49c939e09d56492baf397175ff39926d0bd5bd2d191ac2bff95f"}, - {file = "lz4-4.4.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a1acbbba9edbcbb982bc2cac5e7108f0f553aebac1040fbec67a011a45afa1ba"}, - {file = "lz4-4.4.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a482eecc0b7829c89b498fda883dbd50e98153a116de612ee7c111c8bcf82d1d"}, - {file = "lz4-4.4.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e099ddfaa88f59dd8d36c8a3c66bd982b4984edf127eb18e30bb49bdba68ce67"}, - {file = "lz4-4.4.5-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2af2897333b421360fdcce895c6f6281dc3fab018d19d341cf64d043fc8d90d"}, - {file = "lz4-4.4.5-cp313-cp313t-win32.whl", hash = "sha256:66c5de72bf4988e1b284ebdd6524c4bead2c507a2d7f172201572bac6f593901"}, - {file = "lz4-4.4.5-cp313-cp313t-win_amd64.whl", hash = "sha256:cdd4bdcbaf35056086d910d219106f6a04e1ab0daa40ec0eeef1626c27d0fddb"}, - {file = "lz4-4.4.5-cp313-cp313t-win_arm64.whl", hash = "sha256:28ccaeb7c5222454cd5f60fcd152564205bcb801bd80e125949d2dfbadc76bbd"}, - {file = "lz4-4.4.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c216b6d5275fc060c6280936bb3bb0e0be6126afb08abccde27eed23dead135f"}, - {file = "lz4-4.4.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c8e71b14938082ebaf78144f3b3917ac715f72d14c076f384a4c062df96f9df6"}, - {file = "lz4-4.4.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b5e6abca8df9f9bdc5c3085f33ff32cdc86ed04c65e0355506d46a5ac19b6e9"}, - {file = "lz4-4.4.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b84a42da86e8ad8537aabef062e7f661f4a877d1c74d65606c49d835d36d668"}, - {file = "lz4-4.4.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bba042ec5a61fa77c7e380351a61cb768277801240249841defd2ff0a10742f"}, - {file = "lz4-4.4.5-cp314-cp314-win32.whl", hash = "sha256:bd85d118316b53ed73956435bee1997bd06cc66dd2fa74073e3b1322bd520a67"}, - {file = "lz4-4.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:92159782a4502858a21e0079d77cdcaade23e8a5d252ddf46b0652604300d7be"}, - {file = "lz4-4.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:d994b87abaa7a88ceb7a37c90f547b8284ff9da694e6afcfaa8568d739faf3f7"}, - {file = "lz4-4.4.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f6538aaaedd091d6e5abdaa19b99e6e82697d67518f114721b5248709b639fad"}, - {file = "lz4-4.4.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:13254bd78fef50105872989a2dc3418ff09aefc7d0765528adc21646a7288294"}, - {file = "lz4-4.4.5-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e64e61f29cf95afb43549063d8433b46352baf0c8a70aa45e2585618fcf59d86"}, - {file = "lz4-4.4.5-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff1b50aeeec64df5603f17984e4b5be6166058dcf8f1e26a3da40d7a0f6ab547"}, - {file = "lz4-4.4.5-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1dd4d91d25937c2441b9fc0f4af01704a2d09f30a38c5798bc1d1b5a15ec9581"}, - {file = "lz4-4.4.5-cp39-cp39-win32.whl", hash = "sha256:d64141085864918392c3159cdad15b102a620a67975c786777874e1e90ef15ce"}, - {file = "lz4-4.4.5-cp39-cp39-win_amd64.whl", hash = "sha256:f32b9e65d70f3684532358255dc053f143835c5f5991e28a5ac4c93ce94b9ea7"}, - {file = "lz4-4.4.5-cp39-cp39-win_arm64.whl", hash = "sha256:f9b8bde9909a010c75b3aea58ec3910393b758f3c219beed67063693df854db0"}, - {file = "lz4-4.4.5.tar.gz", hash = "sha256:5f0b9e53c1e82e88c10d7c180069363980136b9d7a8306c4dca4f760d60c39f0"}, -] - -[package.extras] -docs = ["sphinx (>=1.6.0)", "sphinx_bootstrap_theme"] -flake8 = ["flake8"] -tests = ["psutil", "pytest (!=3.3.0)", "pytest-cov"] - -[[package]] -name = "markdown" -version = "3.10.2" -description = "Python implementation of John Gruber's Markdown." -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36"}, - {file = "markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950"}, -] - -[package.extras] -docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python] (>=0.28.3)"] -testing = ["coverage", "pyyaml"] - -[[package]] -name = "markdown-it-py" -version = "3.0.0" -description = "Python port of markdown-it. Markdown parsing, done right!" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, - {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, -] - -[package.dependencies] -mdurl = ">=0.1,<1.0" - -[package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark"] -code-style = ["pre-commit (>=3.0,<4.0)"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] -linkify = ["linkify-it-py (>=1,<3)"] -plugins = ["mdit-py-plugins"] -profiling = ["gprof2dot"] -rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] - -[[package]] -name = "markupsafe" -version = "3.0.2" -description = "Safely add untrusted strings to HTML/XML markup." -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, - {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, -] - -[[package]] -name = "marshmallow" -version = "4.3.0" -description = "A lightweight library for converting complex datatypes to and from native Python datatypes." -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "marshmallow-4.3.0-py3-none-any.whl", hash = "sha256:46c4fe6984707e3cbd485dfebbf0a59874f58d695aad05c1668d15e8c6e13b46"}, - {file = "marshmallow-4.3.0.tar.gz", hash = "sha256:fb43c53b3fe240b8f6af37223d6ef1636f927ad9bea8ab323afad95dff090880"}, -] - -[package.dependencies] -backports-datetime-fromisoformat = {version = "*", markers = "python_version < \"3.11\""} -typing-extensions = {version = "*", markers = "python_version < \"3.11\""} - -[[package]] -name = "mccabe" -version = "0.7.0" -description = "McCabe checker, plugin for flake8" -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -description = "Markdown URL utilities" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, -] - -[[package]] -name = "microsoft-kiota-abstractions" -version = "1.9.2" -description = "Core abstractions for kiota generated libraries in Python" -optional = false -python-versions = "<4.0,>=3.9" -groups = ["main"] -files = [ - {file = "microsoft_kiota_abstractions-1.9.2-py3-none-any.whl", hash = "sha256:a8853d272a84da59d6a2fe11a76c28e9c55bdab268a345ba48e918cb6822b607"}, - {file = "microsoft_kiota_abstractions-1.9.2.tar.gz", hash = "sha256:29cdafe8d0672f23099556e0b120dca6231c752cca9393e1e0092fa9ca594572"}, -] - -[package.dependencies] -opentelemetry-api = ">=1.27.0" -opentelemetry-sdk = ">=1.27.0" -std-uritemplate = ">=2.0.0" - -[[package]] -name = "microsoft-kiota-authentication-azure" -version = "1.9.2" -description = "Core abstractions for kiota generated libraries in Python" -optional = false -python-versions = "<4.0,>=3.9" -groups = ["main"] -files = [ - {file = "microsoft_kiota_authentication_azure-1.9.2-py3-none-any.whl", hash = "sha256:56840f8b15df8aedfd143fb2deb7cc7fae4ac0bafb1a50546b7313a7b3ab4ca0"}, - {file = "microsoft_kiota_authentication_azure-1.9.2.tar.gz", hash = "sha256:171045f522a93d9340fbddc4cabb218f14f1d9d289e82e535b3d9291986c3d5a"}, -] - -[package.dependencies] -aiohttp = ">=3.8.0" -azure-core = ">=1.21.1" -microsoft-kiota-abstractions = ">=1.9.2,<1.10.0" -opentelemetry-api = ">=1.27.0" -opentelemetry-sdk = ">=1.27.0" - -[[package]] -name = "microsoft-kiota-http" -version = "1.9.2" -description = "Core abstractions for kiota generated libraries in Python" -optional = false -python-versions = "<4.0,>=3.9" -groups = ["main"] -files = [ - {file = "microsoft_kiota_http-1.9.2-py3-none-any.whl", hash = "sha256:3a2d930a70d0184d9f4848473f929ee892462cae1acfaf33b2d193f1828c76c2"}, - {file = "microsoft_kiota_http-1.9.2.tar.gz", hash = "sha256:2ba3d04a3d1d5d600736eebc1e33533d54d87799ac4fbb92c9cce4a97809af61"}, -] - -[package.dependencies] -httpx = {version = ">=0.25,<1.0.0", extras = ["http2"]} -microsoft-kiota-abstractions = ">=1.9.2,<1.10.0" -opentelemetry-api = ">=1.27.0" -opentelemetry-sdk = ">=1.27.0" - -[[package]] -name = "microsoft-kiota-serialization-form" -version = "1.9.2" -description = "Core abstractions for kiota generated libraries in Python" -optional = false -python-versions = "<4.0,>=3.9" -groups = ["main"] -files = [ - {file = "microsoft_kiota_serialization_form-1.9.2-py3-none-any.whl", hash = "sha256:7b997efb2c8750b1d4fbc00878ba2a3e6e1df3fcefc8815226c90fcc9c54f218"}, - {file = "microsoft_kiota_serialization_form-1.9.2.tar.gz", hash = "sha256:badfbe65d8ec3369bd58b01022d13ef590edf14babeef94188efe3f4ec24fe41"}, -] - -[package.dependencies] -microsoft-kiota-abstractions = ">=1.9.2,<1.10.0" - -[[package]] -name = "microsoft-kiota-serialization-json" -version = "1.9.2" -description = "Core abstractions for kiota generated libraries in Python" -optional = false -python-versions = "<4.0,>=3.9" -groups = ["main"] -files = [ - {file = "microsoft_kiota_serialization_json-1.9.2-py3-none-any.whl", hash = "sha256:8f4ecf485607fff3df5ce8fa9b9c957bc7f4bff1658b183703e180af753098e3"}, - {file = "microsoft_kiota_serialization_json-1.9.2.tar.gz", hash = "sha256:19f7beb69c67b2cb77ca96f77824ee78a693929e20237bb5476ea54f69118bf1"}, -] - -[package.dependencies] -microsoft-kiota-abstractions = ">=1.9.2,<1.10.0" - -[[package]] -name = "microsoft-kiota-serialization-multipart" -version = "1.9.2" -description = "Core abstractions for kiota generated libraries in Python" -optional = false -python-versions = "<4.0,>=3.9" -groups = ["main"] -files = [ - {file = "microsoft_kiota_serialization_multipart-1.9.2-py3-none-any.whl", hash = "sha256:641ad374046f1c7adff90d110bdc68d77418adb1e479a716f4ffea3647f0ead6"}, - {file = "microsoft_kiota_serialization_multipart-1.9.2.tar.gz", hash = "sha256:b1851409205668d83f5c7a35a8b6fca974b341985b4a92841e95aaec93b7ca0a"}, -] - -[package.dependencies] -microsoft-kiota-abstractions = ">=1.9.2,<1.10.0" - -[[package]] -name = "microsoft-kiota-serialization-text" -version = "1.9.2" -description = "Core abstractions for kiota generated libraries in Python" -optional = false -python-versions = "<4.0,>=3.9" -groups = ["main"] -files = [ - {file = "microsoft_kiota_serialization_text-1.9.2-py3-none-any.whl", hash = "sha256:6e63129ea29eb9b976f4ed56fc6595d204e29fc309958b639299e9f9f4e5edb4"}, - {file = "microsoft_kiota_serialization_text-1.9.2.tar.gz", hash = "sha256:4289508ebac0cefdc4fa21c545051769a9409913972355ccda9116b647f978f2"}, -] - -[package.dependencies] -microsoft-kiota-abstractions = ">=1.9.2,<1.10.0" - -[[package]] -name = "mock" -version = "5.2.0" -description = "Rolling backport of unittest.mock for all Pythons" -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "mock-5.2.0-py3-none-any.whl", hash = "sha256:7ba87f72ca0e915175596069dbbcc7c75af7b5e9b9bc107ad6349ede0819982f"}, - {file = "mock-5.2.0.tar.gz", hash = "sha256:4e460e818629b4b173f32d08bf30d3af8123afbb8e04bb5707a1fd4799e503f0"}, -] - -[package.extras] -build = ["blurb", "twine", "wheel"] -docs = ["sphinx"] -test = ["pytest", "pytest-cov"] - -[[package]] -name = "moto" -version = "5.1.11" -description = "A library that allows you to easily mock out tests based on AWS infrastructure" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "moto-5.1.11-py3-none-any.whl", hash = "sha256:d09429ed5f67f8568637700cd525997d6abe7f91439a6f900b4f98a9fe4ecac9"}, - {file = "moto-5.1.11.tar.gz", hash = "sha256:1330b6d9b91088e971469dfb67f297595541914b364e0b49047bb82622975ec7"}, -] - -[package.dependencies] -antlr4-python3-runtime = {version = "*", optional = true, markers = "extra == \"all\""} -aws-xray-sdk = {version = ">=0.93,<0.96 || >0.96", optional = true, markers = "extra == \"all\""} -boto3 = ">=1.9.201" -botocore = ">=1.20.88,<1.35.45 || >1.35.45,<1.35.46 || >1.35.46" -cfn-lint = {version = ">=0.40.0", optional = true, markers = "extra == \"all\""} -cryptography = ">=35.0.0" -docker = {version = ">=3.0.0", optional = true, markers = "extra == \"all\""} -graphql-core = {version = "*", optional = true, markers = "extra == \"all\""} -Jinja2 = ">=2.10.1" -joserfc = {version = ">=0.9.0", optional = true, markers = "extra == \"all\""} -jsonpath_ng = {version = "*", optional = true, markers = "extra == \"all\""} -jsonschema = {version = "*", optional = true, markers = "extra == \"all\""} -multipart = {version = "*", optional = true, markers = "extra == \"all\""} -openapi-spec-validator = {version = ">=0.5.0", optional = true, markers = "extra == \"all\""} -py-partiql-parser = {version = "0.6.1", optional = true, markers = "extra == \"all\""} -pyparsing = {version = ">=3.0.7", optional = true, markers = "extra == \"all\""} -python-dateutil = ">=2.1,<3.0.0" -PyYAML = {version = ">=5.1", optional = true, markers = "extra == \"all\""} -requests = ">=2.5" -responses = ">=0.15.0,<0.25.5 || >0.25.5" -setuptools = {version = "*", optional = true, markers = "extra == \"all\""} -werkzeug = ">=0.5,<2.2.0 || >2.2.0,<2.2.1 || >2.2.1" -xmltodict = "*" - -[package.extras] -all = ["PyYAML (>=5.1)", "antlr4-python3-runtime", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "graphql-core", "joserfc (>=0.9.0)", "jsonpath_ng", "jsonschema", "multipart", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.6.1)", "pyparsing (>=3.0.7)", "setuptools"] -apigateway = ["PyYAML (>=5.1)", "joserfc (>=0.9.0)", "openapi-spec-validator (>=0.5.0)"] -apigatewayv2 = ["PyYAML (>=5.1)", "openapi-spec-validator (>=0.5.0)"] -appsync = ["graphql-core"] -awslambda = ["docker (>=3.0.0)"] -batch = ["docker (>=3.0.0)"] -cloudformation = ["PyYAML (>=5.1)", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "graphql-core", "joserfc (>=0.9.0)", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.6.1)", "pyparsing (>=3.0.7)", "setuptools"] -cognitoidp = ["joserfc (>=0.9.0)"] -dynamodb = ["docker (>=3.0.0)", "py-partiql-parser (==0.6.1)"] -dynamodbstreams = ["docker (>=3.0.0)", "py-partiql-parser (==0.6.1)"] -events = ["jsonpath_ng"] -glue = ["pyparsing (>=3.0.7)"] -proxy = ["PyYAML (>=5.1)", "antlr4-python3-runtime", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=2.5.1)", "graphql-core", "joserfc (>=0.9.0)", "jsonpath_ng", "multipart", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.6.1)", "pyparsing (>=3.0.7)", "setuptools"] -quicksight = ["jsonschema"] -resourcegroupstaggingapi = ["PyYAML (>=5.1)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "graphql-core", "joserfc (>=0.9.0)", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.6.1)", "pyparsing (>=3.0.7)"] -s3 = ["PyYAML (>=5.1)", "py-partiql-parser (==0.6.1)"] -s3crc32c = ["PyYAML (>=5.1)", "crc32c", "py-partiql-parser (==0.6.1)"] -server = ["PyYAML (>=5.1)", "antlr4-python3-runtime", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "flask (!=2.2.0,!=2.2.1)", "flask-cors", "graphql-core", "joserfc (>=0.9.0)", "jsonpath_ng", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.6.1)", "pyparsing (>=3.0.7)", "setuptools"] -ssm = ["PyYAML (>=5.1)"] -stepfunctions = ["antlr4-python3-runtime", "jsonpath_ng"] -xray = ["aws-xray-sdk (>=0.93,!=0.96)", "setuptools"] - -[[package]] -name = "mpmath" -version = "1.3.0" -description = "Python library for arbitrary-precision floating-point arithmetic" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"}, - {file = "mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f"}, -] - -[package.extras] -develop = ["codecov", "pycodestyle", "pytest (>=4.6)", "pytest-cov", "wheel"] -docs = ["sphinx"] -gmpy = ["gmpy2 (>=2.1.0a4) ; platform_python_implementation != \"PyPy\""] -tests = ["pytest (>=4.6)"] - -[[package]] -name = "msal" -version = "1.33.0" -description = "The Microsoft Authentication Library (MSAL) for Python library enables your app to access the Microsoft Cloud by supporting authentication of users with Microsoft Azure Active Directory accounts (AAD) and Microsoft Accounts (MSA) using industry standard OAuth2 and OpenID Connect." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "msal-1.33.0-py3-none-any.whl", hash = "sha256:c0cd41cecf8eaed733ee7e3be9e040291eba53b0f262d3ae9c58f38b04244273"}, - {file = "msal-1.33.0.tar.gz", hash = "sha256:836ad80faa3e25a7d71015c990ce61f704a87328b1e73bcbb0623a18cbf17510"}, -] - -[package.dependencies] -cryptography = ">=2.5,<48" -PyJWT = {version = ">=1.0.0,<3", extras = ["crypto"]} -requests = ">=2.0.0,<3" - -[package.extras] -broker = ["pymsalruntime (>=0.14,<0.19) ; python_version >= \"3.6\" and platform_system == \"Windows\"", "pymsalruntime (>=0.17,<0.19) ; python_version >= \"3.8\" and platform_system == \"Darwin\"", "pymsalruntime (>=0.18,<0.19) ; python_version >= \"3.8\" and platform_system == \"Linux\""] - -[[package]] -name = "msal-extensions" -version = "1.3.1" -description = "Microsoft Authentication Library extensions (MSAL EX) provides a persistence API that can save your data on disk, encrypted on Windows, macOS and Linux. Concurrent data access will be coordinated by a file lock mechanism." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca"}, - {file = "msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4"}, -] - -[package.dependencies] -msal = ">=1.29,<2" - -[package.extras] -portalocker = ["portalocker (>=1.4,<4)"] - -[[package]] -name = "msgraph-core" -version = "1.3.5" -description = "Core component of the Microsoft Graph Python SDK" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "msgraph_core-1.3.5-py3-none-any.whl", hash = "sha256:bc496c6f99c626bc534012c6fe9afa35c37bcdce0f92acf26e4210f4ff9bb154"}, - {file = "msgraph_core-1.3.5.tar.gz", hash = "sha256:43aec9df1c011f1c6a1e14f2b5e9266c05a723ed750a5d3ea1eb0c0f1deb9975"}, -] - -[package.dependencies] -httpx = {version = ">=0.23.0", extras = ["http2"]} -microsoft-kiota-abstractions = ">=1.8.0,<2.0.0" -microsoft-kiota-authentication-azure = ">=1.8.0,<2.0.0" -microsoft-kiota-http = ">=1.8.0,<2.0.0" - -[package.extras] -dev = ["bumpver", "isort", "mypy", "pylint", "pytest", "yapf"] - -[[package]] -name = "msgraph-sdk" -version = "1.55.0" -description = "The Microsoft Graph Python SDK" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "msgraph_sdk-1.55.0-py3-none-any.whl", hash = "sha256:c8e68ebc4b88af5111de312e7fa910a4e76ddf48a4534feadb1fb8a411c48cfc"}, - {file = "msgraph_sdk-1.55.0.tar.gz", hash = "sha256:6df691a31954a050d26b8a678968017e157d940fb377f2a8a4e17a9741b98756"}, -] - -[package.dependencies] -azure-identity = ">=1.12.0" -microsoft-kiota-serialization-form = ">=1.8.0,<2.0.0" -microsoft-kiota-serialization-json = ">=1.8.0,<2.0.0" -microsoft-kiota-serialization-multipart = ">=1.8.0,<2.0.0" -microsoft-kiota-serialization-text = ">=1.8.0,<2.0.0" -msgraph_core = ">=1.3.1" - -[package.extras] -dev = ["bumpver", "isort", "mypy", "pylint", "pytest", "yapf"] - -[[package]] -name = "msrest" -version = "0.7.1" -description = "AutoRest swagger generator Python client runtime." -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "msrest-0.7.1-py3-none-any.whl", hash = "sha256:21120a810e1233e5e6cc7fe40b474eeb4ec6f757a15d7cf86702c369f9567c32"}, - {file = "msrest-0.7.1.zip", hash = "sha256:6e7661f46f3afd88b75667b7187a92829924446c7ea1d169be8c4bb7eeb788b9"}, -] - -[package.dependencies] -azure-core = ">=1.24.0" -certifi = ">=2017.4.17" -isodate = ">=0.6.0" -requests = ">=2.16,<3.0" -requests-oauthlib = ">=0.5.0" - -[package.extras] -async = ["aiodns ; python_version >= \"3.5\"", "aiohttp (>=3.0) ; python_version >= \"3.5\""] - -[[package]] -name = "multidict" -version = "6.6.3" -description = "multidict implementation" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "multidict-6.6.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a2be5b7b35271f7fff1397204ba6708365e3d773579fe2a30625e16c4b4ce817"}, - {file = "multidict-6.6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:12f4581d2930840295c461764b9a65732ec01250b46c6b2c510d7ee68872b140"}, - {file = "multidict-6.6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dd7793bab517e706c9ed9d7310b06c8672fd0aeee5781bfad612f56b8e0f7d14"}, - {file = "multidict-6.6.3-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:72d8815f2cd3cf3df0f83cac3f3ef801d908b2d90409ae28102e0553af85545a"}, - {file = "multidict-6.6.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:531e331a2ee53543ab32b16334e2deb26f4e6b9b28e41f8e0c87e99a6c8e2d69"}, - {file = "multidict-6.6.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:42ca5aa9329a63be8dc49040f63817d1ac980e02eeddba763a9ae5b4027b9c9c"}, - {file = "multidict-6.6.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:208b9b9757060b9faa6f11ab4bc52846e4f3c2fb8b14d5680c8aac80af3dc751"}, - {file = "multidict-6.6.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:acf6b97bd0884891af6a8b43d0f586ab2fcf8e717cbd47ab4bdddc09e20652d8"}, - {file = "multidict-6.6.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:68e9e12ed00e2089725669bdc88602b0b6f8d23c0c95e52b95f0bc69f7fe9b55"}, - {file = "multidict-6.6.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:05db2f66c9addb10cfa226e1acb363450fab2ff8a6df73c622fefe2f5af6d4e7"}, - {file = "multidict-6.6.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:0db58da8eafb514db832a1b44f8fa7906fdd102f7d982025f816a93ba45e3dcb"}, - {file = "multidict-6.6.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:14117a41c8fdb3ee19c743b1c027da0736fdb79584d61a766da53d399b71176c"}, - {file = "multidict-6.6.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:877443eaaabcd0b74ff32ebeed6f6176c71850feb7d6a1d2db65945256ea535c"}, - {file = "multidict-6.6.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:70b72e749a4f6e7ed8fb334fa8d8496384840319512746a5f42fa0aec79f4d61"}, - {file = "multidict-6.6.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:43571f785b86afd02b3855c5ac8e86ec921b760298d6f82ff2a61daf5a35330b"}, - {file = "multidict-6.6.3-cp310-cp310-win32.whl", hash = "sha256:20c5a0c3c13a15fd5ea86c42311859f970070e4e24de5a550e99d7c271d76318"}, - {file = "multidict-6.6.3-cp310-cp310-win_amd64.whl", hash = "sha256:ab0a34a007704c625e25a9116c6770b4d3617a071c8a7c30cd338dfbadfe6485"}, - {file = "multidict-6.6.3-cp310-cp310-win_arm64.whl", hash = "sha256:769841d70ca8bdd140a715746199fc6473414bd02efd678d75681d2d6a8986c5"}, - {file = "multidict-6.6.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:18f4eba0cbac3546b8ae31e0bbc55b02c801ae3cbaf80c247fcdd89b456ff58c"}, - {file = "multidict-6.6.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef43b5dd842382329e4797c46f10748d8c2b6e0614f46b4afe4aee9ac33159df"}, - {file = "multidict-6.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf9bd1fd5eec01494e0f2e8e446a74a85d5e49afb63d75a9934e4a5423dba21d"}, - {file = "multidict-6.6.3-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5bd8d6f793a787153956cd35e24f60485bf0651c238e207b9a54f7458b16d539"}, - {file = "multidict-6.6.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bf99b4daf908c73856bd87ee0a2499c3c9a3d19bb04b9c6025e66af3fd07462"}, - {file = "multidict-6.6.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b9e59946b49dafaf990fd9c17ceafa62976e8471a14952163d10a7a630413a9"}, - {file = "multidict-6.6.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e2db616467070d0533832d204c54eea6836a5e628f2cb1e6dfd8cd6ba7277cb7"}, - {file = "multidict-6.6.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7394888236621f61dcdd25189b2768ae5cc280f041029a5bcf1122ac63df79f9"}, - {file = "multidict-6.6.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f114d8478733ca7388e7c7e0ab34b72547476b97009d643644ac33d4d3fe1821"}, - {file = "multidict-6.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cdf22e4db76d323bcdc733514bf732e9fb349707c98d341d40ebcc6e9318ef3d"}, - {file = "multidict-6.6.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e995a34c3d44ab511bfc11aa26869b9d66c2d8c799fa0e74b28a473a692532d6"}, - {file = "multidict-6.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:766a4a5996f54361d8d5a9050140aa5362fe48ce51c755a50c0bc3706460c430"}, - {file = "multidict-6.6.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3893a0d7d28a7fe6ca7a1f760593bc13038d1d35daf52199d431b61d2660602b"}, - {file = "multidict-6.6.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:934796c81ea996e61914ba58064920d6cad5d99140ac3167901eb932150e2e56"}, - {file = "multidict-6.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9ed948328aec2072bc00f05d961ceadfd3e9bfc2966c1319aeaf7b7c21219183"}, - {file = "multidict-6.6.3-cp311-cp311-win32.whl", hash = "sha256:9f5b28c074c76afc3e4c610c488e3493976fe0e596dd3db6c8ddfbb0134dcac5"}, - {file = "multidict-6.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:bc7f6fbc61b1c16050a389c630da0b32fc6d4a3d191394ab78972bf5edc568c2"}, - {file = "multidict-6.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:d4e47d8faffaae822fb5cba20937c048d4f734f43572e7079298a6c39fb172cb"}, - {file = "multidict-6.6.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:056bebbeda16b2e38642d75e9e5310c484b7c24e3841dc0fb943206a72ec89d6"}, - {file = "multidict-6.6.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e5f481cccb3c5c5e5de5d00b5141dc589c1047e60d07e85bbd7dea3d4580d63f"}, - {file = "multidict-6.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:10bea2ee839a759ee368b5a6e47787f399b41e70cf0c20d90dfaf4158dfb4e55"}, - {file = "multidict-6.6.3-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:2334cfb0fa9549d6ce2c21af2bfbcd3ac4ec3646b1b1581c88e3e2b1779ec92b"}, - {file = "multidict-6.6.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8fee016722550a2276ca2cb5bb624480e0ed2bd49125b2b73b7010b9090e888"}, - {file = "multidict-6.6.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5511cb35f5c50a2db21047c875eb42f308c5583edf96bd8ebf7d770a9d68f6d"}, - {file = "multidict-6.6.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:712b348f7f449948e0a6c4564a21c7db965af900973a67db432d724619b3c680"}, - {file = "multidict-6.6.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e4e15d2138ee2694e038e33b7c3da70e6b0ad8868b9f8094a72e1414aeda9c1a"}, - {file = "multidict-6.6.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8df25594989aebff8a130f7899fa03cbfcc5d2b5f4a461cf2518236fe6f15961"}, - {file = "multidict-6.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:159ca68bfd284a8860f8d8112cf0521113bffd9c17568579e4d13d1f1dc76b65"}, - {file = "multidict-6.6.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e098c17856a8c9ade81b4810888c5ad1914099657226283cab3062c0540b0643"}, - {file = "multidict-6.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:67c92ed673049dec52d7ed39f8cf9ebbadf5032c774058b4406d18c8f8fe7063"}, - {file = "multidict-6.6.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:bd0578596e3a835ef451784053cfd327d607fc39ea1a14812139339a18a0dbc3"}, - {file = "multidict-6.6.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:346055630a2df2115cd23ae271910b4cae40f4e336773550dca4889b12916e75"}, - {file = "multidict-6.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:555ff55a359302b79de97e0468e9ee80637b0de1fce77721639f7cd9440b3a10"}, - {file = "multidict-6.6.3-cp312-cp312-win32.whl", hash = "sha256:73ab034fb8d58ff85c2bcbadc470efc3fafeea8affcf8722855fb94557f14cc5"}, - {file = "multidict-6.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:04cbcce84f63b9af41bad04a54d4cc4e60e90c35b9e6ccb130be2d75b71f8c17"}, - {file = "multidict-6.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:0f1130b896ecb52d2a1e615260f3ea2af55fa7dc3d7c3003ba0c3121a759b18b"}, - {file = "multidict-6.6.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:540d3c06d48507357a7d57721e5094b4f7093399a0106c211f33540fdc374d55"}, - {file = "multidict-6.6.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9c19cea2a690f04247d43f366d03e4eb110a0dc4cd1bbeee4d445435428ed35b"}, - {file = "multidict-6.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7af039820cfd00effec86bda5d8debef711a3e86a1d3772e85bea0f243a4bd65"}, - {file = "multidict-6.6.3-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:500b84f51654fdc3944e936f2922114349bf8fdcac77c3092b03449f0e5bc2b3"}, - {file = "multidict-6.6.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3fc723ab8a5c5ed6c50418e9bfcd8e6dceba6c271cee6728a10a4ed8561520c"}, - {file = "multidict-6.6.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:94c47ea3ade005b5976789baaed66d4de4480d0a0bf31cef6edaa41c1e7b56a6"}, - {file = "multidict-6.6.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dbc7cf464cc6d67e83e136c9f55726da3a30176f020a36ead246eceed87f1cd8"}, - {file = "multidict-6.6.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:900eb9f9da25ada070f8ee4a23f884e0ee66fe4e1a38c3af644256a508ad81ca"}, - {file = "multidict-6.6.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c6df517cf177da5d47ab15407143a89cd1a23f8b335f3a28d57e8b0a3dbb884"}, - {file = "multidict-6.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ef421045f13879e21c994b36e728d8e7d126c91a64b9185810ab51d474f27e7"}, - {file = "multidict-6.6.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6c1e61bb4f80895c081790b6b09fa49e13566df8fbff817da3f85b3a8192e36b"}, - {file = "multidict-6.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e5e8523bb12d7623cd8300dbd91b9e439a46a028cd078ca695eb66ba31adee3c"}, - {file = "multidict-6.6.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ef58340cc896219e4e653dade08fea5c55c6df41bcc68122e3be3e9d873d9a7b"}, - {file = "multidict-6.6.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc9dc435ec8699e7b602b94fe0cd4703e69273a01cbc34409af29e7820f777f1"}, - {file = "multidict-6.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9e864486ef4ab07db5e9cb997bad2b681514158d6954dd1958dfb163b83d53e6"}, - {file = "multidict-6.6.3-cp313-cp313-win32.whl", hash = "sha256:5633a82fba8e841bc5c5c06b16e21529573cd654f67fd833650a215520a6210e"}, - {file = "multidict-6.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:e93089c1570a4ad54c3714a12c2cef549dc9d58e97bcded193d928649cab78e9"}, - {file = "multidict-6.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:c60b401f192e79caec61f166da9c924e9f8bc65548d4246842df91651e83d600"}, - {file = "multidict-6.6.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:02fd8f32d403a6ff13864b0851f1f523d4c988051eea0471d4f1fd8010f11134"}, - {file = "multidict-6.6.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f3aa090106b1543f3f87b2041eef3c156c8da2aed90c63a2fbed62d875c49c37"}, - {file = "multidict-6.6.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e924fb978615a5e33ff644cc42e6aa241effcf4f3322c09d4f8cebde95aff5f8"}, - {file = "multidict-6.6.3-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:b9fe5a0e57c6dbd0e2ce81ca66272282c32cd11d31658ee9553849d91289e1c1"}, - {file = "multidict-6.6.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b24576f208793ebae00280c59927c3b7c2a3b1655e443a25f753c4611bc1c373"}, - {file = "multidict-6.6.3-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:135631cb6c58eac37d7ac0df380294fecdc026b28837fa07c02e459c7fb9c54e"}, - {file = "multidict-6.6.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:274d416b0df887aef98f19f21578653982cfb8a05b4e187d4a17103322eeaf8f"}, - {file = "multidict-6.6.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e252017a817fad7ce05cafbe5711ed40faeb580e63b16755a3a24e66fa1d87c0"}, - {file = "multidict-6.6.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e4cc8d848cd4fe1cdee28c13ea79ab0ed37fc2e89dd77bac86a2e7959a8c3bc"}, - {file = "multidict-6.6.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9e236a7094b9c4c1b7585f6b9cca34b9d833cf079f7e4c49e6a4a6ec9bfdc68f"}, - {file = "multidict-6.6.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:e0cb0ab69915c55627c933f0b555a943d98ba71b4d1c57bc0d0a66e2567c7471"}, - {file = "multidict-6.6.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:81ef2f64593aba09c5212a3d0f8c906a0d38d710a011f2f42759704d4557d3f2"}, - {file = "multidict-6.6.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:b9cbc60010de3562545fa198bfc6d3825df430ea96d2cc509c39bd71e2e7d648"}, - {file = "multidict-6.6.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:70d974eaaa37211390cd02ef93b7e938de564bbffa866f0b08d07e5e65da783d"}, - {file = "multidict-6.6.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3713303e4a6663c6d01d648a68f2848701001f3390a030edaaf3fc949c90bf7c"}, - {file = "multidict-6.6.3-cp313-cp313t-win32.whl", hash = "sha256:639ecc9fe7cd73f2495f62c213e964843826f44505a3e5d82805aa85cac6f89e"}, - {file = "multidict-6.6.3-cp313-cp313t-win_amd64.whl", hash = "sha256:9f97e181f344a0ef3881b573d31de8542cc0dbc559ec68c8f8b5ce2c2e91646d"}, - {file = "multidict-6.6.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ce8b7693da41a3c4fde5871c738a81490cea5496c671d74374c8ab889e1834fb"}, - {file = "multidict-6.6.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c8161b5a7778d3137ea2ee7ae8a08cce0010de3b00ac671c5ebddeaa17cefd22"}, - {file = "multidict-6.6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1328201ee930f069961ae707d59c6627ac92e351ed5b92397cf534d1336ce557"}, - {file = "multidict-6.6.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b1db4d2093d6b235de76932febf9d50766cf49a5692277b2c28a501c9637f616"}, - {file = "multidict-6.6.3-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53becb01dd8ebd19d1724bebe369cfa87e4e7f29abbbe5c14c98ce4c383e16cd"}, - {file = "multidict-6.6.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41bb9d1d4c303886e2d85bade86e59885112a7f4277af5ad47ab919a2251f306"}, - {file = "multidict-6.6.3-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:775b464d31dac90f23192af9c291dc9f423101857e33e9ebf0020a10bfcf4144"}, - {file = "multidict-6.6.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d04d01f0a913202205a598246cf77826fe3baa5a63e9f6ccf1ab0601cf56eca0"}, - {file = "multidict-6.6.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d25594d3b38a2e6cabfdcafef339f754ca6e81fbbdb6650ad773ea9775af35ab"}, - {file = "multidict-6.6.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:35712f1748d409e0707b165bf49f9f17f9e28ae85470c41615778f8d4f7d9609"}, - {file = "multidict-6.6.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1c8082e5814b662de8589d6a06c17e77940d5539080cbab9fe6794b5241b76d9"}, - {file = "multidict-6.6.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:61af8a4b771f1d4d000b3168c12c3120ccf7284502a94aa58c68a81f5afac090"}, - {file = "multidict-6.6.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:448e4a9afccbf297577f2eaa586f07067441e7b63c8362a3540ba5a38dc0f14a"}, - {file = "multidict-6.6.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:233ad16999afc2bbd3e534ad8dbe685ef8ee49a37dbc2cdc9514e57b6d589ced"}, - {file = "multidict-6.6.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:bb933c891cd4da6bdcc9733d048e994e22e1883287ff7540c2a0f3b117605092"}, - {file = "multidict-6.6.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:37b09ca60998e87734699e88c2363abfd457ed18cfbf88e4009a4e83788e63ed"}, - {file = "multidict-6.6.3-cp39-cp39-win32.whl", hash = "sha256:f54cb79d26d0cd420637d184af38f0668558f3c4bbe22ab7ad830e67249f2e0b"}, - {file = "multidict-6.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:295adc9c0551e5d5214b45cf29ca23dbc28c2d197a9c30d51aed9e037cb7c578"}, - {file = "multidict-6.6.3-cp39-cp39-win_arm64.whl", hash = "sha256:15332783596f227db50fb261c2c251a58ac3873c457f3a550a95d5c0aa3c770d"}, - {file = "multidict-6.6.3-py3-none-any.whl", hash = "sha256:8db10f29c7541fc5da4defd8cd697e1ca429db743fa716325f236079b96f775a"}, - {file = "multidict-6.6.3.tar.gz", hash = "sha256:798a9eb12dab0a6c2e29c1de6f3468af5cb2da6053a20dfa3344907eed0937cc"}, -] - -[package.dependencies] -typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} - -[[package]] -name = "multipart" -version = "1.3.0" -description = "Parser for multipart/form-data" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "multipart-1.3.0-py3-none-any.whl", hash = "sha256:439bf4b00fd7cb2dbff08ae13f49f4f49798931ecd8d496372c63537fa19f304"}, - {file = "multipart-1.3.0.tar.gz", hash = "sha256:a46bd6b0eb4c1ba865beb88ddd886012a3da709b6e7b86084fc37e99087e5cf1"}, -] - -[package.extras] -dev = ["build", "pytest", "pytest-cov", "tox", "tox-uv", "twine"] -docs = ["sphinx (>=8,<9)", "sphinx-autobuild"] - -[[package]] -name = "mypy-extensions" -version = "1.1.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, - {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, -] - -[[package]] -name = "narwhals" -version = "2.0.0" -description = "Extremely lightweight compatibility layer between dataframe libraries" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "narwhals-2.0.0-py3-none-any.whl", hash = "sha256:9c9fe8a969b090d783edbcb3b58e1d0d15f5100fdf85b53f5e76d38f4ce7f19a"}, - {file = "narwhals-2.0.0.tar.gz", hash = "sha256:d967bea54dfb6cd787abf3865ab4d72b8259d8f798c1c12c4eb693d5e9cebb24"}, -] - -[package.extras] -cudf = ["cudf (>=24.10.0)"] -dask = ["dask[dataframe] (>=2024.8)"] -duckdb = ["duckdb (>=1.0)"] -ibis = ["ibis-framework (>=6.0.0)", "packaging", "pyarrow-hotfix", "rich"] -modin = ["modin"] -pandas = ["pandas (>=1.1.3)"] -polars = ["polars (>=0.20.4)"] -pyarrow = ["pyarrow (>=13.0.0)"] -pyspark = ["pyspark (>=3.5.0)"] -pyspark-connect = ["pyspark[connect] (>=3.5.0)"] -sqlframe = ["sqlframe (>=3.22.0)"] - -[[package]] -name = "nest-asyncio" -version = "1.6.0" -description = "Patch asyncio to allow nested event loops" -optional = false -python-versions = ">=3.5" -groups = ["main"] -files = [ - {file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"}, - {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, -] - -[[package]] -name = "networkx" -version = "3.4.2" -description = "Python package for creating and manipulating graphs and networks" -optional = false -python-versions = ">=3.10" -groups = ["dev"] -markers = "python_version == \"3.10\"" -files = [ - {file = "networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f"}, - {file = "networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1"}, -] - -[package.extras] -default = ["matplotlib (>=3.7)", "numpy (>=1.24)", "pandas (>=2.0)", "scipy (>=1.10,!=1.11.0,!=1.11.1)"] -developer = ["changelist (==0.5)", "mypy (>=1.1)", "pre-commit (>=3.2)", "rtoml"] -doc = ["intersphinx-registry", "myst-nb (>=1.1)", "numpydoc (>=1.8.0)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.15)", "sphinx (>=7.3)", "sphinx-gallery (>=0.16)", "texext (>=0.6.7)"] -example = ["cairocffi (>=1.7)", "contextily (>=1.6)", "igraph (>=0.11)", "momepy (>=0.7.2)", "osmnx (>=1.9)", "scikit-learn (>=1.5)", "seaborn (>=0.13)"] -extra = ["lxml (>=4.6)", "pydot (>=3.0.1)", "pygraphviz (>=1.14)", "sympy (>=1.10)"] -test = ["pytest (>=7.2)", "pytest-cov (>=4.0)"] - -[[package]] -name = "networkx" -version = "3.5" -description = "Python package for creating and manipulating graphs and networks" -optional = false -python-versions = ">=3.11" -groups = ["dev"] -markers = "python_version >= \"3.11\"" -files = [ - {file = "networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec"}, - {file = "networkx-3.5.tar.gz", hash = "sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037"}, -] - -[package.extras] -default = ["matplotlib (>=3.8)", "numpy (>=1.25)", "pandas (>=2.0)", "scipy (>=1.11.2)"] -developer = ["mypy (>=1.15)", "pre-commit (>=4.1)"] -doc = ["intersphinx-registry", "myst-nb (>=1.1)", "numpydoc (>=1.8.0)", "pillow (>=10)", "pydata-sphinx-theme (>=0.16)", "sphinx (>=8.0)", "sphinx-gallery (>=0.18)", "texext (>=0.6.7)"] -example = ["cairocffi (>=1.7)", "contextily (>=1.6)", "igraph (>=0.11)", "momepy (>=0.7.2)", "osmnx (>=2.0.0)", "scikit-learn (>=1.5)", "seaborn (>=0.13)"] -extra = ["lxml (>=4.6)", "pydot (>=3.0.1)", "pygraphviz (>=1.14)", "sympy (>=1.10)"] -test = ["pytest (>=7.2)", "pytest-cov (>=4.0)", "pytest-xdist (>=3.0)"] -test-extras = ["pytest-mpl", "pytest-randomly"] - -[[package]] -name = "nltk" -version = "3.9.4" -description = "Natural Language Toolkit" -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "nltk-3.9.4-py3-none-any.whl", hash = "sha256:f2fa301c3a12718ce4a0e9305c5675299da5ad9e26068218b69d692fda84828f"}, - {file = "nltk-3.9.4.tar.gz", hash = "sha256:ed03bc098a40481310320808b2db712d95d13ca65b27372f8a403949c8b523d0"}, -] - -[package.dependencies] -click = "*" -joblib = "*" -regex = ">=2021.8.3" -tqdm = "*" - -[package.extras] -all = ["matplotlib", "numpy", "pyparsing", "python-crfsuite", "requests", "scikit-learn", "scipy", "twython"] -corenlp = ["requests"] -machine-learning = ["numpy", "python-crfsuite", "scikit-learn", "scipy"] -plot = ["matplotlib"] -tgrep = ["pyparsing"] -twitter = ["twython"] - -[[package]] -name = "numpy" -version = "2.0.2" -description = "Fundamental package for array computing in Python" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece"}, - {file = "numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04"}, - {file = "numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66"}, - {file = "numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b"}, - {file = "numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd"}, - {file = "numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318"}, - {file = "numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8"}, - {file = "numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326"}, - {file = "numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97"}, - {file = "numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a"}, - {file = "numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669"}, - {file = "numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951"}, - {file = "numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9"}, - {file = "numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15"}, - {file = "numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4"}, - {file = "numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c"}, - {file = "numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692"}, - {file = "numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a"}, - {file = "numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c"}, - {file = "numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded"}, - {file = "numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5"}, - {file = "numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729"}, - {file = "numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1"}, - {file = "numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd"}, - {file = "numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d"}, - {file = "numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d"}, - {file = "numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa"}, - {file = "numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385"}, - {file = "numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78"}, -] - -[[package]] -name = "oauthlib" -version = "3.3.1" -description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1"}, - {file = "oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9"}, -] - -[package.extras] -rsa = ["cryptography (>=3.0.0)"] -signals = ["blinker (>=1.4.0)"] -signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] - -[[package]] -name = "oci" -version = "2.169.0" -description = "Oracle Cloud Infrastructure Python SDK" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "oci-2.169.0-py3-none-any.whl", hash = "sha256:c71bb5143f307791082b3e33cc1545c2490a518cfed85ab1948ef5107c36d30b"}, - {file = "oci-2.169.0.tar.gz", hash = "sha256:f3c5fff00b01783b5325ea7b13bf140053ec1e9f41da20bfb9c8a349ee7662fa"}, -] - -[package.dependencies] -certifi = "*" -circuitbreaker = {version = ">=1.3.1,<3.0.0", markers = "python_version >= \"3.7\""} -cryptography = ">=3.2.1,<47.0.0" -pyOpenSSL = ">=17.5.0,<27.0.0" -python-dateutil = ">=2.5.3,<3.0.0" -pytz = ">=2016.10" -urllib3 = {version = ">=2.6.3", markers = "python_version >= \"3.10.0\""} - -[package.extras] -adk = ["docstring-parser (>=0.16) ; python_version >= \"3.10\" and python_version < \"4\"", "mcp (>=1.6.0) ; python_version >= \"3.10\" and python_version < \"4\"", "pydantic (>=2.10.6) ; python_version >= \"3.10\" and python_version < \"4\"", "rich (>=13.9.4) ; python_version >= \"3.10\" and python_version < \"4\""] - -[[package]] -name = "openapi-schema-validator" -version = "0.6.3" -description = "OpenAPI schema validation for Python" -optional = false -python-versions = "<4.0.0,>=3.8.0" -groups = ["dev"] -files = [ - {file = "openapi_schema_validator-0.6.3-py3-none-any.whl", hash = "sha256:f3b9870f4e556b5a62a1c39da72a6b4b16f3ad9c73dc80084b1b11e74ba148a3"}, - {file = "openapi_schema_validator-0.6.3.tar.gz", hash = "sha256:f37bace4fc2a5d96692f4f8b31dc0f8d7400fd04f3a937798eaf880d425de6ee"}, -] - -[package.dependencies] -jsonschema = ">=4.19.1,<5.0.0" -jsonschema-specifications = ">=2023.5.2" -rfc3339-validator = "*" - -[[package]] -name = "openapi-spec-validator" -version = "0.7.1" -description = "OpenAPI 2.0 (aka Swagger) and OpenAPI 3 spec validator" -optional = false -python-versions = ">=3.8.0,<4.0.0" -groups = ["dev"] -files = [ - {file = "openapi_spec_validator-0.7.1-py3-none-any.whl", hash = "sha256:3c81825043f24ccbcd2f4b149b11e8231abce5ba84f37065e14ec947d8f4e959"}, - {file = "openapi_spec_validator-0.7.1.tar.gz", hash = "sha256:8577b85a8268685da6f8aa30990b83b7960d4d1117e901d451b5d572605e5ec7"}, -] - -[package.dependencies] -jsonschema = ">=4.18.0,<5.0.0" -jsonschema-path = ">=0.3.1,<0.4.0" -lazy-object-proxy = ">=1.7.1,<2.0.0" -openapi-schema-validator = ">=0.6.0,<0.7.0" - -[[package]] -name = "openstacksdk" -version = "4.2.0" -description = "An SDK for building applications to work with OpenStack" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "openstacksdk-4.2.0-py3-none-any.whl", hash = "sha256:238be0fa5d9899872b00787ab38e84f92fd6dc87525fde0965dadcdc12196dc6"}, - {file = "openstacksdk-4.2.0.tar.gz", hash = "sha256:5cb9450dcce8054a2caf89d8be9e55057ddfa219a954e781032241eb29280445"}, -] - -[package.dependencies] -cryptography = ">=2.7" -decorator = ">=4.4.1" -"dogpile.cache" = ">=0.6.5" -iso8601 = ">=0.1.11" -jmespath = ">=0.9.0" -jsonpatch = ">=1.16,<1.20 || >1.20" -keystoneauth1 = ">=3.18.0" -os-service-types = ">=1.7.0" -pbr = ">=2.0.0,<2.1.0 || >2.1.0" -platformdirs = ">=3" -psutil = ">=3.2.2" -PyYAML = ">=3.13" -requestsexceptions = ">=1.2.0" - -[[package]] -name = "opentelemetry-api" -version = "1.35.0" -description = "OpenTelemetry Python API" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "opentelemetry_api-1.35.0-py3-none-any.whl", hash = "sha256:c4ea7e258a244858daf18474625e9cc0149b8ee354f37843415771a40c25ee06"}, - {file = "opentelemetry_api-1.35.0.tar.gz", hash = "sha256:a111b959bcfa5b4d7dffc2fbd6a241aa72dd78dd8e79b5b1662bda896c5d2ffe"}, -] - -[package.dependencies] -importlib-metadata = ">=6.0,<8.8.0" -typing-extensions = ">=4.5.0" - -[[package]] -name = "opentelemetry-sdk" -version = "1.35.0" -description = "OpenTelemetry Python SDK" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "opentelemetry_sdk-1.35.0-py3-none-any.whl", hash = "sha256:223d9e5f5678518f4842311bb73966e0b6db5d1e0b74e35074c052cd2487f800"}, - {file = "opentelemetry_sdk-1.35.0.tar.gz", hash = "sha256:2a400b415ab68aaa6f04e8a6a9f6552908fb3090ae2ff78d6ae0c597ac581954"}, -] - -[package.dependencies] -opentelemetry-api = "1.35.0" -opentelemetry-semantic-conventions = "0.56b0" -typing-extensions = ">=4.5.0" - -[[package]] -name = "opentelemetry-semantic-conventions" -version = "0.56b0" -description = "OpenTelemetry Semantic Conventions" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "opentelemetry_semantic_conventions-0.56b0-py3-none-any.whl", hash = "sha256:df44492868fd6b482511cc43a942e7194be64e94945f572db24df2e279a001a2"}, - {file = "opentelemetry_semantic_conventions-0.56b0.tar.gz", hash = "sha256:c114c2eacc8ff6d3908cb328c811eaf64e6d68623840be9224dc829c4fd6c2ea"}, -] - -[package.dependencies] -opentelemetry-api = "1.35.0" -typing-extensions = ">=4.5.0" - -[[package]] -name = "os-service-types" -version = "1.8.2" -description = "Python library for consuming OpenStack sevice-types-authority data" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "os_service_types-1.8.2-py3-none-any.whl", hash = "sha256:f78890d71814deffabf0ed4358288ec2ced579bc4d0bb87a79ae806cbb4deb6e"}, - {file = "os_service_types-1.8.2.tar.gz", hash = "sha256:ab7648d7232849943196e1bb00a30e2e25e600fa3b57bb241d15b7f521b5b575"}, -] - -[package.dependencies] -pbr = ">=2.0.0,<2.1.0 || >2.1.0" -typing-extensions = ">=4.1.0" - -[[package]] -name = "packaging" -version = "25.0" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, - {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, -] - -[[package]] -name = "pandas" -version = "2.2.3" -description = "Powerful data structures for data analysis, time series, and statistics" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5"}, - {file = "pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348"}, - {file = "pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed"}, - {file = "pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57"}, - {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42"}, - {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f"}, - {file = "pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645"}, - {file = "pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039"}, - {file = "pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd"}, - {file = "pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698"}, - {file = "pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc"}, - {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3"}, - {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32"}, - {file = "pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5"}, - {file = "pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9"}, - {file = "pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4"}, - {file = "pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3"}, - {file = "pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319"}, - {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8"}, - {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a"}, - {file = "pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13"}, - {file = "pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015"}, - {file = "pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28"}, - {file = "pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0"}, - {file = "pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24"}, - {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659"}, - {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb"}, - {file = "pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d"}, - {file = "pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468"}, - {file = "pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18"}, - {file = "pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2"}, - {file = "pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4"}, - {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d"}, - {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a"}, - {file = "pandas-2.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc6b93f9b966093cb0fd62ff1a7e4c09e6d546ad7c1de191767baffc57628f39"}, - {file = "pandas-2.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5dbca4c1acd72e8eeef4753eeca07de9b1db4f398669d5994086f788a5d7cc30"}, - {file = "pandas-2.2.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8cd6d7cc958a3910f934ea8dbdf17b2364827bb4dafc38ce6eef6bb3d65ff09c"}, - {file = "pandas-2.2.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99df71520d25fade9db7c1076ac94eb994f4d2673ef2aa2e86ee039b6746d20c"}, - {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31d0ced62d4ea3e231a9f228366919a5ea0b07440d9d4dac345376fd8e1477ea"}, - {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7eee9e7cea6adf3e3d24e304ac6b8300646e2a5d1cd3a3c2abed9101b0846761"}, - {file = "pandas-2.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:4850ba03528b6dd51d6c5d273c46f183f39a9baf3f0143e566b89450965b105e"}, - {file = "pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667"}, -] - -[package.dependencies] -numpy = [ - {version = ">=1.22.4", markers = "python_version < \"3.11\""}, - {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, - {version = ">=1.23.2", markers = "python_version == \"3.11\""}, -] -python-dateutil = ">=2.8.2" -pytz = ">=2020.1" -tzdata = ">=2022.7" - -[package.extras] -all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"] -aws = ["s3fs (>=2022.11.0)"] -clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"] -compression = ["zstandard (>=0.19.0)"] -computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"] -consortium-standard = ["dataframe-api-compat (>=0.1.7)"] -excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"] -feather = ["pyarrow (>=10.0.1)"] -fss = ["fsspec (>=2022.11.0)"] -gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"] -hdf5 = ["tables (>=3.8.0)"] -html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"] -mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"] -output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"] -parquet = ["pyarrow (>=10.0.1)"] -performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"] -plot = ["matplotlib (>=3.6.3)"] -postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"] -pyarrow = ["pyarrow (>=10.0.1)"] -spss = ["pyreadstat (>=1.2.0)"] -sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"] -test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] -xml = ["lxml (>=4.9.2)"] - -[[package]] -name = "pathable" -version = "0.4.4" -description = "Object-oriented paths" -optional = false -python-versions = "<4.0.0,>=3.7.0" -groups = ["dev"] -files = [ - {file = "pathable-0.4.4-py3-none-any.whl", hash = "sha256:5ae9e94793b6ef5a4cbe0a7ce9dbbefc1eec38df253763fd0aeeacf2762dbbc2"}, - {file = "pathable-0.4.4.tar.gz", hash = "sha256:6905a3cd17804edfac7875b5f6c9142a218c7caef78693c2dbbbfbac186d88b2"}, -] - -[[package]] -name = "pathspec" -version = "0.12.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - -[[package]] -name = "pbr" -version = "6.1.1" -description = "Python Build Reasonableness" -optional = false -python-versions = ">=2.6" -groups = ["main", "dev"] -files = [ - {file = "pbr-6.1.1-py2.py3-none-any.whl", hash = "sha256:38d4daea5d9fa63b3f626131b9d34947fd0c8be9b05a29276870580050a25a76"}, - {file = "pbr-6.1.1.tar.gz", hash = "sha256:93ea72ce6989eb2eed99d0f75721474f69ad88128afdef5ac377eb797c4bf76b"}, -] - -[package.dependencies] -setuptools = "*" - -[[package]] -name = "platformdirs" -version = "4.3.8" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, - {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, -] - -[package.extras] -docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] -type = ["mypy (>=1.14.1)"] - -[[package]] -name = "plotly" -version = "6.2.0" -description = "An open-source interactive data visualization library for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "plotly-6.2.0-py3-none-any.whl", hash = "sha256:32c444d4c940887219cb80738317040363deefdfee4f354498cc0b6dab8978bd"}, - {file = "plotly-6.2.0.tar.gz", hash = "sha256:9dfa23c328000f16c928beb68927444c1ab9eae837d1fe648dbcda5360c7953d"}, -] - -[package.dependencies] -narwhals = ">=1.15.1" -packaging = "*" - -[package.extras] -dev = ["plotly[dev-optional]"] -dev-build = ["build", "jupyter", "plotly[dev-core]"] -dev-core = ["pytest", "requests", "ruff (==0.11.12)"] -dev-optional = ["anywidget", "colorcet", "fiona (<=1.9.6) ; python_version <= \"3.8\"", "geopandas", "inflect", "numpy", "orjson", "pandas", "pdfrw", "pillow", "plotly-geo", "plotly[dev-build]", "plotly[kaleido]", "polars[timezone]", "pyarrow", "pyshp", "pytz", "scikit-image", "scipy", "shapely", "statsmodels", "vaex ; python_version <= \"3.9\"", "xarray"] -express = ["numpy"] -kaleido = ["kaleido (>=1.0.0)"] - -[[package]] -name = "pluggy" -version = "1.6.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, - {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["coverage", "pytest", "pytest-benchmark"] - -[[package]] -name = "ply" -version = "3.11" -description = "Python Lex & Yacc" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce"}, - {file = "ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3"}, -] - -[[package]] -name = "prek" -version = "0.3.9" -description = "A Git hook manager written in Rust, designed as a drop-in alternative to pre-commit." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "prek-0.3.9-py3-none-linux_armv6l.whl", hash = "sha256:3ed793d51bfaa27bddb64d525d7acb77a7c8644f549412d82252e3eb0b88aad8"}, - {file = "prek-0.3.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:399c58400c0bd0b82a93a3c09dc1bfd88d8d0cfb242d414d2ed247187b06ead1"}, - {file = "prek-0.3.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e2ea1ffb124e92f081b8e2ca5b5a623a733efb3be0c5b1f4b7ffe2ee17d1f20c"}, - {file = "prek-0.3.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:aaf639f95b7301639298311d8d44aad0d0b4864e9736083ad3c71ce9765d37ab"}, - {file = "prek-0.3.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff104863b187fa443ea8451ca55d51e2c6e94f99f00d88784b5c3c4c623f1ebe"}, - {file = "prek-0.3.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:039ecaf87c63a3e67cca645ebd5bc5eb6aafa6c9d929e9a27b2921e7849d7ef9"}, - {file = "prek-0.3.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3bde2a3d045705095983c7f78ba04f72a7565fe1c2b4e85f5628502a254754ff"}, - {file = "prek-0.3.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28a0960a21543563e2c8e19aaad176cc8423a87aac3c914d0f313030d7a9244a"}, - {file = "prek-0.3.9-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:0dfb5d5171d7523271909246ee306b4dc3d5b63752e7dd7c7e8a8908fc9490d1"}, - {file = "prek-0.3.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:82b791bd36c1430c84d3ae7220a85152babc7eaf00f70adcb961bd594e756ba3"}, - {file = "prek-0.3.9-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:6eac6d2f736b041118f053a1487abed468a70dd85a8688eaf87bb42d3dcecf20"}, - {file = "prek-0.3.9-py3-none-musllinux_1_1_i686.whl", hash = "sha256:5517e46e761367a3759b3168eabc120840ffbca9dfbc53187167298a98f87dc4"}, - {file = "prek-0.3.9-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:92024778cf78683ca32687bb249ab6a7d5c33887b5ee1d1a9f6d0c14228f4cf3"}, - {file = "prek-0.3.9-py3-none-win32.whl", hash = "sha256:7f89c55e5f480f5d073769e319924ad69d4bf9f98c5cb46a83082e26e634c958"}, - {file = "prek-0.3.9-py3-none-win_amd64.whl", hash = "sha256:7722f3372eaa83b147e70a43cb7b9fe2128c13d0c78d8a1cdbf2a8ec2ee071eb"}, - {file = "prek-0.3.9-py3-none-win_arm64.whl", hash = "sha256:0bced6278d6cc8a4b46048979e36bc9da034611dc8facd77ab123177b833a929"}, - {file = "prek-0.3.9.tar.gz", hash = "sha256:f82b92d81f42f1f90a47f5fbbf492373e25ef1f790080215b2722dd6da66510e"}, -] - -[[package]] -name = "propcache" -version = "0.3.2" -description = "Accelerated property cache" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "propcache-0.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:22d9962a358aedbb7a2e36187ff273adeaab9743373a272976d2e348d08c7770"}, - {file = "propcache-0.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0d0fda578d1dc3f77b6b5a5dce3b9ad69a8250a891760a548df850a5e8da87f3"}, - {file = "propcache-0.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3def3da3ac3ce41562d85db655d18ebac740cb3fa4367f11a52b3da9d03a5cc3"}, - {file = "propcache-0.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bec58347a5a6cebf239daba9bda37dffec5b8d2ce004d9fe4edef3d2815137e"}, - {file = "propcache-0.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55ffda449a507e9fbd4aca1a7d9aa6753b07d6166140e5a18d2ac9bc49eac220"}, - {file = "propcache-0.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64a67fb39229a8a8491dd42f864e5e263155e729c2e7ff723d6e25f596b1e8cb"}, - {file = "propcache-0.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da1cf97b92b51253d5b68cf5a2b9e0dafca095e36b7f2da335e27dc6172a614"}, - {file = "propcache-0.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5f559e127134b07425134b4065be45b166183fdcb433cb6c24c8e4149056ad50"}, - {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aff2e4e06435d61f11a428360a932138d0ec288b0a31dd9bd78d200bd4a2b339"}, - {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4927842833830942a5d0a56e6f4839bc484785b8e1ce8d287359794818633ba0"}, - {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6107ddd08b02654a30fb8ad7a132021759d750a82578b94cd55ee2772b6ebea2"}, - {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:70bd8b9cd6b519e12859c99f3fc9a93f375ebd22a50296c3a295028bea73b9e7"}, - {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2183111651d710d3097338dd1893fcf09c9f54e27ff1a8795495a16a469cc90b"}, - {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fb075ad271405dcad8e2a7ffc9a750a3bf70e533bd86e89f0603e607b93aa64c"}, - {file = "propcache-0.3.2-cp310-cp310-win32.whl", hash = "sha256:404d70768080d3d3bdb41d0771037da19d8340d50b08e104ca0e7f9ce55fce70"}, - {file = "propcache-0.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:7435d766f978b4ede777002e6b3b6641dd229cd1da8d3d3106a45770365f9ad9"}, - {file = "propcache-0.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0b8d2f607bd8f80ddc04088bc2a037fdd17884a6fcadc47a96e334d72f3717be"}, - {file = "propcache-0.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06766d8f34733416e2e34f46fea488ad5d60726bb9481d3cddf89a6fa2d9603f"}, - {file = "propcache-0.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2dc1f4a1df4fecf4e6f68013575ff4af84ef6f478fe5344317a65d38a8e6dc9"}, - {file = "propcache-0.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be29c4f4810c5789cf10ddf6af80b041c724e629fa51e308a7a0fb19ed1ef7bf"}, - {file = "propcache-0.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59d61f6970ecbd8ff2e9360304d5c8876a6abd4530cb752c06586849ac8a9dc9"}, - {file = "propcache-0.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62180e0b8dbb6b004baec00a7983e4cc52f5ada9cd11f48c3528d8cfa7b96a66"}, - {file = "propcache-0.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c144ca294a204c470f18cf4c9d78887810d04a3e2fbb30eea903575a779159df"}, - {file = "propcache-0.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5c2a784234c28854878d68978265617aa6dc0780e53d44b4d67f3651a17a9a2"}, - {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5745bc7acdafa978ca1642891b82c19238eadc78ba2aaa293c6863b304e552d7"}, - {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c0075bf773d66fa8c9d41f66cc132ecc75e5bb9dd7cce3cfd14adc5ca184cb95"}, - {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5f57aa0847730daceff0497f417c9de353c575d8da3579162cc74ac294c5369e"}, - {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:eef914c014bf72d18efb55619447e0aecd5fb7c2e3fa7441e2e5d6099bddff7e"}, - {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2a4092e8549031e82facf3decdbc0883755d5bbcc62d3aea9d9e185549936dcf"}, - {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:85871b050f174bc0bfb437efbdb68aaf860611953ed12418e4361bc9c392749e"}, - {file = "propcache-0.3.2-cp311-cp311-win32.whl", hash = "sha256:36c8d9b673ec57900c3554264e630d45980fd302458e4ac801802a7fd2ef7897"}, - {file = "propcache-0.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53af8cb6a781b02d2ea079b5b853ba9430fcbe18a8e3ce647d5982a3ff69f39"}, - {file = "propcache-0.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8de106b6c84506b31c27168582cd3cb3000a6412c16df14a8628e5871ff83c10"}, - {file = "propcache-0.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:28710b0d3975117239c76600ea351934ac7b5ff56e60953474342608dbbb6154"}, - {file = "propcache-0.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce26862344bdf836650ed2487c3d724b00fbfec4233a1013f597b78c1cb73615"}, - {file = "propcache-0.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca54bd347a253af2cf4544bbec232ab982f4868de0dd684246b67a51bc6b1db"}, - {file = "propcache-0.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55780d5e9a2ddc59711d727226bb1ba83a22dd32f64ee15594b9392b1f544eb1"}, - {file = "propcache-0.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035e631be25d6975ed87ab23153db6a73426a48db688070d925aa27e996fe93c"}, - {file = "propcache-0.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6f22b6eaa39297c751d0e80c0d3a454f112f5c6481214fcf4c092074cecd67"}, - {file = "propcache-0.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ca3aee1aa955438c4dba34fc20a9f390e4c79967257d830f137bd5a8a32ed3b"}, - {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4f30862869fa2b68380d677cc1c5fcf1e0f2b9ea0cf665812895c75d0ca3b8"}, - {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b77ec3c257d7816d9f3700013639db7491a434644c906a2578a11daf13176251"}, - {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cab90ac9d3f14b2d5050928483d3d3b8fb6b4018893fc75710e6aa361ecb2474"}, - {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0b504d29f3c47cf6b9e936c1852246c83d450e8e063d50562115a6be6d3a2535"}, - {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ce2ac2675a6aa41ddb2a0c9cbff53780a617ac3d43e620f8fd77ba1c84dcfc06"}, - {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1"}, - {file = "propcache-0.3.2-cp312-cp312-win32.whl", hash = "sha256:df4a81b9b53449ebc90cc4deefb052c1dd934ba85012aa912c7ea7b7e38b60c1"}, - {file = "propcache-0.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7046e79b989d7fe457bb755844019e10f693752d169076138abf17f31380800c"}, - {file = "propcache-0.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945"}, - {file = "propcache-0.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252"}, - {file = "propcache-0.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f"}, - {file = "propcache-0.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1a342c834734edb4be5ecb1e9fb48cb64b1e2320fccbd8c54bf8da8f2a84c33"}, - {file = "propcache-0.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a544caaae1ac73f1fecfae70ded3e93728831affebd017d53449e3ac052ac1e"}, - {file = "propcache-0.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:310d11aa44635298397db47a3ebce7db99a4cc4b9bbdfcf6c98a60c8d5261cf1"}, - {file = "propcache-0.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1396592321ac83157ac03a2023aa6cc4a3cc3cfdecb71090054c09e5a7cce3"}, - {file = "propcache-0.3.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cabf5b5902272565e78197edb682017d21cf3b550ba0460ee473753f28d23c1"}, - {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0a2f2235ac46a7aa25bdeb03a9e7060f6ecbd213b1f9101c43b3090ffb971ef6"}, - {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:92b69e12e34869a6970fd2f3da91669899994b47c98f5d430b781c26f1d9f387"}, - {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:54e02207c79968ebbdffc169591009f4474dde3b4679e16634d34c9363ff56b4"}, - {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4adfb44cb588001f68c5466579d3f1157ca07f7504fc91ec87862e2b8e556b88"}, - {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fd3e6019dc1261cd0291ee8919dd91fbab7b169bb76aeef6c716833a3f65d206"}, - {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4c181cad81158d71c41a2bce88edce078458e2dd5ffee7eddd6b05da85079f43"}, - {file = "propcache-0.3.2-cp313-cp313-win32.whl", hash = "sha256:8a08154613f2249519e549de2330cf8e2071c2887309a7b07fb56098f5170a02"}, - {file = "propcache-0.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e41671f1594fc4ab0a6dec1351864713cb3a279910ae8b58f884a88a0a632c05"}, - {file = "propcache-0.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9a3cf035bbaf035f109987d9d55dc90e4b0e36e04bbbb95af3055ef17194057b"}, - {file = "propcache-0.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:156c03d07dc1323d8dacaa221fbe028c5c70d16709cdd63502778e6c3ccca1b0"}, - {file = "propcache-0.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74413c0ba02ba86f55cf60d18daab219f7e531620c15f1e23d95563f505efe7e"}, - {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f066b437bb3fa39c58ff97ab2ca351db465157d68ed0440abecb21715eb24b28"}, - {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1304b085c83067914721e7e9d9917d41ad87696bf70f0bc7dee450e9c71ad0a"}, - {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab50cef01b372763a13333b4e54021bdcb291fc9a8e2ccb9c2df98be51bcde6c"}, - {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad3b2a085ec259ad2c2842666b2a0a49dea8463579c606426128925af1ed725"}, - {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:261fa020c1c14deafd54c76b014956e2f86991af198c51139faf41c4d5e83892"}, - {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:46d7f8aa79c927e5f987ee3a80205c987717d3659f035c85cf0c3680526bdb44"}, - {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:6d8f3f0eebf73e3c0ff0e7853f68be638b4043c65a70517bb575eff54edd8dbe"}, - {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:03c89c1b14a5452cf15403e291c0ccd7751d5b9736ecb2c5bab977ad6c5bcd81"}, - {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:0cc17efde71e12bbaad086d679ce575268d70bc123a5a71ea7ad76f70ba30bba"}, - {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:acdf05d00696bc0447e278bb53cb04ca72354e562cf88ea6f9107df8e7fd9770"}, - {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4445542398bd0b5d32df908031cb1b30d43ac848e20470a878b770ec2dcc6330"}, - {file = "propcache-0.3.2-cp313-cp313t-win32.whl", hash = "sha256:f86e5d7cd03afb3a1db8e9f9f6eff15794e79e791350ac48a8c924e6f439f394"}, - {file = "propcache-0.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9704bedf6e7cbe3c65eca4379a9b53ee6a83749f047808cbb5044d40d7d72198"}, - {file = "propcache-0.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a7fad897f14d92086d6b03fdd2eb844777b0c4d7ec5e3bac0fbae2ab0602bbe5"}, - {file = "propcache-0.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1f43837d4ca000243fd7fd6301947d7cb93360d03cd08369969450cc6b2ce3b4"}, - {file = "propcache-0.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:261df2e9474a5949c46e962065d88eb9b96ce0f2bd30e9d3136bcde84befd8f2"}, - {file = "propcache-0.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e514326b79e51f0a177daab1052bc164d9d9e54133797a3a58d24c9c87a3fe6d"}, - {file = "propcache-0.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4a996adb6904f85894570301939afeee65f072b4fd265ed7e569e8d9058e4ec"}, - {file = "propcache-0.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:76cace5d6b2a54e55b137669b30f31aa15977eeed390c7cbfb1dafa8dfe9a701"}, - {file = "propcache-0.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31248e44b81d59d6addbb182c4720f90b44e1efdc19f58112a3c3a1615fb47ef"}, - {file = "propcache-0.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abb7fa19dbf88d3857363e0493b999b8011eea856b846305d8c0512dfdf8fbb1"}, - {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d81ac3ae39d38588ad0549e321e6f773a4e7cc68e7751524a22885d5bbadf886"}, - {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:cc2782eb0f7a16462285b6f8394bbbd0e1ee5f928034e941ffc444012224171b"}, - {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:db429c19a6c7e8a1c320e6a13c99799450f411b02251fb1b75e6217cf4a14fcb"}, - {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:21d8759141a9e00a681d35a1f160892a36fb6caa715ba0b832f7747da48fb6ea"}, - {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2ca6d378f09adb13837614ad2754fa8afaee330254f404299611bce41a8438cb"}, - {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:34a624af06c048946709f4278b4176470073deda88d91342665d95f7c6270fbe"}, - {file = "propcache-0.3.2-cp39-cp39-win32.whl", hash = "sha256:4ba3fef1c30f306b1c274ce0b8baaa2c3cdd91f645c48f06394068f37d3837a1"}, - {file = "propcache-0.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:7a2368eed65fc69a7a7a40b27f22e85e7627b74216f0846b04ba5c116e191ec9"}, - {file = "propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f"}, - {file = "propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168"}, -] - -[[package]] -name = "proto-plus" -version = "1.26.1" -description = "Beautiful, Pythonic protocol buffers" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66"}, - {file = "proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012"}, -] - -[package.dependencies] -protobuf = ">=3.19.0,<7.0.0" - -[package.extras] -testing = ["google-api-core (>=1.31.5)"] - -[[package]] -name = "protobuf" -version = "6.31.1" -description = "" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "protobuf-6.31.1-cp310-abi3-win32.whl", hash = "sha256:7fa17d5a29c2e04b7d90e5e32388b8bfd0e7107cd8e616feef7ed3fa6bdab5c9"}, - {file = "protobuf-6.31.1-cp310-abi3-win_amd64.whl", hash = "sha256:426f59d2964864a1a366254fa703b8632dcec0790d8862d30034d8245e1cd447"}, - {file = "protobuf-6.31.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:6f1227473dc43d44ed644425268eb7c2e488ae245d51c6866d19fe158e207402"}, - {file = "protobuf-6.31.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:a40fc12b84c154884d7d4c4ebd675d5b3b5283e155f324049ae396b95ddebc39"}, - {file = "protobuf-6.31.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:4ee898bf66f7a8b0bd21bce523814e6fbd8c6add948045ce958b73af7e8878c6"}, - {file = "protobuf-6.31.1-cp39-cp39-win32.whl", hash = "sha256:0414e3aa5a5f3ff423828e1e6a6e907d6c65c1d5b7e6e975793d5590bdeecc16"}, - {file = "protobuf-6.31.1-cp39-cp39-win_amd64.whl", hash = "sha256:8764cf4587791e7564051b35524b72844f845ad0bb011704c3736cce762d8fe9"}, - {file = "protobuf-6.31.1-py3-none-any.whl", hash = "sha256:720a6c7e6b77288b85063569baae8536671b39f15cc22037ec7045658d80489e"}, - {file = "protobuf-6.31.1.tar.gz", hash = "sha256:d8cac4c982f0b957a4dc73a80e2ea24fab08e679c0de9deb835f4a12d69aca9a"}, -] - -[[package]] -name = "psutil" -version = "7.2.2" -description = "Cross-platform lib for process and system monitoring." -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b"}, - {file = "psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea"}, - {file = "psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63"}, - {file = "psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312"}, - {file = "psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b"}, - {file = "psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9"}, - {file = "psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00"}, - {file = "psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9"}, - {file = "psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a"}, - {file = "psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf"}, - {file = "psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1"}, - {file = "psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841"}, - {file = "psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486"}, - {file = "psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979"}, - {file = "psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9"}, - {file = "psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e"}, - {file = "psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8"}, - {file = "psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc"}, - {file = "psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988"}, - {file = "psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee"}, - {file = "psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372"}, -] - -[package.extras] -dev = ["abi3audit", "black", "check-manifest", "colorama ; os_name == \"nt\"", "coverage", "packaging", "psleak", "pylint", "pyperf", "pypinfo", "pyreadline3 ; os_name == \"nt\"", "pytest", "pytest-cov", "pytest-instafail", "pytest-xdist", "pywin32 ; os_name == \"nt\" and implementation_name != \"pypy\"", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "validate-pyproject[all]", "virtualenv", "vulture", "wheel", "wheel ; os_name == \"nt\" and implementation_name != \"pypy\"", "wmi ; os_name == \"nt\" and implementation_name != \"pypy\""] -test = ["psleak", "pytest", "pytest-instafail", "pytest-xdist", "pywin32 ; os_name == \"nt\" and implementation_name != \"pypy\"", "setuptools", "wheel ; os_name == \"nt\" and implementation_name != \"pypy\"", "wmi ; os_name == \"nt\" and implementation_name != \"pypy\""] - -[[package]] -name = "py-iam-expand" -version = "0.1.0" -description = "This is a Python package to expand and deobfuscate IAM policies." -optional = false -python-versions = "<3.14,>3.9.1" -groups = ["main"] -files = [ - {file = "py_iam_expand-0.1.0-py3-none-any.whl", hash = "sha256:b845ce7b50ac895b02b4f338e09c62a68ea51849794f76e189b02009bd388510"}, - {file = "py_iam_expand-0.1.0.tar.gz", hash = "sha256:5a2884dc267ac59a02c3a80fefc0b34c309dac681baa0f87c436067c6cf53a96"}, -] - -[package.dependencies] -iamdata = ">=0.1.202504091" - -[[package]] -name = "py-ocsf-models" -version = "0.8.1" -description = "This is a Python implementation of the OCSF models. The models are used to represent the data of the OCSF Schema defined in https://schema.ocsf.io/." -optional = false -python-versions = "<3.15,>3.9.1" -groups = ["main"] -files = [ - {file = "py_ocsf_models-0.8.1-py3-none-any.whl", hash = "sha256:061eb446c4171534c09a8b37f5a9d2a2fe9f87c5db32edbd1182446bc5fd097e"}, - {file = "py_ocsf_models-0.8.1.tar.gz", hash = "sha256:c9045237857f951e073c9f9d1f57954c90d86875b469260725292d47f7a7d73c"}, -] - -[package.dependencies] -cryptography = ">=44.0.3,<47" -email-validator = "2.2.0" -pydantic = ">=2.12.0,<3.0.0" - -[[package]] -name = "py-partiql-parser" -version = "0.6.1" -description = "Pure Python PartiQL Parser" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "py_partiql_parser-0.6.1-py2.py3-none-any.whl", hash = "sha256:ff6a48067bff23c37e9044021bf1d949c83e195490c17e020715e927fe5b2456"}, - {file = "py_partiql_parser-0.6.1.tar.gz", hash = "sha256:8583ff2a0e15560ef3bc3df109a7714d17f87d81d33e8c38b7fed4e58a63215d"}, -] - -[package.extras] -dev = ["black (==22.6.0)", "flake8", "mypy", "pytest"] - -[[package]] -name = "pyasn1" -version = "0.6.3" -description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde"}, - {file = "pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf"}, -] - -[[package]] -name = "pyasn1-modules" -version = "0.4.2" -description = "A collection of ASN.1-based protocols modules" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a"}, - {file = "pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6"}, -] - -[package.dependencies] -pyasn1 = ">=0.6.1,<0.7.0" - -[[package]] -name = "pycodestyle" -version = "2.12.1" -description = "Python style guide checker" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3"}, - {file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"}, -] - -[[package]] -name = "pycparser" -version = "2.22" -description = "C parser in Python" -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\"" -files = [ - {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, - {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, -] - -[[package]] -name = "pydantic" -version = "2.12.5" -description = "Data validation using Python type hints" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d"}, - {file = "pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49"}, -] - -[package.dependencies] -annotated-types = ">=0.6.0" -pydantic-core = "2.41.5" -typing-extensions = ">=4.14.1" -typing-inspection = ">=0.4.2" - -[package.extras] -email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] - -[[package]] -name = "pydantic-core" -version = "2.41.5" -description = "Core functionality for Pydantic validation and serialization" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146"}, - {file = "pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c"}, - {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2"}, - {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556"}, - {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49"}, - {file = "pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba"}, - {file = "pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9"}, - {file = "pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6"}, - {file = "pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284"}, - {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594"}, - {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e"}, - {file = "pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe"}, - {file = "pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f"}, - {file = "pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7"}, - {file = "pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5"}, - {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c"}, - {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294"}, - {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1"}, - {file = "pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d"}, - {file = "pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815"}, - {file = "pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3"}, - {file = "pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9"}, - {file = "pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d"}, - {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740"}, - {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e"}, - {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858"}, - {file = "pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36"}, - {file = "pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11"}, - {file = "pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd"}, - {file = "pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a"}, - {file = "pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553"}, - {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90"}, - {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07"}, - {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb"}, - {file = "pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23"}, - {file = "pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf"}, - {file = "pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008"}, - {file = "pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf"}, - {file = "pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425"}, - {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504"}, - {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5"}, - {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3"}, - {file = "pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460"}, - {file = "pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51"}, - {file = "pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e"}, -] - -[package.dependencies] -typing-extensions = ">=4.14.1" - -[[package]] -name = "pyflakes" -version = "3.2.0" -description = "passive checker of Python programs" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, - {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, -] - -[[package]] -name = "pygithub" -version = "2.8.0" -description = "Use the full Github API v3" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "pygithub-2.8.0-py3-none-any.whl", hash = "sha256:11a3473c1c2f1c39c525d0ee8c559f369c6d46c272cb7321c9b0cabc7aa1ce7d"}, - {file = "pygithub-2.8.0.tar.gz", hash = "sha256:72f5f2677d86bc3a8843aa720c6ce4c1c42fb7500243b136e3d5e14ddb5c3386"}, -] - -[package.dependencies] -pyjwt = {version = ">=2.4.0", extras = ["crypto"]} -pynacl = ">=1.4.0" -requests = ">=2.14.0" -typing-extensions = ">=4.5.0" -urllib3 = ">=1.26.0" - -[[package]] -name = "pygments" -version = "2.20.0" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, - {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, -] - -[package.extras] -windows-terminal = ["colorama (>=0.4.6)"] - -[[package]] -name = "pyjwt" -version = "2.12.1" -description = "JSON Web Token implementation in Python" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c"}, - {file = "pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b"}, -] - -[package.dependencies] -cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""} -typing_extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} - -[package.extras] -crypto = ["cryptography (>=3.4.0)"] -dev = ["coverage[toml] (==7.10.7)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=8.4.2,<9.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] -docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] -tests = ["coverage[toml] (==7.10.7)", "pytest (>=8.4.2,<9.0.0)"] - -[[package]] -name = "pylint" -version = "3.3.4" -description = "python code static checker" -optional = false -python-versions = ">=3.9.0" -groups = ["dev"] -files = [ - {file = "pylint-3.3.4-py3-none-any.whl", hash = "sha256:289e6a1eb27b453b08436478391a48cd53bb0efb824873f949e709350f3de018"}, - {file = "pylint-3.3.4.tar.gz", hash = "sha256:74ae7a38b177e69a9b525d0794bd8183820bfa7eb68cc1bee6e8ed22a42be4ce"}, -] - -[package.dependencies] -astroid = ">=3.3.8,<=3.4.0.dev0" -colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -dill = [ - {version = ">=0.2", markers = "python_version < \"3.11\""}, - {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, - {version = ">=0.3.6", markers = "python_version == \"3.11\""}, -] -isort = ">=4.2.5,<5.13.0 || >5.13.0,<7" -mccabe = ">=0.6,<0.8" -platformdirs = ">=2.2.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -tomlkit = ">=0.10.1" - -[package.extras] -spelling = ["pyenchant (>=3.2,<4.0)"] -testutils = ["gitpython (>3)"] - -[[package]] -name = "pynacl" -version = "1.6.2" -description = "Python binding to the Networking and Cryptography (NaCl) library" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "pynacl-1.6.2-cp314-cp314t-macosx_10_10_universal2.whl", hash = "sha256:622d7b07cc5c02c666795792931b50c91f3ce3c2649762efb1ef0d5684c81594"}, - {file = "pynacl-1.6.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d071c6a9a4c94d79eb665db4ce5cedc537faf74f2355e4d502591d850d3913c0"}, - {file = "pynacl-1.6.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe9847ca47d287af41e82be1dd5e23023d3c31a951da134121ab02e42ac218c9"}, - {file = "pynacl-1.6.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04316d1fc625d860b6c162fff704eb8426b1a8bcd3abacea11142cbd99a6b574"}, - {file = "pynacl-1.6.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44081faff368d6c5553ccf55322ef2819abb40e25afaec7e740f159f74813634"}, - {file = "pynacl-1.6.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:a9f9932d8d2811ce1a8ffa79dcbdf3970e7355b5c8eb0c1a881a57e7f7d96e88"}, - {file = "pynacl-1.6.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:bc4a36b28dd72fb4845e5d8f9760610588a96d5a51f01d84d8c6ff9849968c14"}, - {file = "pynacl-1.6.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bffb6d0f6becacb6526f8f42adfb5efb26337056ee0831fb9a7044d1a964444"}, - {file = "pynacl-1.6.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fef529ef3ee487ad8113d287a593fa26f48ee3620d92ecc6f1d09ea38e0709b"}, - {file = "pynacl-1.6.2-cp314-cp314t-win32.whl", hash = "sha256:a84bf1c20339d06dc0c85d9aea9637a24f718f375d861b2668b2f9f96fa51145"}, - {file = "pynacl-1.6.2-cp314-cp314t-win_amd64.whl", hash = "sha256:320ef68a41c87547c91a8b58903c9caa641ab01e8512ce291085b5fe2fcb7590"}, - {file = "pynacl-1.6.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d29bfe37e20e015a7d8b23cfc8bd6aa7909c92a1b8f41ee416bbb3e79ef182b2"}, - {file = "pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:c949ea47e4206af7c8f604b8278093b674f7c79ed0d4719cc836902bf4517465"}, - {file = "pynacl-1.6.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8845c0631c0be43abdd865511c41eab235e0be69c81dc66a50911594198679b0"}, - {file = "pynacl-1.6.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22de65bb9010a725b0dac248f353bb072969c94fa8d6b1f34b87d7953cf7bbe4"}, - {file = "pynacl-1.6.2-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46065496ab748469cdd999246d17e301b2c24ae2fdf739132e580a0e94c94a87"}, - {file = "pynacl-1.6.2-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a66d6fb6ae7661c58995f9c6435bda2b1e68b54b598a6a10247bfcdadac996c"}, - {file = "pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130"}, - {file = "pynacl-1.6.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c8a231e36ec2cab018c4ad4358c386e36eede0319a0c41fed24f840b1dac59f6"}, - {file = "pynacl-1.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:68be3a09455743ff9505491220b64440ced8973fe930f270c8e07ccfa25b1f9e"}, - {file = "pynacl-1.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b097553b380236d51ed11356c953bf8ce36a29a3e596e934ecabe76c985a577"}, - {file = "pynacl-1.6.2-cp38-abi3-win32.whl", hash = "sha256:5811c72b473b2f38f7e2a3dc4f8642e3a3e9b5e7317266e4ced1fba85cae41aa"}, - {file = "pynacl-1.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:62985f233210dee6548c223301b6c25440852e13d59a8b81490203c3227c5ba0"}, - {file = "pynacl-1.6.2-cp38-abi3-win_arm64.whl", hash = "sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c"}, - {file = "pynacl-1.6.2.tar.gz", hash = "sha256:018494d6d696ae03c7e656e5e74cdfd8ea1326962cc401bcf018f1ed8436811c"}, -] - -[package.dependencies] -cffi = {version = ">=2.0.0", markers = "platform_python_implementation != \"PyPy\" and python_version >= \"3.9\""} - -[package.extras] -docs = ["sphinx (<7)", "sphinx_rtd_theme"] -tests = ["hypothesis (>=3.27.0)", "pytest (>=7.4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] - -[[package]] -name = "pyopenssl" -version = "26.0.0" -description = "Python wrapper module around the OpenSSL library" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "pyopenssl-26.0.0-py3-none-any.whl", hash = "sha256:df94d28498848b98cc1c0ffb8ef1e71e40210d3b0a8064c9d29571ed2904bf81"}, - {file = "pyopenssl-26.0.0.tar.gz", hash = "sha256:f293934e52936f2e3413b89c6ce36df66a0b34ae1ea3a053b8c5020ff2f513fc"}, -] - -[package.dependencies] -cryptography = ">=46.0.0,<47" -typing-extensions = {version = ">=4.9", markers = "python_version < \"3.13\" and python_version >= \"3.8\""} - -[package.extras] -docs = ["sphinx (!=5.2.0,!=5.2.0.post0,!=7.2.5)", "sphinx_rtd_theme"] -test = ["pretend", "pytest (>=3.0.1)", "pytest-rerunfailures"] - -[[package]] -name = "pyparsing" -version = "3.2.3" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf"}, - {file = "pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be"}, -] - -[package.extras] -diagrams = ["jinja2", "railroad-diagrams"] - -[[package]] -name = "pytest" -version = "8.3.5" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, - {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=1.5,<2" -tomli = {version = ">=1", markers = "python_version < \"3.11\""} - -[package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-cov" -version = "6.0.0" -description = "Pytest plugin for measuring coverage." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, - {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, -] - -[package.dependencies] -coverage = {version = ">=7.5", extras = ["toml"]} -pytest = ">=4.6" - -[package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] - -[[package]] -name = "pytest-env" -version = "1.1.5" -description = "pytest plugin that allows you to add environment variables." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pytest_env-1.1.5-py3-none-any.whl", hash = "sha256:ce90cf8772878515c24b31cd97c7fa1f4481cd68d588419fd45f10ecaee6bc30"}, - {file = "pytest_env-1.1.5.tar.gz", hash = "sha256:91209840aa0e43385073ac464a554ad2947cc2fd663a9debf88d03b01e0cc1cf"}, -] - -[package.dependencies] -pytest = ">=8.3.3" -tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} - -[package.extras] -testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "pytest-mock (>=3.14)"] - -[[package]] -name = "pytest-randomly" -version = "3.16.0" -description = "Pytest plugin to randomly order tests and control random.seed." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pytest_randomly-3.16.0-py3-none-any.whl", hash = "sha256:8633d332635a1a0983d3bba19342196807f6afb17c3eef78e02c2f85dade45d6"}, - {file = "pytest_randomly-3.16.0.tar.gz", hash = "sha256:11bf4d23a26484de7860d82f726c0629837cf4064b79157bd18ec9d41d7feb26"}, -] - -[package.dependencies] -pytest = "*" - -[[package]] -name = "pytest-xdist" -version = "3.6.1" -description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7"}, - {file = "pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"}, -] - -[package.dependencies] -execnet = ">=2.1" -pytest = ">=7.0.0" - -[package.extras] -psutil = ["psutil (>=3.0)"] -setproctitle = ["setproctitle"] -testing = ["filelock"] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -description = "Extensions to the standard Python datetime module" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main", "dev"] -files = [ - {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, - {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, -] - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "pytz" -version = "2025.1" -description = "World timezone definitions, modern and historical" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57"}, - {file = "pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e"}, -] - -[[package]] -name = "pywin32" -version = "311" -description = "Python for Window Extensions" -optional = false -python-versions = "*" -groups = ["dev"] -markers = "sys_platform == \"win32\"" -files = [ - {file = "pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3"}, - {file = "pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b"}, - {file = "pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b"}, - {file = "pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151"}, - {file = "pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503"}, - {file = "pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2"}, - {file = "pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31"}, - {file = "pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067"}, - {file = "pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852"}, - {file = "pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d"}, - {file = "pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d"}, - {file = "pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a"}, - {file = "pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee"}, - {file = "pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87"}, - {file = "pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42"}, - {file = "pywin32-311-cp38-cp38-win32.whl", hash = "sha256:6c6f2969607b5023b0d9ce2541f8d2cbb01c4f46bc87456017cf63b73f1e2d8c"}, - {file = "pywin32-311-cp38-cp38-win_amd64.whl", hash = "sha256:c8015b09fb9a5e188f83b7b04de91ddca4658cee2ae6f3bc483f0b21a77ef6cd"}, - {file = "pywin32-311-cp39-cp39-win32.whl", hash = "sha256:aba8f82d551a942cb20d4a83413ccbac30790b50efb89a75e4f586ac0bb8056b"}, - {file = "pywin32-311-cp39-cp39-win_amd64.whl", hash = "sha256:e0c4cfb0621281fe40387df582097fd796e80430597cb9944f0ae70447bacd91"}, - {file = "pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d"}, -] - -[[package]] -name = "pyyaml" -version = "6.0.2" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, - {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, - {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, - {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, - {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, - {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, - {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, - {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, - {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, - {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, - {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, - {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, - {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, - {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, - {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, - {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, - {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, -] - -[[package]] -name = "referencing" -version = "0.36.2" -description = "JSON Referencing + Python" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0"}, - {file = "referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa"}, -] - -[package.dependencies] -attrs = ">=22.2.0" -rpds-py = ">=0.7.0" -typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} - -[[package]] -name = "regex" -version = "2025.9.18" -description = "Alternative regular expression module, to replace re." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "regex-2025.9.18-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:12296202480c201c98a84aecc4d210592b2f55e200a1d193235c4db92b9f6788"}, - {file = "regex-2025.9.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:220381f1464a581f2ea988f2220cf2a67927adcef107d47d6897ba5a2f6d51a4"}, - {file = "regex-2025.9.18-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:87f681bfca84ebd265278b5daa1dcb57f4db315da3b5d044add7c30c10442e61"}, - {file = "regex-2025.9.18-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34d674cbba70c9398074c8a1fcc1a79739d65d1105de2a3c695e2b05ea728251"}, - {file = "regex-2025.9.18-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:385c9b769655cb65ea40b6eea6ff763cbb6d69b3ffef0b0db8208e1833d4e746"}, - {file = "regex-2025.9.18-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8900b3208e022570ae34328712bef6696de0804c122933414014bae791437ab2"}, - {file = "regex-2025.9.18-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c204e93bf32cd7a77151d44b05eb36f469d0898e3fba141c026a26b79d9914a0"}, - {file = "regex-2025.9.18-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3acc471d1dd7e5ff82e6cacb3b286750decd949ecd4ae258696d04f019817ef8"}, - {file = "regex-2025.9.18-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6479d5555122433728760e5f29edb4c2b79655a8deb681a141beb5c8a025baea"}, - {file = "regex-2025.9.18-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:431bd2a8726b000eb6f12429c9b438a24062a535d06783a93d2bcbad3698f8a8"}, - {file = "regex-2025.9.18-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0cc3521060162d02bd36927e20690129200e5ac9d2c6d32b70368870b122db25"}, - {file = "regex-2025.9.18-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a021217b01be2d51632ce056d7a837d3fa37c543ede36e39d14063176a26ae29"}, - {file = "regex-2025.9.18-cp310-cp310-win32.whl", hash = "sha256:4a12a06c268a629cb67cc1d009b7bb0be43e289d00d5111f86a2efd3b1949444"}, - {file = "regex-2025.9.18-cp310-cp310-win_amd64.whl", hash = "sha256:47acd811589301298c49db2c56bde4f9308d6396da92daf99cba781fa74aa450"}, - {file = "regex-2025.9.18-cp310-cp310-win_arm64.whl", hash = "sha256:16bd2944e77522275e5ee36f867e19995bcaa533dcb516753a26726ac7285442"}, - {file = "regex-2025.9.18-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:51076980cd08cd13c88eb7365427ae27f0d94e7cebe9ceb2bb9ffdae8fc4d82a"}, - {file = "regex-2025.9.18-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:828446870bd7dee4e0cbeed767f07961aa07f0ea3129f38b3ccecebc9742e0b8"}, - {file = "regex-2025.9.18-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c28821d5637866479ec4cc23b8c990f5bc6dd24e5e4384ba4a11d38a526e1414"}, - {file = "regex-2025.9.18-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:726177ade8e481db669e76bf99de0b278783be8acd11cef71165327abd1f170a"}, - {file = "regex-2025.9.18-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f5cca697da89b9f8ea44115ce3130f6c54c22f541943ac8e9900461edc2b8bd4"}, - {file = "regex-2025.9.18-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dfbde38f38004703c35666a1e1c088b778e35d55348da2b7b278914491698d6a"}, - {file = "regex-2025.9.18-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f2f422214a03fab16bfa495cfec72bee4aaa5731843b771860a471282f1bf74f"}, - {file = "regex-2025.9.18-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a295916890f4df0902e4286bc7223ee7f9e925daa6dcdec4192364255b70561a"}, - {file = "regex-2025.9.18-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:5db95ff632dbabc8c38c4e82bf545ab78d902e81160e6e455598014f0abe66b9"}, - {file = "regex-2025.9.18-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fb967eb441b0f15ae610b7069bdb760b929f267efbf522e814bbbfffdf125ce2"}, - {file = "regex-2025.9.18-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f04d2f20da4053d96c08f7fde6e1419b7ec9dbcee89c96e3d731fca77f411b95"}, - {file = "regex-2025.9.18-cp311-cp311-win32.whl", hash = "sha256:895197241fccf18c0cea7550c80e75f185b8bd55b6924fcae269a1a92c614a07"}, - {file = "regex-2025.9.18-cp311-cp311-win_amd64.whl", hash = "sha256:7e2b414deae99166e22c005e154a5513ac31493db178d8aec92b3269c9cce8c9"}, - {file = "regex-2025.9.18-cp311-cp311-win_arm64.whl", hash = "sha256:fb137ec7c5c54f34a25ff9b31f6b7b0c2757be80176435bf367111e3f71d72df"}, - {file = "regex-2025.9.18-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:436e1b31d7efd4dcd52091d076482031c611dde58bf9c46ca6d0a26e33053a7e"}, - {file = "regex-2025.9.18-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c190af81e5576b9c5fdc708f781a52ff20f8b96386c6e2e0557a78402b029f4a"}, - {file = "regex-2025.9.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e4121f1ce2b2b5eec4b397cc1b277686e577e658d8f5870b7eb2d726bd2300ab"}, - {file = "regex-2025.9.18-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:300e25dbbf8299d87205e821a201057f2ef9aa3deb29caa01cd2cac669e508d5"}, - {file = "regex-2025.9.18-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7b47fcf9f5316c0bdaf449e879407e1b9937a23c3b369135ca94ebc8d74b1742"}, - {file = "regex-2025.9.18-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:57a161bd3acaa4b513220b49949b07e252165e6b6dc910ee7617a37ff4f5b425"}, - {file = "regex-2025.9.18-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f130c3a7845ba42de42f380fff3c8aebe89a810747d91bcf56d40a069f15352"}, - {file = "regex-2025.9.18-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5f96fa342b6f54dcba928dd452e8d8cb9f0d63e711d1721cd765bb9f73bb048d"}, - {file = "regex-2025.9.18-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0f0d676522d68c207828dcd01fb6f214f63f238c283d9f01d85fc664c7c85b56"}, - {file = "regex-2025.9.18-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:40532bff8a1a0621e7903ae57fce88feb2e8a9a9116d341701302c9302aef06e"}, - {file = "regex-2025.9.18-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:039f11b618ce8d71a1c364fdee37da1012f5a3e79b1b2819a9f389cd82fd6282"}, - {file = "regex-2025.9.18-cp312-cp312-win32.whl", hash = "sha256:e1dd06f981eb226edf87c55d523131ade7285137fbde837c34dc9d1bf309f459"}, - {file = "regex-2025.9.18-cp312-cp312-win_amd64.whl", hash = "sha256:3d86b5247bf25fa3715e385aa9ff272c307e0636ce0c9595f64568b41f0a9c77"}, - {file = "regex-2025.9.18-cp312-cp312-win_arm64.whl", hash = "sha256:032720248cbeeae6444c269b78cb15664458b7bb9ed02401d3da59fe4d68c3a5"}, - {file = "regex-2025.9.18-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2a40f929cd907c7e8ac7566ac76225a77701a6221bca937bdb70d56cb61f57b2"}, - {file = "regex-2025.9.18-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c90471671c2cdf914e58b6af62420ea9ecd06d1554d7474d50133ff26ae88feb"}, - {file = "regex-2025.9.18-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a351aff9e07a2dabb5022ead6380cff17a4f10e4feb15f9100ee56c4d6d06af"}, - {file = "regex-2025.9.18-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc4b8e9d16e20ddfe16430c23468a8707ccad3365b06d4536142e71823f3ca29"}, - {file = "regex-2025.9.18-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4b8cdbddf2db1c5e80338ba2daa3cfa3dec73a46fff2a7dda087c8efbf12d62f"}, - {file = "regex-2025.9.18-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a276937d9d75085b2c91fb48244349c6954f05ee97bba0963ce24a9d915b8b68"}, - {file = "regex-2025.9.18-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92a8e375ccdc1256401c90e9dc02b8642894443d549ff5e25e36d7cf8a80c783"}, - {file = "regex-2025.9.18-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0dc6893b1f502d73037cf807a321cdc9be29ef3d6219f7970f842475873712ac"}, - {file = "regex-2025.9.18-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a61e85bfc63d232ac14b015af1261f826260c8deb19401c0597dbb87a864361e"}, - {file = "regex-2025.9.18-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1ef86a9ebc53f379d921fb9a7e42b92059ad3ee800fcd9e0fe6181090e9f6c23"}, - {file = "regex-2025.9.18-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d3bc882119764ba3a119fbf2bd4f1b47bc56c1da5d42df4ed54ae1e8e66fdf8f"}, - {file = "regex-2025.9.18-cp313-cp313-win32.whl", hash = "sha256:3810a65675845c3bdfa58c3c7d88624356dd6ee2fc186628295e0969005f928d"}, - {file = "regex-2025.9.18-cp313-cp313-win_amd64.whl", hash = "sha256:16eaf74b3c4180ede88f620f299e474913ab6924d5c4b89b3833bc2345d83b3d"}, - {file = "regex-2025.9.18-cp313-cp313-win_arm64.whl", hash = "sha256:4dc98ba7dd66bd1261927a9f49bd5ee2bcb3660f7962f1ec02617280fc00f5eb"}, - {file = "regex-2025.9.18-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:fe5d50572bc885a0a799410a717c42b1a6b50e2f45872e2b40f4f288f9bce8a2"}, - {file = "regex-2025.9.18-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b9d9a2d6cda6621551ca8cf7a06f103adf72831153f3c0d982386110870c4d3"}, - {file = "regex-2025.9.18-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:13202e4c4ac0ef9a317fff817674b293c8f7e8c68d3190377d8d8b749f566e12"}, - {file = "regex-2025.9.18-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:874ff523b0fecffb090f80ae53dc93538f8db954c8bb5505f05b7787ab3402a0"}, - {file = "regex-2025.9.18-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d13ab0490128f2bb45d596f754148cd750411afc97e813e4b3a61cf278a23bb6"}, - {file = "regex-2025.9.18-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:05440bc172bc4b4b37fb9667e796597419404dbba62e171e1f826d7d2a9ebcef"}, - {file = "regex-2025.9.18-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5514b8e4031fdfaa3d27e92c75719cbe7f379e28cacd939807289bce76d0e35a"}, - {file = "regex-2025.9.18-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:65d3c38c39efce73e0d9dc019697b39903ba25b1ad45ebbd730d2cf32741f40d"}, - {file = "regex-2025.9.18-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ae77e447ebc144d5a26d50055c6ddba1d6ad4a865a560ec7200b8b06bc529368"}, - {file = "regex-2025.9.18-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e3ef8cf53dc8df49d7e28a356cf824e3623764e9833348b655cfed4524ab8a90"}, - {file = "regex-2025.9.18-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9feb29817df349c976da9a0debf775c5c33fc1c8ad7b9f025825da99374770b7"}, - {file = "regex-2025.9.18-cp313-cp313t-win32.whl", hash = "sha256:168be0d2f9b9d13076940b1ed774f98595b4e3c7fc54584bba81b3cc4181742e"}, - {file = "regex-2025.9.18-cp313-cp313t-win_amd64.whl", hash = "sha256:d59ecf3bb549e491c8104fea7313f3563c7b048e01287db0a90485734a70a730"}, - {file = "regex-2025.9.18-cp313-cp313t-win_arm64.whl", hash = "sha256:dbef80defe9fb21310948a2595420b36c6d641d9bea4c991175829b2cc4bc06a"}, - {file = "regex-2025.9.18-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c6db75b51acf277997f3adcd0ad89045d856190d13359f15ab5dda21581d9129"}, - {file = "regex-2025.9.18-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8f9698b6f6895d6db810e0bda5364f9ceb9e5b11328700a90cae573574f61eea"}, - {file = "regex-2025.9.18-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29cd86aa7cb13a37d0f0d7c21d8d949fe402ffa0ea697e635afedd97ab4b69f1"}, - {file = "regex-2025.9.18-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c9f285a071ee55cd9583ba24dde006e53e17780bb309baa8e4289cd472bcc47"}, - {file = "regex-2025.9.18-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5adf266f730431e3be9021d3e5b8d5ee65e563fec2883ea8093944d21863b379"}, - {file = "regex-2025.9.18-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1137cabc0f38807de79e28d3f6e3e3f2cc8cfb26bead754d02e6d1de5f679203"}, - {file = "regex-2025.9.18-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cc9e5525cada99699ca9223cce2d52e88c52a3d2a0e842bd53de5497c604164"}, - {file = "regex-2025.9.18-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bbb9246568f72dce29bcd433517c2be22c7791784b223a810225af3b50d1aafb"}, - {file = "regex-2025.9.18-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6a52219a93dd3d92c675383efff6ae18c982e2d7651c792b1e6d121055808743"}, - {file = "regex-2025.9.18-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:ae9b3840c5bd456780e3ddf2f737ab55a79b790f6409182012718a35c6d43282"}, - {file = "regex-2025.9.18-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d488c236ac497c46a5ac2005a952c1a0e22a07be9f10c3e735bc7d1209a34773"}, - {file = "regex-2025.9.18-cp314-cp314-win32.whl", hash = "sha256:0c3506682ea19beefe627a38872d8da65cc01ffa25ed3f2e422dffa1474f0788"}, - {file = "regex-2025.9.18-cp314-cp314-win_amd64.whl", hash = "sha256:57929d0f92bebb2d1a83af372cd0ffba2263f13f376e19b1e4fa32aec4efddc3"}, - {file = "regex-2025.9.18-cp314-cp314-win_arm64.whl", hash = "sha256:6a4b44df31d34fa51aa5c995d3aa3c999cec4d69b9bd414a8be51984d859f06d"}, - {file = "regex-2025.9.18-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:b176326bcd544b5e9b17d6943f807697c0cb7351f6cfb45bf5637c95ff7e6306"}, - {file = "regex-2025.9.18-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:0ffd9e230b826b15b369391bec167baed57c7ce39efc35835448618860995946"}, - {file = "regex-2025.9.18-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec46332c41add73f2b57e2f5b642f991f6b15e50e9f86285e08ffe3a512ac39f"}, - {file = "regex-2025.9.18-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b80fa342ed1ea095168a3f116637bd1030d39c9ff38dc04e54ef7c521e01fc95"}, - {file = "regex-2025.9.18-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4d97071c0ba40f0cf2a93ed76e660654c399a0a04ab7d85472239460f3da84b"}, - {file = "regex-2025.9.18-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0ac936537ad87cef9e0e66c5144484206c1354224ee811ab1519a32373e411f3"}, - {file = "regex-2025.9.18-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dec57f96d4def58c422d212d414efe28218d58537b5445cf0c33afb1b4768571"}, - {file = "regex-2025.9.18-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:48317233294648bf7cd068857f248e3a57222259a5304d32c7552e2284a1b2ad"}, - {file = "regex-2025.9.18-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:274687e62ea3cf54846a9b25fc48a04459de50af30a7bd0b61a9e38015983494"}, - {file = "regex-2025.9.18-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a78722c86a3e7e6aadf9579e3b0ad78d955f2d1f1a8ca4f67d7ca258e8719d4b"}, - {file = "regex-2025.9.18-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:06104cd203cdef3ade989a1c45b6215bf42f8b9dd705ecc220c173233f7cba41"}, - {file = "regex-2025.9.18-cp314-cp314t-win32.whl", hash = "sha256:2e1eddc06eeaffd249c0adb6fafc19e2118e6308c60df9db27919e96b5656096"}, - {file = "regex-2025.9.18-cp314-cp314t-win_amd64.whl", hash = "sha256:8620d247fb8c0683ade51217b459cb4a1081c0405a3072235ba43a40d355c09a"}, - {file = "regex-2025.9.18-cp314-cp314t-win_arm64.whl", hash = "sha256:b7531a8ef61de2c647cdf68b3229b071e46ec326b3138b2180acb4275f470b01"}, - {file = "regex-2025.9.18-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3dbcfcaa18e9480669030d07371713c10b4f1a41f791ffa5cb1a99f24e777f40"}, - {file = "regex-2025.9.18-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1e85f73ef7095f0380208269055ae20524bfde3f27c5384126ddccf20382a638"}, - {file = "regex-2025.9.18-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9098e29b3ea4ffffeade423f6779665e2a4f8db64e699c0ed737ef0db6ba7b12"}, - {file = "regex-2025.9.18-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90b6b7a2d0f45b7ecaaee1aec6b362184d6596ba2092dd583ffba1b78dd0231c"}, - {file = "regex-2025.9.18-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c81b892af4a38286101502eae7aec69f7cd749a893d9987a92776954f3943408"}, - {file = "regex-2025.9.18-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3b524d010973f2e1929aeb635418d468d869a5f77b52084d9f74c272189c251d"}, - {file = "regex-2025.9.18-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b498437c026a3d5d0be0020023ff76d70ae4d77118e92f6f26c9d0423452446"}, - {file = "regex-2025.9.18-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0716e4d6e58853d83f6563f3cf25c281ff46cf7107e5f11879e32cb0b59797d9"}, - {file = "regex-2025.9.18-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:065b6956749379d41db2625f880b637d4acc14c0a4de0d25d609a62850e96d36"}, - {file = "regex-2025.9.18-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d4a691494439287c08ddb9b5793da605ee80299dd31e95fa3f323fac3c33d9d4"}, - {file = "regex-2025.9.18-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ef8d10cc0989565bcbe45fb4439f044594d5c2b8919d3d229ea2c4238f1d55b0"}, - {file = "regex-2025.9.18-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4baeb1b16735ac969a7eeecc216f1f8b7caf60431f38a2671ae601f716a32d25"}, - {file = "regex-2025.9.18-cp39-cp39-win32.whl", hash = "sha256:8e5f41ad24a1e0b5dfcf4c4e5d9f5bd54c895feb5708dd0c1d0d35693b24d478"}, - {file = "regex-2025.9.18-cp39-cp39-win_amd64.whl", hash = "sha256:50e8290707f2fb8e314ab3831e594da71e062f1d623b05266f8cfe4db4949afd"}, - {file = "regex-2025.9.18-cp39-cp39-win_arm64.whl", hash = "sha256:039a9d7195fd88c943d7c777d4941e8ef736731947becce773c31a1009cb3c35"}, - {file = "regex-2025.9.18.tar.gz", hash = "sha256:c5ba23274c61c6fef447ba6a39333297d0c247f53059dba0bca415cac511edc4"}, -] - -[[package]] -name = "requests" -version = "2.32.4" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, - {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset_normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "requests-file" -version = "2.1.0" -description = "File transport adapter for Requests" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "requests_file-2.1.0-py2.py3-none-any.whl", hash = "sha256:cf270de5a4c5874e84599fc5778303d496c10ae5e870bfa378818f35d21bda5c"}, - {file = "requests_file-2.1.0.tar.gz", hash = "sha256:0f549a3f3b0699415ac04d167e9cb39bccfb730cb832b4d20be3d9867356e658"}, -] - -[package.dependencies] -requests = ">=1.0.0" - -[[package]] -name = "requests-oauthlib" -version = "2.0.0" -description = "OAuthlib authentication support for Requests." -optional = false -python-versions = ">=3.4" -groups = ["main"] -files = [ - {file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"}, - {file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"}, -] - -[package.dependencies] -oauthlib = ">=3.0.0" -requests = ">=2.0.0" - -[package.extras] -rsa = ["oauthlib[signedtoken] (>=3.0.0)"] - -[[package]] -name = "requestsexceptions" -version = "1.4.0" -description = "Import exceptions from potentially bundled packages in requests." -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "requestsexceptions-1.4.0-py2.py3-none-any.whl", hash = "sha256:3083d872b6e07dc5c323563ef37671d992214ad9a32b0ca4a3d7f5500bf38ce3"}, - {file = "requestsexceptions-1.4.0.tar.gz", hash = "sha256:b095cbc77618f066d459a02b137b020c37da9f46d9b057704019c9f77dba3065"}, -] - -[[package]] -name = "responses" -version = "0.25.7" -description = "A utility library for mocking out the `requests` Python library." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "responses-0.25.7-py3-none-any.whl", hash = "sha256:92ca17416c90fe6b35921f52179bff29332076bb32694c0df02dcac2c6bc043c"}, - {file = "responses-0.25.7.tar.gz", hash = "sha256:8ebae11405d7a5df79ab6fd54277f6f2bc29b2d002d0dd2d5c632594d1ddcedb"}, -] - -[package.dependencies] -pyyaml = "*" -requests = ">=2.30.0,<3.0" -urllib3 = ">=1.25.10,<3.0" - -[package.extras] -tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "tomli ; python_version < \"3.11\"", "tomli-w", "types-PyYAML", "types-requests"] - -[[package]] -name = "retrying" -version = "1.4.1" -description = "Retrying" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "retrying-1.4.1-py3-none-any.whl", hash = "sha256:d736050c1adfc0a71fa022d9198ee130b0e66be318678a3fdd8b1b8872dc0997"}, - {file = "retrying-1.4.1.tar.gz", hash = "sha256:4d206e0ed2aff5ef2f3cd867abb9511e9e8f31127c5aca20f1d5246e476903b0"}, -] - -[[package]] -name = "rfc3339-validator" -version = "0.1.4" -description = "A pure python RFC3339 validator" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -groups = ["dev"] -files = [ - {file = "rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa"}, - {file = "rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b"}, -] - -[package.dependencies] -six = "*" - -[[package]] -name = "rich" -version = "14.1.0" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -optional = false -python-versions = ">=3.8.0" -groups = ["dev"] -files = [ - {file = "rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f"}, - {file = "rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8"}, -] - -[package.dependencies] -markdown-it-py = ">=2.2.0" -pygments = ">=2.13.0,<3.0.0" - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<9)"] - -[[package]] -name = "rpds-py" -version = "0.26.0" -description = "Python bindings to Rust's persistent data structures (rpds)" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "rpds_py-0.26.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:4c70c70f9169692b36307a95f3d8c0a9fcd79f7b4a383aad5eaa0e9718b79b37"}, - {file = "rpds_py-0.26.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:777c62479d12395bfb932944e61e915741e364c843afc3196b694db3d669fcd0"}, - {file = "rpds_py-0.26.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec671691e72dff75817386aa02d81e708b5a7ec0dec6669ec05213ff6b77e1bd"}, - {file = "rpds_py-0.26.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6a1cb5d6ce81379401bbb7f6dbe3d56de537fb8235979843f0d53bc2e9815a79"}, - {file = "rpds_py-0.26.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f789e32fa1fb6a7bf890e0124e7b42d1e60d28ebff57fe806719abb75f0e9a3"}, - {file = "rpds_py-0.26.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c55b0a669976cf258afd718de3d9ad1b7d1fe0a91cd1ab36f38b03d4d4aeaaf"}, - {file = "rpds_py-0.26.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c70d9ec912802ecfd6cd390dadb34a9578b04f9bcb8e863d0a7598ba5e9e7ccc"}, - {file = "rpds_py-0.26.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3021933c2cb7def39d927b9862292e0f4c75a13d7de70eb0ab06efed4c508c19"}, - {file = "rpds_py-0.26.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8a7898b6ca3b7d6659e55cdac825a2e58c638cbf335cde41f4619e290dd0ad11"}, - {file = "rpds_py-0.26.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:12bff2ad9447188377f1b2794772f91fe68bb4bbfa5a39d7941fbebdbf8c500f"}, - {file = "rpds_py-0.26.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:191aa858f7d4902e975d4cf2f2d9243816c91e9605070aeb09c0a800d187e323"}, - {file = "rpds_py-0.26.0-cp310-cp310-win32.whl", hash = "sha256:b37a04d9f52cb76b6b78f35109b513f6519efb481d8ca4c321f6a3b9580b3f45"}, - {file = "rpds_py-0.26.0-cp310-cp310-win_amd64.whl", hash = "sha256:38721d4c9edd3eb6670437d8d5e2070063f305bfa2d5aa4278c51cedcd508a84"}, - {file = "rpds_py-0.26.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9e8cb77286025bdb21be2941d64ac6ca016130bfdcd228739e8ab137eb4406ed"}, - {file = "rpds_py-0.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5e09330b21d98adc8ccb2dbb9fc6cb434e8908d4c119aeaa772cb1caab5440a0"}, - {file = "rpds_py-0.26.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c9c1b92b774b2e68d11193dc39620d62fd8ab33f0a3c77ecdabe19c179cdbc1"}, - {file = "rpds_py-0.26.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:824e6d3503ab990d7090768e4dfd9e840837bae057f212ff9f4f05ec6d1975e7"}, - {file = "rpds_py-0.26.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ad7fd2258228bf288f2331f0a6148ad0186b2e3643055ed0db30990e59817a6"}, - {file = "rpds_py-0.26.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0dc23bbb3e06ec1ea72d515fb572c1fea59695aefbffb106501138762e1e915e"}, - {file = "rpds_py-0.26.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d80bf832ac7b1920ee29a426cdca335f96a2b5caa839811803e999b41ba9030d"}, - {file = "rpds_py-0.26.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0919f38f5542c0a87e7b4afcafab6fd2c15386632d249e9a087498571250abe3"}, - {file = "rpds_py-0.26.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d422b945683e409000c888e384546dbab9009bb92f7c0b456e217988cf316107"}, - {file = "rpds_py-0.26.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:77a7711fa562ba2da1aa757e11024ad6d93bad6ad7ede5afb9af144623e5f76a"}, - {file = "rpds_py-0.26.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238e8c8610cb7c29460e37184f6799547f7e09e6a9bdbdab4e8edb90986a2318"}, - {file = "rpds_py-0.26.0-cp311-cp311-win32.whl", hash = "sha256:893b022bfbdf26d7bedb083efeea624e8550ca6eb98bf7fea30211ce95b9201a"}, - {file = "rpds_py-0.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:87a5531de9f71aceb8af041d72fc4cab4943648d91875ed56d2e629bef6d4c03"}, - {file = "rpds_py-0.26.0-cp311-cp311-win_arm64.whl", hash = "sha256:de2713f48c1ad57f89ac25b3cb7daed2156d8e822cf0eca9b96a6f990718cc41"}, - {file = "rpds_py-0.26.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:894514d47e012e794f1350f076c427d2347ebf82f9b958d554d12819849a369d"}, - {file = "rpds_py-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc921b96fa95a097add244da36a1d9e4f3039160d1d30f1b35837bf108c21136"}, - {file = "rpds_py-0.26.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e1157659470aa42a75448b6e943c895be8c70531c43cb78b9ba990778955582"}, - {file = "rpds_py-0.26.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:521ccf56f45bb3a791182dc6b88ae5f8fa079dd705ee42138c76deb1238e554e"}, - {file = "rpds_py-0.26.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9def736773fd56b305c0eef698be5192c77bfa30d55a0e5885f80126c4831a15"}, - {file = "rpds_py-0.26.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdad4ea3b4513b475e027be79e5a0ceac8ee1c113a1a11e5edc3c30c29f964d8"}, - {file = "rpds_py-0.26.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82b165b07f416bdccf5c84546a484cc8f15137ca38325403864bfdf2b5b72f6a"}, - {file = "rpds_py-0.26.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d04cab0a54b9dba4d278fe955a1390da3cf71f57feb78ddc7cb67cbe0bd30323"}, - {file = "rpds_py-0.26.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:79061ba1a11b6a12743a2b0f72a46aa2758613d454aa6ba4f5a265cc48850158"}, - {file = "rpds_py-0.26.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f405c93675d8d4c5ac87364bb38d06c988e11028a64b52a47158a355079661f3"}, - {file = "rpds_py-0.26.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dafd4c44b74aa4bed4b250f1aed165b8ef5de743bcca3b88fc9619b6087093d2"}, - {file = "rpds_py-0.26.0-cp312-cp312-win32.whl", hash = "sha256:3da5852aad63fa0c6f836f3359647870e21ea96cf433eb393ffa45263a170d44"}, - {file = "rpds_py-0.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf47cfdabc2194a669dcf7a8dbba62e37a04c5041d2125fae0233b720da6f05c"}, - {file = "rpds_py-0.26.0-cp312-cp312-win_arm64.whl", hash = "sha256:20ab1ae4fa534f73647aad289003f1104092890849e0266271351922ed5574f8"}, - {file = "rpds_py-0.26.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:696764a5be111b036256c0b18cd29783fab22154690fc698062fc1b0084b511d"}, - {file = "rpds_py-0.26.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e6c15d2080a63aaed876e228efe4f814bc7889c63b1e112ad46fdc8b368b9e1"}, - {file = "rpds_py-0.26.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:390e3170babf42462739a93321e657444f0862c6d722a291accc46f9d21ed04e"}, - {file = "rpds_py-0.26.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7da84c2c74c0f5bc97d853d9e17bb83e2dcafcff0dc48286916001cc114379a1"}, - {file = "rpds_py-0.26.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c5fe114a6dd480a510b6d3661d09d67d1622c4bf20660a474507aaee7eeeee9"}, - {file = "rpds_py-0.26.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3100b3090269f3a7ea727b06a6080d4eb7439dca4c0e91a07c5d133bb1727ea7"}, - {file = "rpds_py-0.26.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c03c9b0c64afd0320ae57de4c982801271c0c211aa2d37f3003ff5feb75bb04"}, - {file = "rpds_py-0.26.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5963b72ccd199ade6ee493723d18a3f21ba7d5b957017607f815788cef50eaf1"}, - {file = "rpds_py-0.26.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9da4e873860ad5bab3291438525cae80169daecbfafe5657f7f5fb4d6b3f96b9"}, - {file = "rpds_py-0.26.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5afaddaa8e8c7f1f7b4c5c725c0070b6eed0228f705b90a1732a48e84350f4e9"}, - {file = "rpds_py-0.26.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4916dc96489616a6f9667e7526af8fa693c0fdb4f3acb0e5d9f4400eb06a47ba"}, - {file = "rpds_py-0.26.0-cp313-cp313-win32.whl", hash = "sha256:2a343f91b17097c546b93f7999976fd6c9d5900617aa848c81d794e062ab302b"}, - {file = "rpds_py-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:0a0b60701f2300c81b2ac88a5fb893ccfa408e1c4a555a77f908a2596eb875a5"}, - {file = "rpds_py-0.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:257d011919f133a4746958257f2c75238e3ff54255acd5e3e11f3ff41fd14256"}, - {file = "rpds_py-0.26.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:529c8156d7506fba5740e05da8795688f87119cce330c244519cf706a4a3d618"}, - {file = "rpds_py-0.26.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f53ec51f9d24e9638a40cabb95078ade8c99251945dad8d57bf4aabe86ecee35"}, - {file = "rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab504c4d654e4a29558eaa5bb8cea5fdc1703ea60a8099ffd9c758472cf913f"}, - {file = "rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd0641abca296bc1a00183fe44f7fced8807ed49d501f188faa642d0e4975b83"}, - {file = "rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b312fecc1d017b5327afa81d4da1480f51c68810963a7336d92203dbb3d4f1"}, - {file = "rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c741107203954f6fc34d3066d213d0a0c40f7bb5aafd698fb39888af277c70d8"}, - {file = "rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc3e55a7db08dc9a6ed5fb7103019d2c1a38a349ac41901f9f66d7f95750942f"}, - {file = "rpds_py-0.26.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9e851920caab2dbcae311fd28f4313c6953993893eb5c1bb367ec69d9a39e7ed"}, - {file = "rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dfbf280da5f876d0b00c81f26bedce274e72a678c28845453885a9b3c22ae632"}, - {file = "rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1cc81d14ddfa53d7f3906694d35d54d9d3f850ef8e4e99ee68bc0d1e5fed9a9c"}, - {file = "rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dca83c498b4650a91efcf7b88d669b170256bf8017a5db6f3e06c2bf031f57e0"}, - {file = "rpds_py-0.26.0-cp313-cp313t-win32.whl", hash = "sha256:4d11382bcaf12f80b51d790dee295c56a159633a8e81e6323b16e55d81ae37e9"}, - {file = "rpds_py-0.26.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff110acded3c22c033e637dd8896e411c7d3a11289b2edf041f86663dbc791e9"}, - {file = "rpds_py-0.26.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:da619979df60a940cd434084355c514c25cf8eb4cf9a508510682f6c851a4f7a"}, - {file = "rpds_py-0.26.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ea89a2458a1a75f87caabefe789c87539ea4e43b40f18cff526052e35bbb4fdf"}, - {file = "rpds_py-0.26.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feac1045b3327a45944e7dcbeb57530339f6b17baff154df51ef8b0da34c8c12"}, - {file = "rpds_py-0.26.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b818a592bd69bfe437ee8368603d4a2d928c34cffcdf77c2e761a759ffd17d20"}, - {file = "rpds_py-0.26.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a8b0dd8648709b62d9372fc00a57466f5fdeefed666afe3fea5a6c9539a0331"}, - {file = "rpds_py-0.26.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d3498ad0df07d81112aa6ec6c95a7e7b1ae00929fb73e7ebee0f3faaeabad2f"}, - {file = "rpds_py-0.26.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24a4146ccb15be237fdef10f331c568e1b0e505f8c8c9ed5d67759dac58ac246"}, - {file = "rpds_py-0.26.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a9a63785467b2d73635957d32a4f6e73d5e4df497a16a6392fa066b753e87387"}, - {file = "rpds_py-0.26.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:de4ed93a8c91debfd5a047be327b7cc8b0cc6afe32a716bbbc4aedca9e2a83af"}, - {file = "rpds_py-0.26.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:caf51943715b12af827696ec395bfa68f090a4c1a1d2509eb4e2cb69abbbdb33"}, - {file = "rpds_py-0.26.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4a59e5bc386de021f56337f757301b337d7ab58baa40174fb150accd480bc953"}, - {file = "rpds_py-0.26.0-cp314-cp314-win32.whl", hash = "sha256:92c8db839367ef16a662478f0a2fe13e15f2227da3c1430a782ad0f6ee009ec9"}, - {file = "rpds_py-0.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:b0afb8cdd034150d4d9f53926226ed27ad15b7f465e93d7468caaf5eafae0d37"}, - {file = "rpds_py-0.26.0-cp314-cp314-win_arm64.whl", hash = "sha256:ca3f059f4ba485d90c8dc75cb5ca897e15325e4e609812ce57f896607c1c0867"}, - {file = "rpds_py-0.26.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5afea17ab3a126006dc2f293b14ffc7ef3c85336cf451564a0515ed7648033da"}, - {file = "rpds_py-0.26.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:69f0c0a3df7fd3a7eec50a00396104bb9a843ea6d45fcc31c2d5243446ffd7a7"}, - {file = "rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:801a71f70f9813e82d2513c9a96532551fce1e278ec0c64610992c49c04c2dad"}, - {file = "rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df52098cde6d5e02fa75c1f6244f07971773adb4a26625edd5c18fee906fa84d"}, - {file = "rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bc596b30f86dc6f0929499c9e574601679d0341a0108c25b9b358a042f51bca"}, - {file = "rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9dfbe56b299cf5875b68eb6f0ebaadc9cac520a1989cac0db0765abfb3709c19"}, - {file = "rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac64f4b2bdb4ea622175c9ab7cf09444e412e22c0e02e906978b3b488af5fde8"}, - {file = "rpds_py-0.26.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:181ef9b6bbf9845a264f9aa45c31836e9f3c1f13be565d0d010e964c661d1e2b"}, - {file = "rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:49028aa684c144ea502a8e847d23aed5e4c2ef7cadfa7d5eaafcb40864844b7a"}, - {file = "rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e5d524d68a474a9688336045bbf76cb0def88549c1b2ad9dbfec1fb7cfbe9170"}, - {file = "rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c1851f429b822831bd2edcbe0cfd12ee9ea77868f8d3daf267b189371671c80e"}, - {file = "rpds_py-0.26.0-cp314-cp314t-win32.whl", hash = "sha256:7bdb17009696214c3b66bb3590c6d62e14ac5935e53e929bcdbc5a495987a84f"}, - {file = "rpds_py-0.26.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f14440b9573a6f76b4ee4770c13f0b5921f71dde3b6fcb8dabbefd13b7fe05d7"}, - {file = "rpds_py-0.26.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:7a48af25d9b3c15684059d0d1fc0bc30e8eee5ca521030e2bffddcab5be40226"}, - {file = "rpds_py-0.26.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0c71c2f6bf36e61ee5c47b2b9b5d47e4d1baad6426bfed9eea3e858fc6ee8806"}, - {file = "rpds_py-0.26.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d815d48b1804ed7867b539236b6dd62997850ca1c91cad187f2ddb1b7bbef19"}, - {file = "rpds_py-0.26.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:84cfbd4d4d2cdeb2be61a057a258d26b22877266dd905809e94172dff01a42ae"}, - {file = "rpds_py-0.26.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fbaa70553ca116c77717f513e08815aec458e6b69a028d4028d403b3bc84ff37"}, - {file = "rpds_py-0.26.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39bfea47c375f379d8e87ab4bb9eb2c836e4f2069f0f65731d85e55d74666387"}, - {file = "rpds_py-0.26.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1533b7eb683fb5f38c1d68a3c78f5fdd8f1412fa6b9bf03b40f450785a0ab915"}, - {file = "rpds_py-0.26.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c5ab0ee51f560d179b057555b4f601b7df909ed31312d301b99f8b9fc6028284"}, - {file = "rpds_py-0.26.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e5162afc9e0d1f9cae3b577d9c29ddbab3505ab39012cb794d94a005825bde21"}, - {file = "rpds_py-0.26.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:43f10b007033f359bc3fa9cd5e6c1e76723f056ffa9a6b5c117cc35720a80292"}, - {file = "rpds_py-0.26.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e3730a48e5622e598293eee0762b09cff34dd3f271530f47b0894891281f051d"}, - {file = "rpds_py-0.26.0-cp39-cp39-win32.whl", hash = "sha256:4b1f66eb81eab2e0ff5775a3a312e5e2e16bf758f7b06be82fb0d04078c7ac51"}, - {file = "rpds_py-0.26.0-cp39-cp39-win_amd64.whl", hash = "sha256:519067e29f67b5c90e64fb1a6b6e9d2ec0ba28705c51956637bac23a2f4ddae1"}, - {file = "rpds_py-0.26.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3c0909c5234543ada2515c05dc08595b08d621ba919629e94427e8e03539c958"}, - {file = "rpds_py-0.26.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c1fb0cda2abcc0ac62f64e2ea4b4e64c57dfd6b885e693095460c61bde7bb18e"}, - {file = "rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84d142d2d6cf9b31c12aa4878d82ed3b2324226270b89b676ac62ccd7df52d08"}, - {file = "rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a547e21c5610b7e9093d870be50682a6a6cf180d6da0f42c47c306073bfdbbf6"}, - {file = "rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:35e9a70a0f335371275cdcd08bc5b8051ac494dd58bff3bbfb421038220dc871"}, - {file = "rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0dfa6115c6def37905344d56fb54c03afc49104e2ca473d5dedec0f6606913b4"}, - {file = "rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:313cfcd6af1a55a286a3c9a25f64af6d0e46cf60bc5798f1db152d97a216ff6f"}, - {file = "rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f7bf2496fa563c046d05e4d232d7b7fd61346e2402052064b773e5c378bf6f73"}, - {file = "rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:aa81873e2c8c5aa616ab8e017a481a96742fdf9313c40f14338ca7dbf50cb55f"}, - {file = "rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:68ffcf982715f5b5b7686bdd349ff75d422e8f22551000c24b30eaa1b7f7ae84"}, - {file = "rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6188de70e190847bb6db3dc3981cbadff87d27d6fe9b4f0e18726d55795cee9b"}, - {file = "rpds_py-0.26.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1c962145c7473723df9722ba4c058de12eb5ebedcb4e27e7d902920aa3831ee8"}, - {file = "rpds_py-0.26.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f61a9326f80ca59214d1cceb0a09bb2ece5b2563d4e0cd37bfd5515c28510674"}, - {file = "rpds_py-0.26.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:183f857a53bcf4b1b42ef0f57ca553ab56bdd170e49d8091e96c51c3d69ca696"}, - {file = "rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:941c1cfdf4799d623cf3aa1d326a6b4fdb7a5799ee2687f3516738216d2262fb"}, - {file = "rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72a8d9564a717ee291f554eeb4bfeafe2309d5ec0aa6c475170bdab0f9ee8e88"}, - {file = "rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:511d15193cbe013619dd05414c35a7dedf2088fcee93c6bbb7c77859765bd4e8"}, - {file = "rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aea1f9741b603a8d8fedb0ed5502c2bc0accbc51f43e2ad1337fe7259c2b77a5"}, - {file = "rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4019a9d473c708cf2f16415688ef0b4639e07abaa569d72f74745bbeffafa2c7"}, - {file = "rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:093d63b4b0f52d98ebae33b8c50900d3d67e0666094b1be7a12fffd7f65de74b"}, - {file = "rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2abe21d8ba64cded53a2a677e149ceb76dcf44284202d737178afe7ba540c1eb"}, - {file = "rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:4feb7511c29f8442cbbc28149a92093d32e815a28aa2c50d333826ad2a20fdf0"}, - {file = "rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e99685fc95d386da368013e7fb4269dd39c30d99f812a8372d62f244f662709c"}, - {file = "rpds_py-0.26.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a90a13408a7a856b87be8a9f008fff53c5080eea4e4180f6c2e546e4a972fb5d"}, - {file = "rpds_py-0.26.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3ac51b65e8dc76cf4949419c54c5528adb24fc721df722fd452e5fbc236f5c40"}, - {file = "rpds_py-0.26.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59b2093224a18c6508d95cfdeba8db9cbfd6f3494e94793b58972933fcee4c6d"}, - {file = "rpds_py-0.26.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4f01a5d6444a3258b00dc07b6ea4733e26f8072b788bef750baa37b370266137"}, - {file = "rpds_py-0.26.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6e2c12160c72aeda9d1283e612f68804621f448145a210f1bf1d79151c47090"}, - {file = "rpds_py-0.26.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cb28c1f569f8d33b2b5dcd05d0e6ef7005d8639c54c2f0be824f05aedf715255"}, - {file = "rpds_py-0.26.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1766b5724c3f779317d5321664a343c07773c8c5fd1532e4039e6cc7d1a815be"}, - {file = "rpds_py-0.26.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b6d9e5a2ed9c4988c8f9b28b3bc0e3e5b1aaa10c28d210a594ff3a8c02742daf"}, - {file = "rpds_py-0.26.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:b5f7a446ddaf6ca0fad9a5535b56fbfc29998bf0e0b450d174bbec0d600e1d72"}, - {file = "rpds_py-0.26.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:eed5ac260dd545fbc20da5f4f15e7efe36a55e0e7cf706e4ec005b491a9546a0"}, - {file = "rpds_py-0.26.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:582462833ba7cee52e968b0341b85e392ae53d44c0f9af6a5927c80e539a8b67"}, - {file = "rpds_py-0.26.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:69a607203441e07e9a8a529cff1d5b73f6a160f22db1097211e6212a68567d11"}, - {file = "rpds_py-0.26.0.tar.gz", hash = "sha256:20dae58a859b0906f0685642e591056f1e787f3a8b39c8e8749a45dc7d26bdb0"}, -] - -[[package]] -name = "rsa" -version = "4.9.1" -description = "Pure-Python RSA implementation" -optional = false -python-versions = "<4,>=3.6" -groups = ["main"] -files = [ - {file = "rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762"}, - {file = "rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75"}, -] - -[package.dependencies] -pyasn1 = ">=0.1.3" - -[[package]] -name = "ruamel-yaml" -version = "0.18.14" -description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "ruamel.yaml-0.18.14-py3-none-any.whl", hash = "sha256:710ff198bb53da66718c7db27eec4fbcc9aa6ca7204e4c1df2f282b6fe5eb6b2"}, - {file = "ruamel.yaml-0.18.14.tar.gz", hash = "sha256:7227b76aaec364df15936730efbf7d72b30c0b79b1d578bbb8e3dcb2d81f52b7"}, -] - -[package.dependencies] -"ruamel.yaml.clib" = {version = ">=0.2.7", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.14\""} - -[package.extras] -docs = ["mercurial (>5.7)", "ryd"] -jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] - -[[package]] -name = "ruamel-yaml-clib" -version = "0.2.12" -description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -markers = "platform_python_implementation == \"CPython\"" -files = [ - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:11f891336688faf5156a36293a9c362bdc7c88f03a8a027c2c1d8e0bcde998e5"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:a606ef75a60ecf3d924613892cc603b154178ee25abb3055db5062da811fd969"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd5415dded15c3822597455bc02bcd66e81ef8b7a48cb71a33628fc9fdde39df"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:d84318609196d6bd6da0edfa25cedfbabd8dbde5140a0a23af29ad4b8f91fb1e"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb43a269eb827806502c7c8efb7ae7e9e9d0573257a46e8e952f4d4caba4f31e"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:943f32bc9dedb3abff9879edc134901df92cfce2c3d5c9348f172f62eb2d771d"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95c3829bb364fdb8e0332c9931ecf57d9be3519241323c5274bd82f709cebc0c"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:e7e3736715fbf53e9be2a79eb4db68e4ed857017344d697e8b9749444ae57475"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7e75b4965e1d4690e93021adfcecccbca7d61c7bddd8e22406ef2ff20d74ef"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bc5f1e1c28e966d61d2519f2a3d451ba989f9ea0f2307de7bc45baa526de9e45"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a0e060aace4c24dcaf71023bbd7d42674e3b230f7e7b97317baf1e953e5b519"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2c59aa6170b990d8d2719323e628aaf36f3bfbc1c26279c0eeeb24d05d2d11c7"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win32.whl", hash = "sha256:beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win_amd64.whl", hash = "sha256:040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b"}, - {file = "ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f"}, -] - -[[package]] -name = "s3transfer" -version = "0.14.0" -description = "An Amazon S3 Transfer Manager" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "s3transfer-0.14.0-py3-none-any.whl", hash = "sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456"}, - {file = "s3transfer-0.14.0.tar.gz", hash = "sha256:eff12264e7c8b4985074ccce27a3b38a485bb7f7422cc8046fee9be4983e4125"}, -] - -[package.dependencies] -botocore = ">=1.37.4,<2.0a0" - -[package.extras] -crt = ["botocore[crt] (>=1.37.4,<2.0a0)"] - -[[package]] -name = "safety" -version = "3.7.0" -description = "Scan dependencies for known vulnerabilities and licenses." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "safety-3.7.0-py3-none-any.whl", hash = "sha256:65e71db45eb832e8840e3456333d44c23927423753d5610596a09e909a66d2bf"}, - {file = "safety-3.7.0.tar.gz", hash = "sha256:daec15a393cafc32b846b7ef93f9c952a1708863e242341ab5bde2e4beabb54e"}, -] - -[package.dependencies] -authlib = ">=1.2.0" -click = ">=8.0.2" -dparse = ">=0.6.4" -filelock = ">=3.16.1,<4.0" -httpx = "*" -jinja2 = ">=3.1.0" -marshmallow = ">=3.15.0" -nltk = ">=3.9" -packaging = ">=21.0" -pydantic = ">=2.6.0" -requests = "*" -ruamel-yaml = ">=0.17.21" -safety-schemas = "0.0.16" -tenacity = ">=8.1.0" -tomli = {version = "*", markers = "python_version < \"3.11\""} -tomlkit = "*" -typer = ">=0.16.0" -typing-extensions = ">=4.7.1" - -[package.extras] -github = ["pygithub (>=1.43.3)"] -gitlab = ["python-gitlab (>=1.3.0)"] -spdx = ["spdx-tools (>=0.8.2)"] - -[[package]] -name = "safety-schemas" -version = "0.0.16" -description = "Schemas for Safety tools" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "safety_schemas-0.0.16-py3-none-any.whl", hash = "sha256:6760515d3fd1e6535b251cd73014bd431d12fe0bfb8b6e8880a9379b5ab7aa44"}, - {file = "safety_schemas-0.0.16.tar.gz", hash = "sha256:3bb04d11bd4b5cc79f9fa183c658a6a8cf827a9ceec443a5ffa6eed38a50a24e"}, -] - -[package.dependencies] -dparse = ">=0.6.4" -packaging = ">=21.0" -pydantic = ">=2.6.0" -ruamel-yaml = ">=0.17.21" -typing-extensions = ">=4.7.1" - -[[package]] -name = "schema" -version = "0.7.5" -description = "Simple data validation library" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "schema-0.7.5-py2.py3-none-any.whl", hash = "sha256:f3ffdeeada09ec34bf40d7d79996d9f7175db93b7a5065de0faa7f41083c1e6c"}, - {file = "schema-0.7.5.tar.gz", hash = "sha256:f06717112c61895cabc4707752b88716e8420a8819d71404501e114f91043197"}, -] - -[package.dependencies] -contextlib2 = ">=0.5.5" - -[[package]] -name = "setuptools" -version = "80.9.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}, - {file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] -core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] - -[[package]] -name = "shellingham" -version = "1.5.4" -description = "Tool to Detect Surrounding Shell" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, - {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, -] - -[[package]] -name = "shodan" -version = "1.31.0" -description = "Python library and command-line utility for Shodan (https://developer.shodan.io)" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "shodan-1.31.0.tar.gz", hash = "sha256:c73275386ea02390e196c35c660706a28dd4d537c5a21eb387ab6236fac251f6"}, -] - -[package.dependencies] -click = "*" -click-plugins = "*" -colorama = "*" -requests = ">=2.2.1" -tldextract = "*" -XlsxWriter = "*" - -[[package]] -name = "six" -version = "1.17.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main", "dev"] -files = [ - {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, - {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, -] - -[[package]] -name = "slack-sdk" -version = "3.39.0" -description = "The Slack API Platform SDK for Python" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "slack_sdk-3.39.0-py2.py3-none-any.whl", hash = "sha256:b1556b2f5b8b12b94e5ea3f56c4f2c7f04462e4e1013d325c5764ff118044fa8"}, - {file = "slack_sdk-3.39.0.tar.gz", hash = "sha256:6a56be10dc155c436ff658c6b776e1c082e29eae6a771fccf8b0a235822bbcb1"}, -] - -[package.extras] -optional = ["SQLAlchemy (>=1.4,<3)", "aiodns (>1.0)", "aiohttp (>=3.7.3,<4)", "boto3 (<=2)", "websocket-client (>=1,<2)", "websockets (>=9.1,<16)"] - -[[package]] -name = "sniffio" -version = "1.3.1" -description = "Sniff out which async library your code is running under" -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, -] - -[[package]] -name = "std-uritemplate" -version = "2.0.5" -description = "std-uritemplate implementation for Python" -optional = false -python-versions = "<4.0,>=3.8" -groups = ["main"] -files = [ - {file = "std_uritemplate-2.0.5-py3-none-any.whl", hash = "sha256:0f5184f8e6f315a01f92cfbed335f62f087e453e79cd586b67a724211e686c28"}, - {file = "std_uritemplate-2.0.5.tar.gz", hash = "sha256:7703a886cce59d155c21b5acf1ad8d48db9f3322de98fa783a8396fbf35cbc06"}, -] - -[[package]] -name = "stevedore" -version = "5.4.1" -description = "Manage dynamic plugins for Python applications" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "stevedore-5.4.1-py3-none-any.whl", hash = "sha256:d10a31c7b86cba16c1f6e8d15416955fc797052351a56af15e608ad20811fcfe"}, - {file = "stevedore-5.4.1.tar.gz", hash = "sha256:3135b5ae50fe12816ef291baff420acb727fcd356106e3e9cbfa9e5985cd6f4b"}, -] - -[package.dependencies] -pbr = ">=2.0.0" - -[[package]] -name = "sympy" -version = "1.14.0" -description = "Computer algebra system (CAS) in Python" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5"}, - {file = "sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517"}, -] - -[package.dependencies] -mpmath = ">=1.1.0,<1.4" - -[package.extras] -dev = ["hypothesis (>=6.70.0)", "pytest (>=7.1.0)"] - -[[package]] -name = "tabulate" -version = "0.9.0" -description = "Pretty-print tabular data" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"}, - {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"}, -] - -[package.extras] -widechars = ["wcwidth"] - -[[package]] -name = "tenacity" -version = "9.1.2" -description = "Retry code until it succeeds" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138"}, - {file = "tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb"}, -] - -[package.extras] -doc = ["reno", "sphinx"] -test = ["pytest", "tornado (>=4.5)", "typeguard"] - -[[package]] -name = "tldextract" -version = "5.3.0" -description = "Accurately separates a URL's subdomain, domain, and public suffix, using the Public Suffix List (PSL). By default, this includes the public ICANN TLDs and their exceptions. You can optionally support the Public Suffix List's private domains as well." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "tldextract-5.3.0-py3-none-any.whl", hash = "sha256:f70f31d10b55c83993f55e91ecb7c5d84532a8972f22ec578ecfbe5ea2292db2"}, - {file = "tldextract-5.3.0.tar.gz", hash = "sha256:b3d2b70a1594a0ecfa6967d57251527d58e00bb5a91a74387baa0d87a0678609"}, -] - -[package.dependencies] -filelock = ">=3.0.8" -idna = "*" -requests = ">=2.1.0" -requests-file = ">=1.4" - -[package.extras] -release = ["build", "twine"] -testing = ["mypy", "pytest", "pytest-gitignore", "pytest-mock", "responses", "ruff", "syrupy", "tox", "tox-uv", "types-filelock", "types-requests"] - -[[package]] -name = "tomli" -version = "2.2.1" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -markers = "python_version == \"3.10\"" -files = [ - {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, - {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, - {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, - {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, - {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, - {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, - {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, - {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, - {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, - {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, -] - -[[package]] -name = "tomlkit" -version = "0.13.3" -description = "Style preserving TOML library" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0"}, - {file = "tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1"}, -] - -[[package]] -name = "tqdm" -version = "4.67.1" -description = "Fast, Extensible Progress Meter" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, - {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[package.extras] -dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"] -discord = ["requests"] -notebook = ["ipywidgets (>=6)"] -slack = ["slack-sdk"] -telegram = ["requests"] - -[[package]] -name = "typer" -version = "0.16.0" -description = "Typer, build great CLIs. Easy to code. Based on Python type hints." -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855"}, - {file = "typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b"}, -] - -[package.dependencies] -click = ">=8.0.0" -rich = ">=10.11.0" -shellingham = ">=1.3.0" -typing-extensions = ">=3.7.4.3" - -[[package]] -name = "typing-extensions" -version = "4.14.1" -description = "Backported and Experimental Type Hints for Python 3.9+" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"}, - {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"}, -] - -[[package]] -name = "typing-inspection" -version = "0.4.2" -description = "Runtime typing introspection tools" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, - {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, -] - -[package.dependencies] -typing-extensions = ">=4.12.0" - -[[package]] -name = "tzdata" -version = "2025.2" -description = "Provider of IANA time zone data" -optional = false -python-versions = ">=2" -groups = ["main"] -files = [ - {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, - {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, -] - -[[package]] -name = "tzlocal" -version = "5.3.1" -description = "tzinfo object for the local timezone" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d"}, - {file = "tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd"}, -] - -[package.dependencies] -tzdata = {version = "*", markers = "platform_system == \"Windows\""} - -[package.extras] -devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] - -[[package]] -name = "uritemplate" -version = "4.2.0" -description = "Implementation of RFC 6570 URI Templates" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686"}, - {file = "uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e"}, -] - -[[package]] -name = "urllib3" -version = "2.6.3" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, - {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, -] - -[package.extras] -brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] - -[[package]] -name = "uuid6" -version = "2024.7.10" -description = "New time-based UUID formats which are suited for use as a database key" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "uuid6-2024.7.10-py3-none-any.whl", hash = "sha256:93432c00ba403751f722829ad21759ff9db051dea140bf81493271e8e4dd18b7"}, - {file = "uuid6-2024.7.10.tar.gz", hash = "sha256:2d29d7f63f593caaeea0e0d0dd0ad8129c9c663b29e19bdf882e864bedf18fb0"}, -] - -[[package]] -name = "vulture" -version = "2.14" -description = "Find dead code" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "vulture-2.14-py2.py3-none-any.whl", hash = "sha256:d9a90dba89607489548a49d557f8bac8112bd25d3cbc8aeef23e860811bd5ed9"}, - {file = "vulture-2.14.tar.gz", hash = "sha256:cb8277902a1138deeab796ec5bef7076a6e0248ca3607a3f3dee0b6d9e9b8415"}, -] - -[package.dependencies] -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} - -[[package]] -name = "websocket-client" -version = "1.8.0" -description = "WebSocket client for Python with low level API options" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526"}, - {file = "websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da"}, -] - -[package.extras] -docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx-rtd-theme (>=1.1.0)"] -optional = ["python-socks", "wsaccel"] -test = ["websockets"] - -[[package]] -name = "werkzeug" -version = "3.1.5" -description = "The comprehensive WSGI web application library." -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc"}, - {file = "werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67"}, -] - -[package.dependencies] -markupsafe = ">=2.1.1" - -[package.extras] -watchdog = ["watchdog (>=2.3)"] - -[[package]] -name = "wrapt" -version = "1.17.2" -description = "Module for decorators, wrappers and monkey patching." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984"}, - {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22"}, - {file = "wrapt-1.17.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80dd7db6a7cb57ffbc279c4394246414ec99537ae81ffd702443335a61dbf3a7"}, - {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a6e821770cf99cc586d33833b2ff32faebdbe886bd6322395606cf55153246c"}, - {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b60fb58b90c6d63779cb0c0c54eeb38941bae3ecf7a73c764c52c88c2dcb9d72"}, - {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b870b5df5b71d8c3359d21be8f0d6c485fa0ebdb6477dda51a1ea54a9b558061"}, - {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4011d137b9955791f9084749cba9a367c68d50ab8d11d64c50ba1688c9b457f2"}, - {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1473400e5b2733e58b396a04eb7f35f541e1fb976d0c0724d0223dd607e0f74c"}, - {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3cedbfa9c940fdad3e6e941db7138e26ce8aad38ab5fe9dcfadfed9db7a54e62"}, - {file = "wrapt-1.17.2-cp310-cp310-win32.whl", hash = "sha256:582530701bff1dec6779efa00c516496968edd851fba224fbd86e46cc6b73563"}, - {file = "wrapt-1.17.2-cp310-cp310-win_amd64.whl", hash = "sha256:58705da316756681ad3c9c73fd15499aa4d8c69f9fd38dc8a35e06c12468582f"}, - {file = "wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58"}, - {file = "wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda"}, - {file = "wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438"}, - {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a"}, - {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000"}, - {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6"}, - {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b"}, - {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662"}, - {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72"}, - {file = "wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317"}, - {file = "wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3"}, - {file = "wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925"}, - {file = "wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392"}, - {file = "wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40"}, - {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d"}, - {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b"}, - {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98"}, - {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82"}, - {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae"}, - {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9"}, - {file = "wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9"}, - {file = "wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991"}, - {file = "wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125"}, - {file = "wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998"}, - {file = "wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5"}, - {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8"}, - {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6"}, - {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc"}, - {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2"}, - {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b"}, - {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504"}, - {file = "wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a"}, - {file = "wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845"}, - {file = "wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192"}, - {file = "wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b"}, - {file = "wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0"}, - {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306"}, - {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb"}, - {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681"}, - {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6"}, - {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6"}, - {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f"}, - {file = "wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555"}, - {file = "wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c"}, - {file = "wrapt-1.17.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5c803c401ea1c1c18de70a06a6f79fcc9c5acfc79133e9869e730ad7f8ad8ef9"}, - {file = "wrapt-1.17.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f917c1180fdb8623c2b75a99192f4025e412597c50b2ac870f156de8fb101119"}, - {file = "wrapt-1.17.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ecc840861360ba9d176d413a5489b9a0aff6d6303d7e733e2c4623cfa26904a6"}, - {file = "wrapt-1.17.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb87745b2e6dc56361bfde481d5a378dc314b252a98d7dd19a651a3fa58f24a9"}, - {file = "wrapt-1.17.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58455b79ec2661c3600e65c0a716955adc2410f7383755d537584b0de41b1d8a"}, - {file = "wrapt-1.17.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4e42a40a5e164cbfdb7b386c966a588b1047558a990981ace551ed7e12ca9c2"}, - {file = "wrapt-1.17.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:91bd7d1773e64019f9288b7a5101f3ae50d3d8e6b1de7edee9c2ccc1d32f0c0a"}, - {file = "wrapt-1.17.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:bb90fb8bda722a1b9d48ac1e6c38f923ea757b3baf8ebd0c82e09c5c1a0e7a04"}, - {file = "wrapt-1.17.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:08e7ce672e35efa54c5024936e559469436f8b8096253404faeb54d2a878416f"}, - {file = "wrapt-1.17.2-cp38-cp38-win32.whl", hash = "sha256:410a92fefd2e0e10d26210e1dfb4a876ddaf8439ef60d6434f21ef8d87efc5b7"}, - {file = "wrapt-1.17.2-cp38-cp38-win_amd64.whl", hash = "sha256:95c658736ec15602da0ed73f312d410117723914a5c91a14ee4cdd72f1d790b3"}, - {file = "wrapt-1.17.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99039fa9e6306880572915728d7f6c24a86ec57b0a83f6b2491e1d8ab0235b9a"}, - {file = "wrapt-1.17.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2696993ee1eebd20b8e4ee4356483c4cb696066ddc24bd70bcbb80fa56ff9061"}, - {file = "wrapt-1.17.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:612dff5db80beef9e649c6d803a8d50c409082f1fedc9dbcdfde2983b2025b82"}, - {file = "wrapt-1.17.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c2caa1585c82b3f7a7ab56afef7b3602021d6da34fbc1cf234ff139fed3cd9"}, - {file = "wrapt-1.17.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c958bcfd59bacc2d0249dcfe575e71da54f9dcf4a8bdf89c4cb9a68a1170d73f"}, - {file = "wrapt-1.17.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc78a84e2dfbc27afe4b2bd7c80c8db9bca75cc5b85df52bfe634596a1da846b"}, - {file = "wrapt-1.17.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ba0f0eb61ef00ea10e00eb53a9129501f52385c44853dbd6c4ad3f403603083f"}, - {file = "wrapt-1.17.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1e1fe0e6ab7775fd842bc39e86f6dcfc4507ab0ffe206093e76d61cde37225c8"}, - {file = "wrapt-1.17.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c86563182421896d73858e08e1db93afdd2b947a70064b813d515d66549e15f9"}, - {file = "wrapt-1.17.2-cp39-cp39-win32.whl", hash = "sha256:f393cda562f79828f38a819f4788641ac7c4085f30f1ce1a68672baa686482bb"}, - {file = "wrapt-1.17.2-cp39-cp39-win_amd64.whl", hash = "sha256:36ccae62f64235cf8ddb682073a60519426fdd4725524ae38874adf72b5f2aeb"}, - {file = "wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8"}, - {file = "wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3"}, -] - -[[package]] -name = "xlsxwriter" -version = "3.2.5" -description = "A Python module for creating Excel XLSX files." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "xlsxwriter-3.2.5-py3-none-any.whl", hash = "sha256:4f4824234e1eaf9d95df9a8fe974585ff91d0f5e3d3f12ace5b71e443c1c6abd"}, - {file = "xlsxwriter-3.2.5.tar.gz", hash = "sha256:7e88469d607cdc920151c0ab3ce9cf1a83992d4b7bc730c5ffdd1a12115a7dbe"}, -] - -[[package]] -name = "xmltodict" -version = "0.14.2" -description = "Makes working with XML feel like you are working with JSON" -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "xmltodict-0.14.2-py2.py3-none-any.whl", hash = "sha256:20cc7d723ed729276e808f26fb6b3599f786cbc37e06c65e192ba77c40f20aac"}, - {file = "xmltodict-0.14.2.tar.gz", hash = "sha256:201e7c28bb210e374999d1dde6382923ab0ed1a8a5faeece48ab525b7810a553"}, -] - -[[package]] -name = "yarl" -version = "1.20.1" -description = "Yet another URL library" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "yarl-1.20.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6032e6da6abd41e4acda34d75a816012717000fa6839f37124a47fcefc49bec4"}, - {file = "yarl-1.20.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2c7b34d804b8cf9b214f05015c4fee2ebe7ed05cf581e7192c06555c71f4446a"}, - {file = "yarl-1.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0c869f2651cc77465f6cd01d938d91a11d9ea5d798738c1dc077f3de0b5e5fed"}, - {file = "yarl-1.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62915e6688eb4d180d93840cda4110995ad50c459bf931b8b3775b37c264af1e"}, - {file = "yarl-1.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:41ebd28167bc6af8abb97fec1a399f412eec5fd61a3ccbe2305a18b84fb4ca73"}, - {file = "yarl-1.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21242b4288a6d56f04ea193adde174b7e347ac46ce6bc84989ff7c1b1ecea84e"}, - {file = "yarl-1.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bea21cdae6c7eb02ba02a475f37463abfe0a01f5d7200121b03e605d6a0439f8"}, - {file = "yarl-1.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f8a891e4a22a89f5dde7862994485e19db246b70bb288d3ce73a34422e55b23"}, - {file = "yarl-1.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd803820d44c8853a109a34e3660e5a61beae12970da479cf44aa2954019bf70"}, - {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b982fa7f74c80d5c0c7b5b38f908971e513380a10fecea528091405f519b9ebb"}, - {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:33f29ecfe0330c570d997bcf1afd304377f2e48f61447f37e846a6058a4d33b2"}, - {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:835ab2cfc74d5eb4a6a528c57f05688099da41cf4957cf08cad38647e4a83b30"}, - {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:46b5e0ccf1943a9a6e766b2c2b8c732c55b34e28be57d8daa2b3c1d1d4009309"}, - {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:df47c55f7d74127d1b11251fe6397d84afdde0d53b90bedb46a23c0e534f9d24"}, - {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76d12524d05841276b0e22573f28d5fbcb67589836772ae9244d90dd7d66aa13"}, - {file = "yarl-1.20.1-cp310-cp310-win32.whl", hash = "sha256:6c4fbf6b02d70e512d7ade4b1f998f237137f1417ab07ec06358ea04f69134f8"}, - {file = "yarl-1.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:aef6c4d69554d44b7f9d923245f8ad9a707d971e6209d51279196d8e8fe1ae16"}, - {file = "yarl-1.20.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:47ee6188fea634bdfaeb2cc420f5b3b17332e6225ce88149a17c413c77ff269e"}, - {file = "yarl-1.20.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d0f6500f69e8402d513e5eedb77a4e1818691e8f45e6b687147963514d84b44b"}, - {file = "yarl-1.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a8900a42fcdaad568de58887c7b2f602962356908eedb7628eaf6021a6e435b"}, - {file = "yarl-1.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bad6d131fda8ef508b36be3ece16d0902e80b88ea7200f030a0f6c11d9e508d4"}, - {file = "yarl-1.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:df018d92fe22aaebb679a7f89fe0c0f368ec497e3dda6cb81a567610f04501f1"}, - {file = "yarl-1.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f969afbb0a9b63c18d0feecf0db09d164b7a44a053e78a7d05f5df163e43833"}, - {file = "yarl-1.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:812303eb4aa98e302886ccda58d6b099e3576b1b9276161469c25803a8db277d"}, - {file = "yarl-1.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98c4a7d166635147924aa0bf9bfe8d8abad6fffa6102de9c99ea04a1376f91e8"}, - {file = "yarl-1.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12e768f966538e81e6e7550f9086a6236b16e26cd964cf4df35349970f3551cf"}, - {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe41919b9d899661c5c28a8b4b0acf704510b88f27f0934ac7a7bebdd8938d5e"}, - {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8601bc010d1d7780592f3fc1bdc6c72e2b6466ea34569778422943e1a1f3c389"}, - {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:daadbdc1f2a9033a2399c42646fbd46da7992e868a5fe9513860122d7fe7a73f"}, - {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:03aa1e041727cb438ca762628109ef1333498b122e4c76dd858d186a37cec845"}, - {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:642980ef5e0fa1de5fa96d905c7e00cb2c47cb468bfcac5a18c58e27dbf8d8d1"}, - {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:86971e2795584fe8c002356d3b97ef6c61862720eeff03db2a7c86b678d85b3e"}, - {file = "yarl-1.20.1-cp311-cp311-win32.whl", hash = "sha256:597f40615b8d25812f14562699e287f0dcc035d25eb74da72cae043bb884d773"}, - {file = "yarl-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:26ef53a9e726e61e9cd1cda6b478f17e350fb5800b4bd1cd9fe81c4d91cfeb2e"}, - {file = "yarl-1.20.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdcc4cd244e58593a4379fe60fdee5ac0331f8eb70320a24d591a3be197b94a9"}, - {file = "yarl-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b29a2c385a5f5b9c7d9347e5812b6f7ab267193c62d282a540b4fc528c8a9d2a"}, - {file = "yarl-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1112ae8154186dfe2de4732197f59c05a83dc814849a5ced892b708033f40dc2"}, - {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90bbd29c4fe234233f7fa2b9b121fb63c321830e5d05b45153a2ca68f7d310ee"}, - {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:680e19c7ce3710ac4cd964e90dad99bf9b5029372ba0c7cbfcd55e54d90ea819"}, - {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a979218c1fdb4246a05efc2cc23859d47c89af463a90b99b7c56094daf25a16"}, - {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255b468adf57b4a7b65d8aad5b5138dce6a0752c139965711bdcb81bc370e1b6"}, - {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97d67108e79cfe22e2b430d80d7571ae57d19f17cda8bb967057ca8a7bf5bfd"}, - {file = "yarl-1.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8570d998db4ddbfb9a590b185a0a33dbf8aafb831d07a5257b4ec9948df9cb0a"}, - {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97c75596019baae7c71ccf1d8cc4738bc08134060d0adfcbe5642f778d1dca38"}, - {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c48912653e63aef91ff988c5432832692ac5a1d8f0fb8a33091520b5bbe19ef"}, - {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4c3ae28f3ae1563c50f3d37f064ddb1511ecc1d5584e88c6b7c63cf7702a6d5f"}, - {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e9642f27036283550f5f57dc6156c51084b458570b9d0d96100c8bebb186a8"}, - {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2c26b0c49220d5799f7b22c6838409ee9bc58ee5c95361a4d7831f03cc225b5a"}, - {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004"}, - {file = "yarl-1.20.1-cp312-cp312-win32.whl", hash = "sha256:daea0d313868da1cf2fac6b2d3a25c6e3a9e879483244be38c8e6a41f1d876a5"}, - {file = "yarl-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698"}, - {file = "yarl-1.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a"}, - {file = "yarl-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3"}, - {file = "yarl-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7"}, - {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49bdd1b8e00ce57e68ba51916e4bb04461746e794e7c4d4bbc42ba2f18297691"}, - {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:66252d780b45189975abfed839616e8fd2dbacbdc262105ad7742c6ae58f3e31"}, - {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59174e7332f5d153d8f7452a102b103e2e74035ad085f404df2e40e663a22b28"}, - {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3968ec7d92a0c0f9ac34d5ecfd03869ec0cab0697c91a45db3fbbd95fe1b653"}, - {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1a4fbb50e14396ba3d375f68bfe02215d8e7bc3ec49da8341fe3157f59d2ff5"}, - {file = "yarl-1.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11a62c839c3a8eac2410e951301309426f368388ff2f33799052787035793b02"}, - {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:041eaa14f73ff5a8986b4388ac6bb43a77f2ea09bf1913df7a35d4646db69e53"}, - {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:377fae2fef158e8fd9d60b4c8751387b8d1fb121d3d0b8e9b0be07d1b41e83dc"}, - {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1c92f4390e407513f619d49319023664643d3339bd5e5a56a3bebe01bc67ec04"}, - {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d25ddcf954df1754ab0f86bb696af765c5bfaba39b74095f27eececa049ef9a4"}, - {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:909313577e9619dcff8c31a0ea2aa0a2a828341d92673015456b3ae492e7317b"}, - {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:793fd0580cb9664548c6b83c63b43c477212c0260891ddf86809e1c06c8b08f1"}, - {file = "yarl-1.20.1-cp313-cp313-win32.whl", hash = "sha256:468f6e40285de5a5b3c44981ca3a319a4b208ccc07d526b20b12aeedcfa654b7"}, - {file = "yarl-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:495b4ef2fea40596bfc0affe3837411d6aa3371abcf31aac0ccc4bdd64d4ef5c"}, - {file = "yarl-1.20.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f60233b98423aab21d249a30eb27c389c14929f47be8430efa7dbd91493a729d"}, - {file = "yarl-1.20.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6f3eff4cc3f03d650d8755c6eefc844edde99d641d0dcf4da3ab27141a5f8ddf"}, - {file = "yarl-1.20.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:69ff8439d8ba832d6bed88af2c2b3445977eba9a4588b787b32945871c2444e3"}, - {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf34efa60eb81dd2645a2e13e00bb98b76c35ab5061a3989c7a70f78c85006d"}, - {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8e0fe9364ad0fddab2688ce72cb7a8e61ea42eff3c7caeeb83874a5d479c896c"}, - {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f64fbf81878ba914562c672024089e3401974a39767747691c65080a67b18c1"}, - {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6342d643bf9a1de97e512e45e4b9560a043347e779a173250824f8b254bd5ce"}, - {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56dac5f452ed25eef0f6e3c6a066c6ab68971d96a9fb441791cad0efba6140d3"}, - {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7d7f497126d65e2cad8dc5f97d34c27b19199b6414a40cb36b52f41b79014be"}, - {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:67e708dfb8e78d8a19169818eeb5c7a80717562de9051bf2413aca8e3696bf16"}, - {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:595c07bc79af2494365cc96ddeb772f76272364ef7c80fb892ef9d0649586513"}, - {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7bdd2f80f4a7df852ab9ab49484a4dee8030023aa536df41f2d922fd57bf023f"}, - {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c03bfebc4ae8d862f853a9757199677ab74ec25424d0ebd68a0027e9c639a390"}, - {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:344d1103e9c1523f32a5ed704d576172d2cabed3122ea90b1d4e11fe17c66458"}, - {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:88cab98aa4e13e1ade8c141daeedd300a4603b7132819c484841bb7af3edce9e"}, - {file = "yarl-1.20.1-cp313-cp313t-win32.whl", hash = "sha256:b121ff6a7cbd4abc28985b6028235491941b9fe8fe226e6fdc539c977ea1739d"}, - {file = "yarl-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f"}, - {file = "yarl-1.20.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e42ba79e2efb6845ebab49c7bf20306c4edf74a0b20fc6b2ccdd1a219d12fad3"}, - {file = "yarl-1.20.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:41493b9b7c312ac448b7f0a42a089dffe1d6e6e981a2d76205801a023ed26a2b"}, - {file = "yarl-1.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f5a5928ff5eb13408c62a968ac90d43f8322fd56d87008b8f9dabf3c0f6ee983"}, - {file = "yarl-1.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30c41ad5d717b3961b2dd785593b67d386b73feca30522048d37298fee981805"}, - {file = "yarl-1.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:59febc3969b0781682b469d4aca1a5cab7505a4f7b85acf6db01fa500fa3f6ba"}, - {file = "yarl-1.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d2b6fb3622b7e5bf7a6e5b679a69326b4279e805ed1699d749739a61d242449e"}, - {file = "yarl-1.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:749d73611db8d26a6281086f859ea7ec08f9c4c56cec864e52028c8b328db723"}, - {file = "yarl-1.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9427925776096e664c39e131447aa20ec738bdd77c049c48ea5200db2237e000"}, - {file = "yarl-1.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff70f32aa316393eaf8222d518ce9118148eddb8a53073c2403863b41033eed5"}, - {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c7ddf7a09f38667aea38801da8b8d6bfe81df767d9dfc8c88eb45827b195cd1c"}, - {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:57edc88517d7fc62b174fcfb2e939fbc486a68315d648d7e74d07fac42cec240"}, - {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:dab096ce479d5894d62c26ff4f699ec9072269d514b4edd630a393223f45a0ee"}, - {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:14a85f3bd2d7bb255be7183e5d7d6e70add151a98edf56a770d6140f5d5f4010"}, - {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c89b5c792685dd9cd3fa9761c1b9f46fc240c2a3265483acc1565769996a3f8"}, - {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:69e9b141de5511021942a6866990aea6d111c9042235de90e08f94cf972ca03d"}, - {file = "yarl-1.20.1-cp39-cp39-win32.whl", hash = "sha256:b5f307337819cdfdbb40193cad84978a029f847b0a357fbe49f712063cfc4f06"}, - {file = "yarl-1.20.1-cp39-cp39-win_amd64.whl", hash = "sha256:eae7bfe2069f9c1c5b05fc7fe5d612e5bbc089a39309904ee8b829e322dcad00"}, - {file = "yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77"}, - {file = "yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac"}, -] - -[package.dependencies] -idna = ">=2.0" -multidict = ">=4.0" -propcache = ">=0.2.1" - -[[package]] -name = "zipp" -version = "3.23.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, - {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] -type = ["pytest-mypy"] - -[[package]] -name = "zstd" -version = "1.5.7.2" -description = "ZSTD Bindings for Python" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "zstd-1.5.7.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e17104d0e88367a7571dde4286e233126c8551691ceff11f9ae2e3a3ac1bb483"}, - {file = "zstd-1.5.7.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:d6ee5dfada4c8fa32f43cc092fcf7d8482da6ad242c22fdf780f7eebd0febcc7"}, - {file = "zstd-1.5.7.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:ae1100776cb400100e2d2f427b50dc983c005c38cd59502eb56d2cfea3402ad5"}, - {file = "zstd-1.5.7.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:489a0ff15caf7640851e63f85b680c4279c99094cd500a29c7ed3ab82505fce0"}, - {file = "zstd-1.5.7.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:92590cf54318849d492445c885f1a42b9dbb47cdc070659c7cb61df6e8531047"}, - {file = "zstd-1.5.7.2-cp27-cp27mu-manylinux_2_4_i686.whl", hash = "sha256:2bc21650f7b9c058a3c4cb503e906fe9cce293941ec1b48bc5d005c3b4422b42"}, - {file = "zstd-1.5.7.2-cp27-cp27mu-manylinux_2_4_x86_64.whl", hash = "sha256:7b13e7eef9aa192804d38bf413924d347c6f6c6ac07f5a0c1ae4a6d7b3af70f0"}, - {file = "zstd-1.5.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d3f14c5c405ea353b68fe105236780494eb67c756ecd346fd295498f5eab6d24"}, - {file = "zstd-1.5.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07d2061df22a3efc06453089e6e8b96e58f5bb7a0c4074dcfd0b0ce243ddde72"}, - {file = "zstd-1.5.7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:27e55aa2043ba7d8a08aba0978c652d4d5857338a8188aa84522569f3586c7bb"}, - {file = "zstd-1.5.7.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:8e97933addfd71ea9608306f18dc18e7d2a5e64212ba2bb9a4ccb6d714f9f280"}, - {file = "zstd-1.5.7.2-cp310-cp310-manylinux_2_4_i686.whl", hash = "sha256:27e2ed58b64001c9ef0a8e028625477f1a6ed4ca949412ff6548544945cc59c2"}, - {file = "zstd-1.5.7.2-cp310-cp310-manylinux_2_4_x86_64.whl", hash = "sha256:92f072819fc0c7e8445f51a232c9ad76642027c069d2f36470cdb5e663839cdb"}, - {file = "zstd-1.5.7.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:2a653cdd2c52d60c28e519d44bde8d759f2c1837f0ff8e8e1b0045ca62fcf70e"}, - {file = "zstd-1.5.7.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:047803d87d910f4905f48d99aeff1e0539ec2e4f4bf17d077701b5d0b2392a95"}, - {file = "zstd-1.5.7.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0d8c1dc947e5ccea3bd81043080213685faf1d43886c27c51851fabf325f05c0"}, - {file = "zstd-1.5.7.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8291d393321fac30604c6bbf40067103fee315aa476647a5eaecf877ee53496f"}, - {file = "zstd-1.5.7.2-cp310-cp310-win32.whl", hash = "sha256:6922ceac5f2d60bb57a7875168c8aa442477b83e8951f2206cf1e9be788b0a6e"}, - {file = "zstd-1.5.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:346d1e4774d89a77d67fc70d53964bfca57c0abecfd885a4e00f87fd7c71e074"}, - {file = "zstd-1.5.7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f799c1e9900ad77e7a3d994b9b5146d7cfd1cbd1b61c3db53a697bf21ffcc57b"}, - {file = "zstd-1.5.7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ff4c667f29101566a7b71f06bbd677a63192818396003354131f586383db042"}, - {file = "zstd-1.5.7.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8526a32fa9f67b07fd09e62474e345f8ca1daf3e37a41137643d45bd1bc90773"}, - {file = "zstd-1.5.7.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:2cec2472760d48a7a3445beaba509d3f7850e200fed65db15a1a66e315baec6a"}, - {file = "zstd-1.5.7.2-cp311-cp311-manylinux_2_4_i686.whl", hash = "sha256:a200c479ee1bb661bc45518e016a1fdc215a1d8f7e4bf6c7de0af254976cfdf6"}, - {file = "zstd-1.5.7.2-cp311-cp311-manylinux_2_4_x86_64.whl", hash = "sha256:f5d159e57a13147aa8293c0f14803a75e9039fd8afdf6cf1c8c2289fb4d2333a"}, - {file = "zstd-1.5.7.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:7206934a2bd390080e972a1fed5a897e184dfd71dbb54e978dc11c6b295e1806"}, - {file = "zstd-1.5.7.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7e0027b20f296d1c9a8e85b8436834cf46560240a29d623aa8eaa8911832eb58"}, - {file = "zstd-1.5.7.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d6b17e5581dd1a13437079bd62838d2635db8eb8aca9c0e9251faa5d4d40a6d7"}, - {file = "zstd-1.5.7.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b13285c99cc710f60dd270785ec75233018870a1831f5655d862745470a0ca29"}, - {file = "zstd-1.5.7.2-cp311-cp311-win32.whl", hash = "sha256:cdb5ec80da299f63f8aeccec0bff3247e96252d4c8442876363ff1b438d8049b"}, - {file = "zstd-1.5.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:4f6861c8edceb25fda37cdaf422fc5f15dcc88ced37c6a5b3c9011eda51aa218"}, - {file = "zstd-1.5.7.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2ebe3e60dbace52525fa7aa604479e231dc3e4fcc76d0b4c54d8abce5e58734"}, - {file = "zstd-1.5.7.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ef201b6f7d3a6751d85cc52f9e6198d4d870e83d490172016b64a6dd654a9583"}, - {file = "zstd-1.5.7.2-cp312-cp312-manylinux_2_14_x86_64.whl", hash = "sha256:ac7bdfedda51b1fcdcf0ab69267d01256fc97ddf666ce894fde0fae9f3630eac"}, - {file = "zstd-1.5.7.2-cp312-cp312-manylinux_2_4_i686.whl", hash = "sha256:b835405cc4080b378e45029f2fe500e408d1eaedfba7dd7402aba27af16955f9"}, - {file = "zstd-1.5.7.2-cp312-cp312-win32.whl", hash = "sha256:e4cf97bb97ed6dbb62d139d68fd42fa1af51fd26fd178c501f7b62040e897c50"}, - {file = "zstd-1.5.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:55e2edc4560a5cf8ee9908595e90a15b1f47536ea9aad4b2889f0e6165890a38"}, - {file = "zstd-1.5.7.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6e684e27064b6550aa2e7dc85d171ea1b62cb5930a2c99b3df9b30bf620b5c06"}, - {file = "zstd-1.5.7.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd6262788a98807d6b2befd065d127db177c1cd76bb8e536e0dded419eb7c7fb"}, - {file = "zstd-1.5.7.2-cp313-cp313-manylinux_2_14_x86_64.whl", hash = "sha256:53948be45f286a1b25c07a6aa2aca5c902208eb3df9fe36cf891efa0394c8b71"}, - {file = "zstd-1.5.7.2-cp313-cp313-win32.whl", hash = "sha256:edf816c218e5978033b7bb47dcb453dfb71038cb8a9bf4877f3f823e74d58174"}, - {file = "zstd-1.5.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:eea9bddf06f3f5e1e450fd647665c86df048a45e8b956d53522387c1dff41b7a"}, - {file = "zstd-1.5.7.2-cp313-cp313t-manylinux_2_14_x86_64.whl", hash = "sha256:1d71f9f92b3abe18b06b5f0aefa5b9c42112beef3bff27e36028d147cb4426a6"}, - {file = "zstd-1.5.7.2-cp314-cp314-manylinux_2_14_x86_64.whl", hash = "sha256:a6105b8fa21dbc59e05b6113e8e5d5aaf56c5d2886aa5778d61030af3256bbb7"}, - {file = "zstd-1.5.7.2-cp314-cp314t-manylinux_2_14_x86_64.whl", hash = "sha256:d0b0ca097efb5f67157c61a744c926848dcccf6e913df2f814e719aa78197a4b"}, - {file = "zstd-1.5.7.2-cp34-cp34m-manylinux_2_4_i686.whl", hash = "sha256:a371274668182ae06be2e321089b207fa0a75a58ae2fd4dfb7eafded9e041b2f"}, - {file = "zstd-1.5.7.2-cp34-cp34m-manylinux_2_4_x86_64.whl", hash = "sha256:74c3f006c9a3a191ed454183f0fb78172444f5cb431be04d85044a27f1b58c7b"}, - {file = "zstd-1.5.7.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:f19a3e658d92b6b52020c4c6d4c159480bcd3b47658773ea0e8d343cee849f33"}, - {file = "zstd-1.5.7.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:d9d1bcb6441841c599883139c1b0e47bddb262cce04b37dc2c817da5802c1158"}, - {file = "zstd-1.5.7.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:bb1cb423fc40468cc9b7ab51a5b33c618eefd2c910a5bffed6ed76fe1cbb20b0"}, - {file = "zstd-1.5.7.2-cp35-cp35m-manylinux_2_14_x86_64.whl", hash = "sha256:e2476ba12597e58c5fc7a3ae547ee1bef9dd6b9d5ea80cf8d4034930c5a336e0"}, - {file = "zstd-1.5.7.2-cp35-cp35m-manylinux_2_4_i686.whl", hash = "sha256:2bf6447373782a2a9df3015121715f6d0b80a49a884c2d7d4518c9571e9fca16"}, - {file = "zstd-1.5.7.2-cp35-cp35m-win32.whl", hash = "sha256:a59a136a9eaa1849d715c004e30344177e85ad6e7bc4a5d0b6ad2495c5402675"}, - {file = "zstd-1.5.7.2-cp35-cp35m-win_amd64.whl", hash = "sha256:114115af8c68772a3205414597f626b604c7879f6662a2a79c88312e0f50361f"}, - {file = "zstd-1.5.7.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f576ec00e99db124309dac1e1f34bc320eb69624189f5fdaf9ebe1dc81581a84"}, - {file = "zstd-1.5.7.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:f97d8593da0e23a47f148a1cb33300dccd513fb0df9f7911c274e228a8c1a300"}, - {file = "zstd-1.5.7.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:a130243e875de5aeda6099d12b11bc2fcf548dce618cf6b17f731336ba5338e4"}, - {file = "zstd-1.5.7.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:73cec37649fda383348dc8b3b5fba535f1dbb1bbaeb60fd36f4c145820208619"}, - {file = "zstd-1.5.7.2-cp36-cp36m-manylinux_2_14_x86_64.whl", hash = "sha256:883e7b77a3124011b8badd0c7c9402af3884700a3431d07877972e157d85afb8"}, - {file = "zstd-1.5.7.2-cp36-cp36m-manylinux_2_4_i686.whl", hash = "sha256:b5af6aa041b5515934afef2ef4af08566850875c3c890109088eedbe190eeefb"}, - {file = "zstd-1.5.7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:53abf577aec7b30afa3c024143f4866676397c846b44f1b30d8097b5e4f5c7d7"}, - {file = "zstd-1.5.7.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:660945ba16c16957c94dafc40aff1db02a57af0489aa3a896866239d47bb44b0"}, - {file = "zstd-1.5.7.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:3e220d2d7005822bb72a52e76410ca4634f941d8062c08e8e3285733c63b1db7"}, - {file = "zstd-1.5.7.2-cp37-cp37m-manylinux_2_4_i686.whl", hash = "sha256:7e998f86a9d1e576c0158bf0b0a6a5c4685679d74ba0053a2e87f684f9bdc8eb"}, - {file = "zstd-1.5.7.2-cp37-cp37m-manylinux_2_4_x86_64.whl", hash = "sha256:70d0c4324549073e05aa72e9eb6a593f89cba59da804b946d325d68467b93ad5"}, - {file = "zstd-1.5.7.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:b9518caabf59405eddd667bbb161d9ae7f13dbf96967fd998d095589c8d41c86"}, - {file = "zstd-1.5.7.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:30d339d8e5c4b14c2015b50371fcdb8a93b451ca6d3ef813269ccbb8b3b3ef7d"}, - {file = "zstd-1.5.7.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:6f5539a10b838ee576084870eed65b63c13845e30a5b552cfe40f7e6b621e61a"}, - {file = "zstd-1.5.7.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:5540ce1c99fa0b59dad2eff771deb33872754000da875be50ac8c2beab42b433"}, - {file = "zstd-1.5.7.2-cp37-cp37m-win32.whl", hash = "sha256:56c4b8cd0a88fd721213661c28b87b64fbd14b6019df39b21b0117a68162b0f2"}, - {file = "zstd-1.5.7.2-cp37-cp37m-win_amd64.whl", hash = "sha256:594f256fa72852ade60e3acb909f983d5cf6839b9fc79728dd4b48b31112058f"}, - {file = "zstd-1.5.7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9dc05618eb0abceb296b77e5f608669c12abc69cbf447d08151bcb14d290ab07"}, - {file = "zstd-1.5.7.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:70231ba799d681b6fc17456c3e39895c493b5dff400aa7842166322a952b7f2a"}, - {file = "zstd-1.5.7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:5a73f0f20f71d4eef970a3fed7baac64d9a2a00b238acc4eca2bd7172bd7effb"}, - {file = "zstd-1.5.7.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0a470f8938f69f632b8f88b96578a5e8825c18ddbbea7de63493f74874f963ef"}, - {file = "zstd-1.5.7.2-cp38-cp38-manylinux_2_4_i686.whl", hash = "sha256:d104f1cb2a7c142007c29a2a62dfe633155c648317a465674e583c295e5f792d"}, - {file = "zstd-1.5.7.2-cp38-cp38-manylinux_2_4_x86_64.whl", hash = "sha256:70f29e0504fc511d4b9f921e69637fca79c050e618ba23732a3f75c044814d89"}, - {file = "zstd-1.5.7.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:a62c2f6f7b8fc69767392084828740bd6faf35ff54d4ccb2e90e199327c64140"}, - {file = "zstd-1.5.7.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f2dda0c76f87723fb7f75d7ad3bbd90f7fb47b75051978d22535099325111b41"}, - {file = "zstd-1.5.7.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:f9cf09c2aa6f67750fe9f33fdd122f021b1a23bf7326064a8e21f7af7e77faee"}, - {file = "zstd-1.5.7.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:910bd9eac2488439f597504756b03c74aa63ed71b21e5d0aa2c7e249b3f1c13f"}, - {file = "zstd-1.5.7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9838ec7eb9f1beb2f611b9bcac7a169cb3de708ccf779aead29787e4482fe232"}, - {file = "zstd-1.5.7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:83a36bb1fd574422a77b36ccf3315ab687aef9a802b0c3312ca7006b74eeb109"}, - {file = "zstd-1.5.7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:6f8189bc58415758bbbd419695012194f5e5e22c34553712d9a3eb009c09808d"}, - {file = "zstd-1.5.7.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:632e3c1b7e1ebb0580f6d92b781a8f7901d367cf72725d5642e6d3a32e404e45"}, - {file = "zstd-1.5.7.2-cp39-cp39-manylinux_2_4_i686.whl", hash = "sha256:df8083c40fdbfe970324f743f0b5ecc244c37736e5f3ad2670de61dde5e0b024"}, - {file = "zstd-1.5.7.2-cp39-cp39-manylinux_2_4_x86_64.whl", hash = "sha256:300db1ede4d10f8b9b3b99ca52b22f0e2303dc4f1cf6994d1f8345ce22dd5a7e"}, - {file = "zstd-1.5.7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:97b908ccb385047b0c020ce3dc55e6f51078c9790722fdb3620c076be4a69ecf"}, - {file = "zstd-1.5.7.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c59218bd36a7431a40591504f299de836ea0d63bc68ea76d58c4cf5262f0fa3c"}, - {file = "zstd-1.5.7.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4d5a85344193ec967d05da8e2c10aed400e2d83e16041d2fdfb713cfc8caceeb"}, - {file = "zstd-1.5.7.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ebf6c1d7f0ceb0af5a383d2a1edc8ab9ace655e62a41c8a4ed5a031ee2ef8006"}, - {file = "zstd-1.5.7.2-cp39-cp39-win32.whl", hash = "sha256:44a5142123d59a0dbbd9ba9720c23521be57edbc24202223a5e17405c3bdd4a6"}, - {file = "zstd-1.5.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:8dc542a9818712a9fb37563fa88cdbbbb2b5f8733111d412b718fa602b83ba45"}, - {file = "zstd-1.5.7.2-pp27-pypy_73-manylinux1_x86_64.whl", hash = "sha256:24371a7b0475eef7d933c72067d363c5dc17282d2aa5d4f5837774378718509e"}, - {file = "zstd-1.5.7.2-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:c21d44981b068551f13097be3809fadb7f81617d0c21b2c28a7d04653dde958f"}, - {file = "zstd-1.5.7.2-pp27-pypy_73-manylinux_2_14_x86_64.whl", hash = "sha256:b011bf4cfad78cdf9116d6731234ff181deb9560645ffdcc8d54861ae5d1edfc"}, - {file = "zstd-1.5.7.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:426e5c6b7b3e2401b734bfd08050b071e17c15df5e3b31e63651d1fd9ba4c751"}, - {file = "zstd-1.5.7.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:53375b23f2f39359ade944169bbd88f8895eed91290ee608ccbc28810ac360ba"}, - {file = "zstd-1.5.7.2-pp310-pypy310_pp73-manylinux_2_14_x86_64.whl", hash = "sha256:1b301b2f9dbb0e848093127fb10cbe6334a697dc3aea6740f0bb726450ee9a34"}, - {file = "zstd-1.5.7.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5414c9ae27069ab3ec8420fe8d005cb1b227806cbc874a7b4c73a96b4697a633"}, - {file = "zstd-1.5.7.2-pp311-pypy311_pp73-manylinux_2_14_x86_64.whl", hash = "sha256:5fb2ff5718fe89181223c23ce7308bd0b4a427239379e2566294da805d8df68a"}, - {file = "zstd-1.5.7.2-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:9714d5642867fceb22e4ab74aebf81a2e62dc9206184d603cb39277b752d5885"}, - {file = "zstd-1.5.7.2-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:6584fd081a6e7d92dffa8e7373d1fced6b3cbf473154b82c17a99438c5e1de51"}, - {file = "zstd-1.5.7.2-pp36-pypy36_pp73-manylinux_2_14_x86_64.whl", hash = "sha256:52f27a198e2a72632bae12ec63ebaa31b10e3d5f3dd3df2e01376979b168e2e6"}, - {file = "zstd-1.5.7.2-pp36-pypy36_pp73-win32.whl", hash = "sha256:3b14793d2a2cb3a7ddd1cf083321b662dd20bc11143abc719456e9bfd22a32aa"}, - {file = "zstd-1.5.7.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:faf3fd38ba26167c5a085c04b8c931a216f1baf072709db7a38e61dea52e316e"}, - {file = "zstd-1.5.7.2-pp37-pypy37_pp73-manylinux_2_14_x86_64.whl", hash = "sha256:d17ac6d2584168247796174e599d4adbee00153246287e68881efaf8d48a6970"}, - {file = "zstd-1.5.7.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:9a24d492c63555b55e6bc73a9e82a38bf7c3e8f7cde600f079210ed19cb061f2"}, - {file = "zstd-1.5.7.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c6abf4ab9a9d1feb14bc3cbcc32d723d340ce43b79b1812805916f3ac069b073"}, - {file = "zstd-1.5.7.2-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:d7131bb4e55d075cb7847555a1e17fca5b816a550c9b9ac260c01799b6f8e8d9"}, - {file = "zstd-1.5.7.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:a03608499794148f39c932c508d4eb3622e79ca2411b1d0438a2ee8cafdc0111"}, - {file = "zstd-1.5.7.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:86e64c71b4d00bf28be50e4941586e7874bdfa74858274d9f7571dd5dda92086"}, - {file = "zstd-1.5.7.2-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:0f79492bf86aef6e594b11e29c5589ddd13253db3ada0c7a14fb176b132fb65e"}, - {file = "zstd-1.5.7.2-pp38-pypy38_pp73-manylinux_2_14_x86_64.whl", hash = "sha256:8c3f4bb8508bc54c00532931da4a5261f08493363da14a5526c986765973e35d"}, - {file = "zstd-1.5.7.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:787bcf55cefc08d27aca34c6dcaae1a24940963d1a73d4cec894ee458c541ac4"}, - {file = "zstd-1.5.7.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0f97f872cb78a4fd60b6c1024a65a4c52a971e9d991f33c7acd833ee73050f85"}, - {file = "zstd-1.5.7.2-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:5e530b75452fdcff4ea67268d9e7cb37a38e7abbac84fa845205f0b36da81aaf"}, - {file = "zstd-1.5.7.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:7c1cc65fc2789dd97a98202df840537de186ed04fd1804a17fcb15d1232442c4"}, - {file = "zstd-1.5.7.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:05604a693fa53b60ca083992324b08dafd15a4ac37ac4cffe4b43b9eb93d4440"}, - {file = "zstd-1.5.7.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:baf4e8b46d8934d4e85373f303eb048c63897fc4191d8ab301a1bbdf30b7a3cc"}, - {file = "zstd-1.5.7.2-pp39-pypy39_pp73-manylinux_2_14_x86_64.whl", hash = "sha256:8cc35cc25e2d4a0f68020f05cba96912a2881ebaca890d990abe37aa3aa27045"}, - {file = "zstd-1.5.7.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:ceae57e369e1b821b8f2b4c59bc08acd27d8e4bf9687bfa5211bc4cdb080fe7b"}, - {file = "zstd-1.5.7.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:5189fb44c44ab9b6c45f734bd7093a67686193110dc90dcfaf0e3a31b2385f38"}, - {file = "zstd-1.5.7.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:f51a965871b25911e06d421212f9be7f7bcd3cedc43ea441a8a73fad9952baa0"}, - {file = "zstd-1.5.7.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:624022851c51dd6d6b31dbfd793347c4bd6339095e8383e2f74faf4f990b04c6"}, - {file = "zstd-1.5.7.2.tar.gz", hash = "sha256:6d8684c69009be49e1b18ec251a5eb0d7e24f93624990a8a124a1da66a92fc8a"}, -] - -[metadata] -lock-version = "2.1" -python-versions = ">=3.10,<3.13" -content-hash = "09ce4507a464b318702ed8c6a738f3bb1bc4cc6ff5a50a9c2884f560af9ab034" diff --git a/prowler/AGENTS.md b/prowler/AGENTS.md index 5f7099ac0a..ab3ba1ce67 100644 --- a/prowler/AGENTS.md +++ b/prowler/AGENTS.md @@ -7,22 +7,26 @@ > - [`prowler-compliance`](../skills/prowler-compliance/SKILL.md) - Compliance framework structure > - [`pytest`](../skills/pytest/SKILL.md) - Generic pytest patterns -### Auto-invoke Skills +## Auto-invoke Skills When performing these actions, ALWAYS invoke the corresponding skill FIRST: | Action | Skill | |--------|-------| | Add changelog entry for a PR or feature | `prowler-changelog` | +| Adding a compliance output formatter (per-provider class + table dispatcher) | `prowler-compliance` | | Adding new providers | `prowler-provider` | | Adding services to existing providers | `prowler-provider` | +| Auditing check-to-requirement mappings as a cloud auditor | `prowler-compliance` | | Create PR that requires changelog entry | `prowler-changelog` | | Creating new checks | `prowler-sdk-check` | | Creating/updating compliance frameworks | `prowler-compliance` | +| Fixing compliance JSON bugs (duplicate IDs, empty Section, stale refs) | `prowler-compliance` | | Mapping checks to compliance controls | `prowler-compliance` | | Mocking AWS with moto in tests | `prowler-test-sdk` | | Review changelog format and conventions | `prowler-changelog` | | Reviewing compliance framework PRs | `prowler-compliance-review` | +| Syncing compliance framework with upstream catalog | `prowler-compliance` | | Update CHANGELOG.md in any component | `prowler-changelog` | | Updating existing checks and metadata | `prowler-sdk-check` | | Writing Prowler SDK tests | `prowler-test-sdk` | @@ -40,7 +44,7 @@ The Prowler SDK is the core Python engine powering cloud security assessments ac ### Provider Architecture -``` +```text prowler/providers/{provider}/ ├── {provider}_provider.py # Main provider class ├── models.py # Provider-specific models @@ -81,13 +85,13 @@ class {check_name}(Check): ## TECH STACK -Python 3.10+ | Poetry 2.3+ | pytest | moto (AWS mocking) | Pre-commit hooks (black, flake8, pylint, bandit) +Python 3.10+ | uv | pytest | moto (AWS mocking) | Pre-commit hooks (black, flake8, pylint, bandit) --- ## PROJECT STRUCTURE -``` +```text prowler/ ├── __main__.py # CLI entry point ├── config/ # Global configuration @@ -108,20 +112,20 @@ prowler/ ```bash # Setup -poetry install --with dev -poetry run pre-commit install +uv sync +uv run pre-commit install # Run Prowler -poetry run python prowler-cli.py {provider} -poetry run python prowler-cli.py {provider} --check {check_name} -poetry run python prowler-cli.py {provider} --list-checks +uv run python prowler-cli.py {provider} +uv run python prowler-cli.py {provider} --check {check_name} +uv run python prowler-cli.py {provider} --list-checks # Testing -poetry run pytest -n auto -vvv tests/ -poetry run pytest tests/providers/{provider}/services/{service}/ -v +uv run pytest -n auto -vvv tests/ +uv run pytest tests/providers/{provider}/services/{service}/ -v # Code Quality -poetry run pre-commit run --all-files +uv run pre-commit run --all-files ``` --- @@ -141,8 +145,8 @@ poetry run pre-commit run --all-files ## QA CHECKLIST -- [ ] `poetry run pytest` passes -- [ ] `poetry run pre-commit run --all-files` passes +- [ ] `uv run pytest` passes +- [ ] `uv run pre-commit run --all-files` passes - [ ] Check metadata JSON is valid - [ ] Tests cover PASS, FAIL, and empty resource scenarios - [ ] Docstrings follow Google style diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 5c059287dd..607cbb2e80 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -2,16 +2,116 @@ All notable changes to the **Prowler SDK** are documented in this file. -## [5.26.0] (Prowler UNRELEASED) +## [5.29.0] (Prowler UNRELEASED) + +### 🚀 Added + +- `application` service for Okta provider with `application_admin_console_session_idle_timeout_15min`, `application_admin_console_mfa_required`, `application_admin_console_phishing_resistant_authentication`, `application_dashboard_mfa_required`, `application_dashboard_phishing_resistant_authentication`, and `application_authentication_policy_network_zone_enforced` checks [(#11358)](https://github.com/prowler-cloud/prowler/pull/11358) +- AWS AI Security Framework compliance for AWS provider [(#11353)](https://github.com/prowler-cloud/prowler/pull/11353) +- `storage_account_public_network_access_disabled` check for Azure provider and remapped the Azure CIS "Public Network Access is Disabled" requirements to it [(#11334)](https://github.com/prowler-cloud/prowler/pull/11334) + +### 🐞 Fixed + +- ENS RD 311/2022 (AWS) compliance mapping: `vpc_different_regions` was uncorrectly mapped under the `mp.com.4` family (Network segregation). That check is now mapped to a new `op.cont.2.aws.vpc.1` requirement under the Continuity of Service control [(#11372)](https://github.com/prowler-cloud/prowler/pull/11372) +- Compliance CSV row count now matches the UI per requirement by sourcing rows from the framework JSON's `requirement.Checks` instead of the stale `finding.compliance` snapshot [(#11370)](https://github.com/prowler-cloud/prowler/pull/11370) + +--- + +## [5.28.1] (Prowler 5.28.1) + +### 🐞 Fixed + +- `compute_project_os_login_enabled` and `compute_project_os_login_2fa_enabled` checks for GCP provider no longer false-FAIL on projects where the `enable-oslogin` / `enable-oslogin-2fa` metadata is not set explicitly but is inherited automatically from the `constraints/compute.requireOsLogin` org policy. The policy controller writes the inherited value in lowercase (`"true"`), but the service-layer parser compared it to the uppercase string literal `"TRUE"`. Comparison is now case-insensitive [(#11341)](https://github.com/prowler-cloud/prowler/pull/11341) +- `storage_smb_channel_encryption_with_secure_algorithm` check for Azure provider no longer passes when a storage account allows a weak SMB channel encryption algorithm (e.g. `AES-128-CCM`/`AES-128-GCM`) alongside `AES-256-GCM`; it now requires every enabled algorithm to be in the recommended list, configurable via `azure.recommended_smb_channel_encryption_algorithms` (defaults to `AES-256-GCM` only, as required by CIS) [(#11327)](https://github.com/prowler-cloud/prowler/pull/11327) +- Azure and M365 providers crashing with `RuntimeError: There is no current event loop` on Python 3.12 when called from threads without an active event loop (e.g. Celery workers) [(#11360)](https://github.com/prowler-cloud/prowler/pull/11360) + +--- + +## [5.28.0] (Prowler v5.28.0) + +### 🚀 Added + +- Sites, Additional Google services, and Marketplace checks for Google Workspace provider using the Cloud Identity Policy API [(#11281)](https://github.com/prowler-cloud/prowler/pull/11281) +- `entra_app_registration_client_secret_unused` check for M365 provider [(#11232)](https://github.com/prowler-cloud/prowler/pull/11232) +- `cloudsql_instance_cmek_encryption_enabled` check for GCP provider [(#11023)](https://github.com/prowler-cloud/prowler/pull/11023) +- Google Workspace Groups service with 3 new checks [(#11186)](https://github.com/prowler-cloud/prowler/pull/11186) +- `ses_identity_dkim_enabled` check for AWS provider [(#10923)](https://github.com/prowler-cloud/prowler/pull/10923) +- `sagemaker_models_registry_in_use` check for AWS provider, verifying that at least one SageMaker Model Package Group has an approved model package to enforce ML governance workflows [(#11196)](https://github.com/prowler-cloud/prowler/pull/11196) +- `signon_dod_warning_banner_configured`, `signon_global_session_lifetime_18h`, `signon_global_session_cookies_not_persistent` and `signon_global_session_policy_network_zone_enforced` checks for Okta provider [(#11224)](https://github.com/prowler-cloud/prowler/pull/11224) + +### 🔄 Changed + +- `OktaProvider.test_connection` accepts an optional `provider_id` (org domain) and raises `OktaInvalidProviderIdError` (14007) when it doesn't match the authenticated org — guards against stored UID drifting from the credentials' org [(#11184)](https://github.com/prowler-cloud/prowler/pull/11184) +- Use single-quoted strings for credential variables in the M365 provider PowerShell session, following PowerShell best practices for literal values [(#9997)](https://github.com/prowler-cloud/prowler/pull/9997) + +### 🐞 Fixed + +- OCI Audit service configuration lookup when the configured region differs from the tenancy home region [(#10347)](https://github.com/prowler-cloud/prowler/pull/10347) +- Container image now uses an absolute `ENTRYPOINT` (`/home/prowler/.venv/bin/prowler`) so it works under any runtime `--workdir`. The relative entrypoint was breaking the official GitHub Action (`prowler-cloud/prowler@v5.27.0`) and any `docker run` with a custom `-w` [(#11313)](https://github.com/prowler-cloud/prowler/pull/11313) + +--- + +## [5.27.1] (Prowler v5.27.1) + +### 🐞 Fixed + +- `s3_bucket_shadow_resource_vulnerability` no longer emits a tautological `PASS` finding for every bucket; a finding is now produced only when the bucket name matches one of the predictable service patterns (Glue, SageMaker, EMR, CodeStar) [(#11220)](https://github.com/prowler-cloud/prowler/pull/11220) +- `sqlserver_tde_encrypted_with_cmk` check for Azure provider no longer reports a false `FAIL` for SQL Servers whose user databases are correctly encrypted with a customer-managed key, by excluding the system `master` database (always reports TDE `Disabled` and is not customer-controllable) from the TDE evaluation [(#11233)](https://github.com/prowler-cloud/prowler/pull/11233) + +--- + +## [5.27.0] (Prowler v5.27.0) + +### 🚀 Added + +- 6 Chat file sharing, external messaging, spaces, and apps access checks for Google Workspace provider using the Cloud Identity Policy API [(#11126)](https://github.com/prowler-cloud/prowler/pull/11126) +- `entra_service_principal_no_secrets_for_permanent_tier0_roles` check for M365 provider [(#10788)](https://github.com/prowler-cloud/prowler/pull/10788) +- `iam_user_access_not_stale_to_sagemaker` check for AWS provider with configurable `max_unused_sagemaker_access_days` (default 90) [(#11000)](https://github.com/prowler-cloud/prowler/pull/11000) +- `cloudtrail_bedrock_logging_enabled` check for AWS provider [(#10858)](https://github.com/prowler-cloud/prowler/pull/10858) +- Okta provider with OAuth 2.0 authentication and `signon_global_session_idle_timeout_15min` check [(#11079)](https://github.com/prowler-cloud/prowler/pull/11079) +- `sagemaker_domain_sso_configured` check for AWS provider [(#11094)](https://github.com/prowler-cloud/prowler/pull/11094) +- Scaleway provider with `iam_api_keys_no_root_owned` check [(#11166)](https://github.com/prowler-cloud/prowler/pull/11166) + +### 🔄 Changed + +- `entra_emergency_access_exclusion` check for M365 provider now scopes the exclusion requirement to enabled Conditional Access policies with a `Block` grant control instead of every enabled policy, focusing on the lockout-relevant policy set [(#10849)](https://github.com/prowler-cloud/prowler/pull/10849) +- AWS IAM customer-managed policy checks no longer emit `FAIL` on unattached policies unless `--scan-unused-services` is enabled [(#11150)](https://github.com/prowler-cloud/prowler/pull/11150) +- Replace `poetry` with `uv` as package manager [(#11162)](https://github.com/prowler-cloud/prowler/pull/11162) +- Replace `safety` with `osv-scanner` for dependency vulnerability scanning in SDK CI and pre-commit [(#11167)](https://github.com/prowler-cloud/prowler/pull/11167) + +### 🐞 Fixed + +- Google Workspace Directory checks sharing a single resource row, causing the service field to be overwritten by the last check executed [(#11176)](https://github.com/prowler-cloud/prowler/pull/11176) +- Google Workspace Calendar and Drive services sharing a single resource row, causing the service field to be overwritten by the last check executed [(#11161)](https://github.com/prowler-cloud/prowler/pull/11161) +- `zone_waf_enabled` check for Cloudflare provider now appends a plan-aware hint to the FAIL `status_extended`: a possible-false-positive note on paid plans (Pro, Business, Enterprise) where the legacy `waf` zone setting can read `off` even though WAF managed rulesets are deployed via the dashboard, and a "not available on the Cloudflare Free plan" note on Free zones [(#9896)](https://github.com/prowler-cloud/prowler/pull/9896) +- Google Workspace Gmail checks sharing a single resource row, causing the service field to be overwritten by the last check executed [(#11169)](https://github.com/prowler-cloud/prowler/pull/11169) +- Google Workspace Drive and Calendar services missing server-side policy filters [(#11195)](https://github.com/prowler-cloud/prowler/pull/11195) +- `entra_users_mfa_capable` and `entra_break_glass_account_fido2_security_key_registered` report a preventive FAIL per affected user (with the missing permission named) when the M365 service principal lacks `AuditLog.Read.All`, instead of mass false positives [(#10907)](https://github.com/prowler-cloud/prowler/pull/10907) +- Duplicated GCP CIS requirements IDs [(#11180)](https://github.com/prowler-cloud/prowler/pull/11180) +- `VercelSession.token` is now excluded from serialization and representation to prevent the Vercel API token from leaking through `.dict()`, `.json()` or logs [(#11198)](https://github.com/prowler-cloud/prowler/pull/11198) + +--- + +## [5.26.1] (Prowler v5.26.1) + +### 🐞 Fixed + +- `entra_users_mfa_capable` no longer flags disabled guest users by requesting `accountEnabled` and `userType` from Microsoft Graph via `$select` and using Graph as the source of truth for `account_enabled` (EXO `Get-User` does not return guest users) [(#11002)](https://github.com/prowler-cloud/prowler/pull/11002) + +--- + +## [5.26.0] (Prowler v5.26.0) ### 🚀 Added - Support for external/custom providers, checks, and compliance frameworks without modifying core code [(#10700)](https://github.com/prowler-cloud/prowler/pull/10700) - `bedrock_guardrails_configured` check for AWS provider [(#10844)](https://github.com/prowler-cloud/prowler/pull/10844) -- Universal compliance pipeline integrated into the CLI: `--list-compliance` and `--list-compliance-requirements` show universal frameworks, and CSV plus OCSF outputs are generated for any framework declaring a `TableConfig` [(#10301)](https://github.com/prowler-cloud/prowler/pull/10301) +- Universal compliance with OCSF support [(#10301)](https://github.com/prowler-cloud/prowler/pull/10301) - ASD Essential Eight Maturity Model compliance framework for AWS (Maturity Level One, Nov 2023) [(#10808)](https://github.com/prowler-cloud/prowler/pull/10808) -- Update Vercel checks to return personalized finding status extended depending on billing plan and classify them with billing-plan categories [(#10663)](https://github.com/prowler-cloud/prowler/pull/10663) +- Vercel checks to return personalized finding status extended depending on billing plan and classify them with billing-plan categories [(#10663)](https://github.com/prowler-cloud/prowler/pull/10663) - `bedrock_prompt_management_exists` check for AWS provider [(#10878)](https://github.com/prowler-cloud/prowler/pull/10878) +- 8 Gmail attachment safety and spoofing protection checks for Google Workspace provider using the Cloud Identity Policy API [(#10980)](https://github.com/prowler-cloud/prowler/pull/10980) +- `bedrock_prompt_encrypted_with_cmk` check for AWS provider [(#10905)](https://github.com/prowler-cloud/prowler/pull/10905) ### 🔄 Changed @@ -19,16 +119,28 @@ All notable changes to the **Prowler SDK** are documented in this file. - Azure compliance entries for legacy Network Watcher flow log controls now use retirement-aware guidance and point new deployments to VNet flow logs [(#10937)](https://github.com/prowler-cloud/prowler/pull/10937) - AWS CodeBuild service now batches `BatchGetProjects` and `BatchGetBuilds` calls per region (up to 100 items per call) to reduce API call volume and prevent throttling-induced false positives in `codebuild_project_not_publicly_accessible` [(#10639)](https://github.com/prowler-cloud/prowler/pull/10639) - `display_compliance_table` dispatch switched from substring `in` checks to `startswith` to prevent false matches between similarly named frameworks (e.g. `cisa` vs `cis`) [(#10301)](https://github.com/prowler-cloud/prowler/pull/10301) +- Restore the `ec2-imdsv1` category for EC2 IMDS checks to keep Attack Surface and findings filters aligned [(#10998)](https://github.com/prowler-cloud/prowler/pull/10998) +- Container image CVE findings and IaC findings now use official CVE, Prowler Hub, or GitHub Security Advisory URLs instead of Aqua advisory URLs in remediation and references; Trivy rule IDs map to Prowler Hub without the `AVD-` prefix so links resolve [(#10853)](https://github.com/prowler-cloud/prowler/pull/10853) ### 🐞 Fixed - AWS SDK test isolation: autouse `mock_aws` fixture and leak detector in `conftest.py` to prevent tests from hitting real AWS endpoints, with idempotent organization setup for tests calling `set_mocked_aws_provider` multiple times [(#10605)](https://github.com/prowler-cloud/prowler/pull/10605) - AWS `boto` user agent extra is now applied to every client [(#10944)](https://github.com/prowler-cloud/prowler/pull/10944) - Image provider connection check no longer fails with a misleading `host='https'` resolution error when the registry URL includes an `http://` or `https://` scheme prefix [(#10950)](https://github.com/prowler-cloud/prowler/pull/10950) +- Azure subscriptions sharing the same display name are no longer collapsed into a single identity entry, so every subscription is scanned [(#10718)](https://github.com/prowler-cloud/prowler/pull/10718) ### 🔐 Security - Parser-mismatch SSRF in image provider registry auth where crafted bearer-token realms and pagination links could force requests to internal addresses and leak credentials cross-origin [(#10945)](https://github.com/prowler-cloud/prowler/pull/10945) +- `cryptography` from 46.0.6 to 46.0.7 and `trivy` binary from 0.69.2 to 0.70.0 in the SDK image for CVE-2026-39892 and CVE-2026-33186 [(#10978)](https://github.com/prowler-cloud/prowler/pull/10978) + +--- + +## [5.25.3] (Prowler v5.25.3) + +### 🐞 Fixed + +- Oracle Cloud identity scans known or supplied regions to better support non Ashburn tenancies [(#10529)](https://github.com/prowler-cloud/prowler/pull/10529) --- diff --git a/prowler/__main__.py b/prowler/__main__.py index 8fc7b5f6d2..b147180e21 100644 --- a/prowler/__main__.py +++ b/prowler/__main__.py @@ -56,6 +56,9 @@ from prowler.lib.check.models import CheckMetadata from prowler.lib.cli.parser import ProwlerArgumentParser from prowler.lib.logger import logger, set_logging_config from prowler.lib.outputs.asff.asff import ASFF +from prowler.lib.outputs.compliance.asd_essential_eight.asd_essential_eight_aws import ( + ASDEssentialEightAWS, +) from prowler.lib.outputs.compliance.aws_well_architected.aws_well_architected import ( AWSWellArchitected, ) @@ -89,9 +92,6 @@ from prowler.lib.outputs.compliance.csa.csa_oraclecloud import OracleCloudCSA from prowler.lib.outputs.compliance.ens.ens_aws import AWSENS from prowler.lib.outputs.compliance.ens.ens_azure import AzureENS from prowler.lib.outputs.compliance.ens.ens_gcp import GCPENS -from prowler.lib.outputs.compliance.essential_eight.essential_eight_aws import ( - EssentialEightAWS, -) from prowler.lib.outputs.compliance.generic.generic import GenericCompliance from prowler.lib.outputs.compliance.iso27001.iso27001_aws import AWSISO27001 from prowler.lib.outputs.compliance.iso27001.iso27001_azure import AzureISO27001 @@ -153,8 +153,10 @@ from prowler.providers.llm.models import LLMOutputOptions from prowler.providers.m365.models import M365OutputOptions from prowler.providers.mongodbatlas.models import MongoDBAtlasOutputOptions from prowler.providers.nhn.models import NHNOutputOptions +from prowler.providers.okta.models import OktaOutputOptions from prowler.providers.openstack.models import OpenStackOutputOptions from prowler.providers.oraclecloud.models import OCIOutputOptions +from prowler.providers.scaleway.models import ScalewayOutputOptions from prowler.providers.vercel.models import VercelOutputOptions @@ -426,6 +428,14 @@ def prowler(): output_options = VercelOutputOptions( args, bulk_checks_metadata, global_provider.identity ) + elif provider == "okta": + output_options = OktaOutputOptions( + args, bulk_checks_metadata, global_provider.identity + ) + elif provider == "scaleway": + output_options = ScalewayOutputOptions( + args, bulk_checks_metadata, global_provider.identity + ) else: # Dynamic fallback: any external/custom provider output_options = global_provider.get_output_options(args, bulk_checks_metadata) @@ -686,18 +696,18 @@ def prowler(): ) generated_outputs["compliance"].append(cis) cis.batch_write_data_to_file() - elif compliance_name.startswith("essential_eight"): + elif compliance_name.startswith("asd_essential_eight"): filename = ( f"{output_options.output_directory}/compliance/" f"{output_options.output_filename}_{compliance_name}.csv" ) - essential_eight = EssentialEightAWS( + asd_essential_eight = ASDEssentialEightAWS( findings=finding_outputs, compliance=bulk_compliance_frameworks[compliance_name], file_path=filename, ) - generated_outputs["compliance"].append(essential_eight) - essential_eight.batch_write_data_to_file() + generated_outputs["compliance"].append(asd_essential_eight) + asd_essential_eight.batch_write_data_to_file() elif compliance_name == "mitre_attack_aws": # Generate MITRE ATT&CK Finding Object filename = ( diff --git a/prowler/compliance/aws/essential_eight_aws.json b/prowler/compliance/aws/asd_essential_eight_aws.json similarity index 99% rename from prowler/compliance/aws/essential_eight_aws.json rename to prowler/compliance/aws/asd_essential_eight_aws.json index 46164ebba5..00b39817e3 100644 --- a/prowler/compliance/aws/essential_eight_aws.json +++ b/prowler/compliance/aws/asd_essential_eight_aws.json @@ -1,5 +1,5 @@ { - "Framework": "Essential-Eight", + "Framework": "ASD-Essential-Eight", "Name": "ASD Essential Eight Maturity Model - Maturity Level One (AWS)", "Version": "Nov 2023", "Provider": "AWS", diff --git a/prowler/compliance/aws/aws_ai_security_framework_aws.json b/prowler/compliance/aws/aws_ai_security_framework_aws.json new file mode 100644 index 0000000000..c6a0a8504b --- /dev/null +++ b/prowler/compliance/aws/aws_ai_security_framework_aws.json @@ -0,0 +1,1160 @@ +{ + "Framework": "AWS-AI-Security-Framework", + "Name": "AWS AI Security Framework", + "Version": "1.0", + "Provider": "AWS", + "Description": "Security compliance framework based on the AWS AI Security Framework blog post (2025). Organizes controls across three security layers (Infrastructure, Identity & Data, AI Application), three deployment phases (Foundational, Enhanced, Advanced), and three AI use cases (AI that Answers, AI that Connects, AI that Acts). Maps existing Prowler checks to AI workload security requirements and identifies gaps requiring new checks.", + "Requirements": [ + { + "Id": "AISF-INFRA-01", + "Description": "Ensure VPC endpoints provide private connectivity for Bedrock APIs, preventing AI traffic from traversing the public internet.", + "Name": "Bedrock VPC Private Connectivity", + "Attributes": [ + { + "Section": "Infrastructure Security", + "SubSection": "Network Isolation", + "Service": "bedrock", + "Type": "Automated" + } + ], + "Checks": [ + "bedrock_vpc_endpoints_configured" + ] + }, + { + "Id": "AISF-INFRA-02", + "Description": "Ensure VPCs have Network Firewall enabled to inspect and filter AI workload traffic, with logging, multi-AZ deployment, and proper default actions for both full and fragmented packets.", + "Name": "Network Firewall for AI Workloads", + "Attributes": [ + { + "Section": "Infrastructure Security", + "SubSection": "Network Firewall", + "Service": "networkfirewall", + "Type": "Automated" + } + ], + "Checks": [ + "networkfirewall_in_all_vpc", + "networkfirewall_logging_enabled", + "networkfirewall_multi_az", + "networkfirewall_policy_default_action_full_packets", + "networkfirewall_policy_default_action_fragmented_packets", + "networkfirewall_policy_rule_group_associated", + "networkfirewall_deletion_protection" + ] + }, + { + "Id": "AISF-INFRA-03", + "Description": "Ensure WAFv2 Web ACLs are configured with rules and logging to protect AI application endpoints from HTTP-based attacks including prompt injection patterns at the perimeter.", + "Name": "WAF Protection for AI Endpoints", + "Attributes": [ + { + "Section": "Infrastructure Security", + "SubSection": "Web Application Firewall", + "Service": "wafv2", + "Type": "Automated" + } + ], + "Checks": [ + "wafv2_webacl_with_rules", + "wafv2_webacl_logging_enabled", + "wafv2_webacl_rule_logging_enabled", + "apigateway_restapi_waf_acl_attached", + "cognito_user_pool_waf_acl_attached" + ] + }, + { + "Id": "AISF-INFRA-04", + "Description": "Ensure AWS Shield Advanced is enabled to protect internet-facing AI application infrastructure from DDoS attacks.", + "Name": "DDoS Protection for AI Infrastructure", + "Attributes": [ + { + "Section": "Infrastructure Security", + "SubSection": "DDoS Protection", + "Service": "shield", + "Type": "Automated" + } + ], + "Checks": [ + "shield_advanced_protection_in_cloudfront_distributions", + "shield_advanced_protection_in_internet_facing_load_balancers", + "shield_advanced_protection_in_classic_load_balancers", + "shield_advanced_protection_in_route53_hosted_zones", + "shield_advanced_protection_in_associated_elastic_ips", + "shield_advanced_protection_in_global_accelerators" + ] + }, + { + "Id": "AISF-INFRA-05", + "Description": "Ensure all data at rest is encrypted with AES-256 across AI workload storage including S3 buckets, EBS volumes, RDS instances, SageMaker notebooks, and Bedrock prompts using customer-managed KMS keys where possible.", + "Name": "Encryption at Rest for AI Data", + "Attributes": [ + { + "Section": "Infrastructure Security", + "SubSection": "Encryption at Rest", + "Service": "kms", + "Type": "Automated" + } + ], + "Checks": [ + "s3_bucket_default_encryption", + "s3_bucket_kms_encryption", + "ec2_ebs_default_encryption", + "ec2_ebs_volume_encryption", + "rds_instance_storage_encrypted", + "sagemaker_notebook_instance_encryption_enabled", + "sagemaker_training_jobs_volume_and_output_encryption_enabled", + "bedrock_model_invocation_logs_encryption_enabled", + "cloudtrail_kms_encryption_enabled", + "cloudwatch_log_group_kms_encryption_enabled", + "eks_cluster_kms_cmk_encryption_in_secrets_enabled", + "dynamodb_tables_kms_cmk_encryption_enabled", + "sns_topics_kms_encryption_at_rest_enabled", + "sqs_queues_server_side_encryption_enabled" + ] + }, + { + "Id": "AISF-INFRA-06", + "Description": "Ensure all data in transit uses TLS 1.2 or higher, including API communications, inter-container traffic for ML training, and connections between AI application components.", + "Name": "Encryption in Transit for AI Workloads", + "Attributes": [ + { + "Section": "Infrastructure Security", + "SubSection": "Encryption in Transit", + "Service": "multiple", + "Type": "Automated" + } + ], + "Checks": [ + "s3_bucket_secure_transport_policy", + "sagemaker_training_jobs_intercontainer_encryption_enabled", + "elbv2_ssl_listeners", + "elbv2_insecure_ssl_ciphers", + "cloudfront_distributions_https_enabled", + "cloudfront_distributions_using_deprecated_ssl_protocols", + "opensearch_service_domains_https_communications_enforced", + "opensearch_service_domains_node_to_node_encryption_enabled", + "apigateway_restapi_client_certificate_enabled" + ] + }, + { + "Id": "AISF-INFRA-07", + "Description": "Ensure customer-managed KMS keys are in use, rotation is enabled, and keys are not scheduled for unintentional deletion to maintain control over AI data encryption.", + "Name": "Customer-Managed Key Governance", + "Attributes": [ + { + "Section": "Infrastructure Security", + "SubSection": "Key Management", + "Service": "kms", + "Type": "Automated" + } + ], + "Checks": [ + "kms_cmk_are_used", + "kms_cmk_rotation_enabled", + "kms_cmk_not_deleted_unintentionally", + "kms_key_not_publicly_accessible" + ] + }, + { + "Id": "AISF-INFRA-08", + "Description": "Ensure VPC endpoints enforce trust boundaries, subnets do not assign public IPs by default, and flow logs are enabled for all VPCs hosting AI workloads.", + "Name": "VPC Security for AI Workloads", + "Attributes": [ + { + "Section": "Infrastructure Security", + "SubSection": "VPC Security", + "Service": "vpc", + "Type": "Automated" + } + ], + "Checks": [ + "vpc_flow_logs_enabled", + "vpc_subnet_no_public_ip_by_default", + "vpc_subnet_separate_private_public", + "vpc_endpoint_connections_trust_boundaries", + "vpc_endpoint_services_allowed_principals_trust_boundaries", + "vpc_endpoint_for_ec2_enabled", + "vpc_peering_routing_tables_with_least_privilege" + ] + }, + { + "Id": "AISF-INFRA-09", + "Description": "Ensure hardware-enforced compute isolation is in use for AI workloads. AWS Nitro System provides isolation with no operator access to customer data during model inference and training.", + "Name": "Hardware-Enforced Compute Isolation", + "Attributes": [ + { + "Section": "Infrastructure Security", + "SubSection": "Compute Isolation", + "Service": "ec2", + "Type": "Manual" + } + ], + "Checks": [] + }, + { + "Id": "AISF-IAM-01", + "Description": "Ensure MFA is enforced for all users accessing AI services, including root account hardware MFA, IAM user MFA for console access, and Cognito user pool MFA for AI application end users.", + "Name": "Multi-Factor Authentication for AI Access", + "Attributes": [ + { + "Section": "Identity and Data Security", + "SubSection": "Authentication", + "Service": "iam", + "Type": "Automated" + } + ], + "Checks": [ + "iam_root_mfa_enabled", + "iam_root_hardware_mfa_enabled", + "iam_user_mfa_enabled_console_access", + "iam_user_hardware_mfa_enabled", + "iam_administrator_access_with_mfa", + "cognito_user_pool_mfa_enabled", + "cognito_user_pool_advanced_security_enabled", + "cognito_user_pool_blocks_compromised_credentials_sign_in_attempts", + "cognito_user_pool_blocks_potential_malicious_sign_in_attempts" + ] + }, + { + "Id": "AISF-IAM-02", + "Description": "Ensure least-privilege access is enforced for all AI service identities. No administrative privileges should be attached to IAM entities that interact with Bedrock, SageMaker, or other AI services.", + "Name": "Least Privilege for AI Identities", + "Attributes": [ + { + "Section": "Identity and Data Security", + "SubSection": "Authorization", + "Service": "iam", + "Type": "Automated" + } + ], + "Checks": [ + "iam_aws_attached_policy_no_administrative_privileges", + "iam_customer_attached_policy_no_administrative_privileges", + "iam_inline_policy_no_administrative_privileges", + "iam_policy_allows_privilege_escalation", + "iam_inline_policy_allows_privilege_escalation", + "iam_role_administratoraccess_policy", + "iam_user_administrator_access_policy", + "iam_group_administrator_access_policy", + "iam_policy_attached_only_to_group_or_roles", + "iam_no_custom_policy_permissive_role_assumption", + "bedrock_full_access_policy_attached", + "bedrock_api_key_no_administrative_privileges" + ] + }, + { + "Id": "AISF-IAM-03", + "Description": "Ensure temporary and scoped credentials are used for AI service access. Long-term access keys should be avoided, rotated within 90 days when necessary, and unused keys should be disabled.", + "Name": "Temporary Scoped Credentials", + "Attributes": [ + { + "Section": "Identity and Data Security", + "SubSection": "Credential Management", + "Service": "iam", + "Type": "Automated" + } + ], + "Checks": [ + "iam_user_with_temporary_credentials", + "iam_rotate_access_key_90_days", + "iam_user_accesskey_unused", + "iam_user_no_setup_initial_access_key", + "iam_user_two_active_access_key", + "iam_user_console_access_unused", + "bedrock_api_key_no_long_term_credentials" + ] + }, + { + "Id": "AISF-IAM-04", + "Description": "Ensure root account is properly secured with no active access keys and minimal usage, as root credentials in AI environments could grant unrestricted access to all AI models, data, and agent configurations.", + "Name": "Root Account Security", + "Attributes": [ + { + "Section": "Identity and Data Security", + "SubSection": "Root Account", + "Service": "iam", + "Type": "Automated" + } + ], + "Checks": [ + "iam_no_root_access_key", + "iam_avoid_root_usage", + "iam_root_credentials_management_enabled", + "cloudwatch_log_metric_filter_root_usage" + ] + }, + { + "Id": "AISF-IAM-05", + "Description": "Ensure IAM roles used for AI services prevent cross-service confused deputy attacks and stale access to Bedrock and SageMaker is reviewed regularly.", + "Name": "AI Service Role Security", + "Attributes": [ + { + "Section": "Identity and Data Security", + "SubSection": "Service Roles", + "Service": "iam", + "Type": "Automated" + } + ], + "Checks": [ + "iam_role_cross_service_confused_deputy_prevention", + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", + "iam_role_cross_account_readonlyaccess_policy" + ] + }, + { + "Id": "AISF-IAM-06", + "Description": "Ensure strong password policies are enforced with minimum length, complexity requirements, expiration, and reuse prevention for all human identities accessing AI services.", + "Name": "Password Policy for AI Service Access", + "Attributes": [ + { + "Section": "Identity and Data Security", + "SubSection": "Password Policy", + "Service": "iam", + "Type": "Automated" + } + ], + "Checks": [ + "iam_password_policy_minimum_length_14", + "iam_password_policy_uppercase", + "iam_password_policy_lowercase", + "iam_password_policy_number", + "iam_password_policy_symbol", + "iam_password_policy_reuse_24", + "iam_password_policy_expires_passwords_within_90_days_or_less", + "cognito_user_pool_password_policy_minimum_length_14", + "cognito_user_pool_password_policy_lowercase", + "cognito_user_pool_password_policy_uppercase", + "cognito_user_pool_password_policy_number", + "cognito_user_pool_password_policy_symbol" + ] + }, + { + "Id": "AISF-IAM-07", + "Description": "Ensure Cognito user pools are properly configured for AI application user authentication with advanced security features, token revocation, self-registration controls, and WAF protection.", + "Name": "Cognito User Authentication for AI Apps", + "Attributes": [ + { + "Section": "Identity and Data Security", + "SubSection": "User Authentication", + "Service": "cognito", + "Type": "Automated" + } + ], + "Checks": [ + "cognito_user_pool_mfa_enabled", + "cognito_user_pool_advanced_security_enabled", + "cognito_user_pool_self_registration_disabled", + "cognito_user_pool_deletion_protection_enabled", + "cognito_user_pool_client_token_revocation_enabled", + "cognito_user_pool_client_prevent_user_existence_errors", + "cognito_user_pool_temporary_password_expiration", + "cognito_user_pool_waf_acl_attached", + "cognito_identity_pool_guest_access_disabled" + ] + }, + { + "Id": "AISF-IAM-08", + "Description": "Ensure API Gateway endpoints serving AI applications have proper authorization configured to authenticate and authorize every request to the model layer.", + "Name": "API Authorization for AI Endpoints", + "Attributes": [ + { + "Section": "Identity and Data Security", + "SubSection": "API Authorization", + "Service": "apigateway", + "Type": "Automated" + } + ], + "Checks": [ + "apigateway_restapi_authorizers_enabled", + "apigateway_restapi_public_with_authorizer", + "apigatewayv2_api_authorizers_enabled" + ] + }, + { + "Id": "AISF-DATA-01", + "Description": "Ensure Amazon Macie is enabled with automated sensitive data discovery to classify and protect enterprise data before it is made available to AI systems through RAG or other patterns.", + "Name": "Data Classification for AI", + "Attributes": [ + { + "Section": "Identity and Data Security", + "SubSection": "Data Classification", + "Service": "macie", + "Type": "Automated" + } + ], + "Checks": [ + "macie_is_enabled", + "macie_automated_sensitive_data_discovery_enabled" + ] + }, + { + "Id": "AISF-DATA-02", + "Description": "Ensure IAM Access Analyzer is enabled to validate access policies and identify unintended access to resources used by AI workloads.", + "Name": "Access Analysis for AI Resources", + "Attributes": [ + { + "Section": "Identity and Data Security", + "SubSection": "Access Analysis", + "Service": "accessanalyzer", + "Type": "Automated" + } + ], + "Checks": [ + "accessanalyzer_enabled", + "accessanalyzer_enabled_without_findings" + ] + }, + { + "Id": "AISF-DATA-03", + "Description": "Ensure Secrets Manager is used for AI application credentials with automatic rotation enabled, restrictive resource policies, and no public access.", + "Name": "Secrets Management for AI Workloads", + "Attributes": [ + { + "Section": "Identity and Data Security", + "SubSection": "Secrets Management", + "Service": "secretsmanager", + "Type": "Automated" + } + ], + "Checks": [ + "secretsmanager_automatic_rotation_enabled", + "secretsmanager_secret_rotated_periodically", + "secretsmanager_has_restrictive_resource_policy", + "secretsmanager_not_publicly_accessible", + "secretsmanager_secret_unused" + ] + }, + { + "Id": "AISF-DATA-04", + "Description": "Ensure S3 buckets used for AI training data, model artifacts, RAG knowledge bases, and inference logs have public access blocked, encryption enabled, secure transport enforced, and access logging configured.", + "Name": "S3 Data Protection for AI", + "Attributes": [ + { + "Section": "Identity and Data Security", + "SubSection": "Storage Security", + "Service": "s3", + "Type": "Automated" + } + ], + "Checks": [ + "s3_account_level_public_access_blocks", + "s3_bucket_level_public_access_block", + "s3_bucket_public_access", + "s3_bucket_policy_public_write_access", + "s3_bucket_default_encryption", + "s3_bucket_kms_encryption", + "s3_bucket_secure_transport_policy", + "s3_bucket_server_access_logging_enabled", + "s3_bucket_object_versioning", + "s3_bucket_acl_prohibited", + "s3_bucket_cross_account_access" + ] + }, + { + "Id": "AISF-DATA-05", + "Description": "Ensure no secrets or credentials are hardcoded in Lambda functions, ECS task definitions, or EC2 instances used as part of AI application architectures.", + "Name": "No Hardcoded Secrets in AI Workloads", + "Attributes": [ + { + "Section": "Identity and Data Security", + "SubSection": "Secret Hygiene", + "Service": "multiple", + "Type": "Automated" + } + ], + "Checks": [ + "awslambda_function_no_secrets_in_code", + "awslambda_function_no_secrets_in_variables", + "ecs_task_definitions_no_environment_secrets", + "ec2_instance_secrets_user_data", + "cloudwatch_log_group_no_secrets_in_logs" + ] + }, + { + "Id": "AISF-DATA-06", + "Description": "Ensure the AWS Organization has opted out of all AI services data usage and child accounts cannot override this policy, preventing AWS from using customer data for AI service improvement.", + "Name": "AI Services Data Opt-Out", + "Attributes": [ + { + "Section": "Identity and Data Security", + "SubSection": "Data Governance", + "Service": "organizations", + "Type": "Automated" + } + ], + "Checks": [ + "organizations_opt_out_ai_services_policy" + ] + }, + { + "Id": "AISF-AI-01", + "Description": "Ensure Amazon Bedrock has at least one guardrail configured to provide content filtering, prompt injection defense, PII filtering, and topic restrictions for foundation model interactions.", + "Name": "Bedrock Guardrails Configuration", + "Attributes": [ + { + "Section": "AI Application Security", + "SubSection": "Content Filtering", + "Service": "bedrock", + "Type": "Automated" + } + ], + "Checks": [ + "bedrock_guardrails_configured" + ] + }, + { + "Id": "AISF-AI-02", + "Description": "Ensure Bedrock guardrails have prompt attack filter strength set to HIGH to detect and block prompt injection attempts, the #1 risk in OWASP Top 10 for LLM Applications.", + "Name": "Prompt Injection Defense", + "Attributes": [ + { + "Section": "AI Application Security", + "SubSection": "Prompt Security", + "Service": "bedrock", + "Type": "Automated" + } + ], + "Checks": [ + "bedrock_guardrail_prompt_attack_filter_enabled" + ] + }, + { + "Id": "AISF-AI-03", + "Description": "Ensure Bedrock guardrails block or mask sensitive information (PII) in both model inputs and outputs to prevent data leakage through AI responses.", + "Name": "PII and Sensitive Data Filtering", + "Attributes": [ + { + "Section": "AI Application Security", + "SubSection": "Output Filtering", + "Service": "bedrock", + "Type": "Automated" + } + ], + "Checks": [ + "bedrock_guardrail_sensitive_information_filter_enabled" + ] + }, + { + "Id": "AISF-AI-04", + "Description": "Ensure all Bedrock agents have guardrails enabled to protect agent sessions from prompt injection, data exfiltration, and unauthorized actions during agentic workflows.", + "Name": "Agent Guardrail Protection", + "Attributes": [ + { + "Section": "AI Application Security", + "SubSection": "Agent Security", + "Service": "bedrock", + "Type": "Automated" + } + ], + "Checks": [ + "bedrock_agent_guardrail_enabled" + ] + }, + { + "Id": "AISF-AI-05", + "Description": "Ensure Bedrock model invocation logging is enabled to maintain an immutable audit trail of all model interactions, enabling incident investigation and behavioral analysis.", + "Name": "Model Invocation Logging", + "Attributes": [ + { + "Section": "AI Application Security", + "SubSection": "AI Audit Logging", + "Service": "bedrock", + "Type": "Automated" + } + ], + "Checks": [ + "bedrock_model_invocation_logging_enabled", + "bedrock_model_invocation_logs_encryption_enabled" + ] + }, + { + "Id": "AISF-AI-06", + "Description": "Ensure CloudTrail is configured to log all Bedrock API calls for security auditing, enabling detection of unauthorized model access, configuration changes, and potential LLM jacking.", + "Name": "Bedrock API Audit Trail", + "Attributes": [ + { + "Section": "AI Application Security", + "SubSection": "AI Audit Logging", + "Service": "cloudtrail", + "Type": "Automated" + } + ], + "Checks": [ + "cloudtrail_threat_detection_llm_jacking" + ] + }, + { + "Id": "AISF-AI-07", + "Description": "Ensure Bedrock prompts are encrypted at rest with customer-managed KMS keys and Prompt Management is used for centralized prompt governance.", + "Name": "Prompt Encryption and Management", + "Attributes": [ + { + "Section": "AI Application Security", + "SubSection": "Prompt Management", + "Service": "bedrock", + "Type": "Automated" + } + ], + "Checks": [ + "bedrock_prompt_encrypted_with_cmk", + "bedrock_prompt_management_exists" + ] + }, + { + "Id": "AISF-AI-08", + "Description": "Ensure Bedrock Automated Reasoning Checks are configured to provide formal verification of model responses against source documents, achieving up to 99% verification accuracy against hallucinations.", + "Name": "Automated Reasoning for Output Validation", + "Attributes": [ + { + "Section": "AI Application Security", + "SubSection": "Output Validation", + "Service": "bedrock", + "Type": "Manual" + } + ], + "Checks": [] + }, + { + "Id": "AISF-AI-09", + "Description": "Ensure Bedrock Contextual Grounding is configured to validate semantic consistency of model responses against sanctioned source documents, preventing hallucinated or fabricated outputs.", + "Name": "Contextual Grounding for RAG Validation", + "Attributes": [ + { + "Section": "AI Application Security", + "SubSection": "Output Validation", + "Service": "bedrock", + "Type": "Manual" + } + ], + "Checks": [] + }, + { + "Id": "AISF-AI-10", + "Description": "Ensure Bedrock Knowledge Bases used for RAG patterns have proper security controls including encryption with customer-managed keys and VPC configuration for private data access.", + "Name": "Knowledge Base Security for RAG", + "Attributes": [ + { + "Section": "AI Application Security", + "SubSection": "RAG Security", + "Service": "bedrock", + "Type": "Manual" + } + ], + "Checks": [] + }, + { + "Id": "AISF-AI-11", + "Description": "Ensure WAF AI Activity Dashboard is configured to monitor and analyze AI-specific traffic patterns, providing visibility into potential attacks targeting AI endpoints.", + "Name": "WAF AI Activity Monitoring", + "Attributes": [ + { + "Section": "AI Application Security", + "SubSection": "AI Traffic Monitoring", + "Service": "wafv2", + "Type": "Manual" + } + ], + "Checks": [] + }, + { + "Id": "AISF-AGENT-01", + "Description": "Ensure every AI agent has its own identity with scoped credentials and independent authorization per request, following zero-trust principles. Agent identities must be separate from human user identities.", + "Name": "Agent Identity and Authentication", + "Attributes": [ + { + "Section": "AI Application Security", + "SubSection": "Agent Governance", + "Service": "bedrock", + "Type": "Manual" + } + ], + "Checks": [] + }, + { + "Id": "AISF-AGENT-02", + "Description": "Ensure Bedrock AgentCore Cedar Policies enforce provable least-privilege authorization on every tool call and data access made by AI agents.", + "Name": "Agent Least-Privilege Authorization", + "Attributes": [ + { + "Section": "AI Application Security", + "SubSection": "Agent Governance", + "Service": "bedrock", + "Type": "Manual" + } + ], + "Checks": [] + }, + { + "Id": "AISF-AGENT-03", + "Description": "Ensure AI agents have behavioral monitoring and observability configured to detect scope violations, anomalous actions, and drift from expected behavior patterns.", + "Name": "Agent Behavioral Monitoring", + "Attributes": [ + { + "Section": "AI Application Security", + "SubSection": "Agent Governance", + "Service": "bedrock", + "Type": "Manual" + } + ], + "Checks": [] + }, + { + "Id": "AISF-AGENT-04", + "Description": "Ensure a central agent registry exists to catalog all AI agents, their permissions, data access patterns, and operational boundaries for governance at scale.", + "Name": "Agent Registry and Catalog", + "Attributes": [ + { + "Section": "AI Application Security", + "SubSection": "Agent Governance", + "Service": "bedrock", + "Type": "Manual" + } + ], + "Checks": [] + }, + { + "Id": "AISF-AGENT-05", + "Description": "Ensure human-in-the-loop approval is required for high-consequence agent actions such as financial transactions, data deletion, or privilege changes.", + "Name": "Human Approval for Critical Agent Actions", + "Attributes": [ + { + "Section": "AI Application Security", + "SubSection": "Agent Governance", + "Service": "bedrock", + "Type": "Manual" + } + ], + "Checks": [] + }, + { + "Id": "AISF-ML-01", + "Description": "Ensure SageMaker models have network isolation enabled to prevent models from making unauthorized network calls during inference.", + "Name": "ML Model Network Isolation", + "Attributes": [ + { + "Section": "AI Application Security", + "SubSection": "ML Platform Security", + "Service": "sagemaker", + "Type": "Automated" + } + ], + "Checks": [ + "sagemaker_models_network_isolation_enabled", + "sagemaker_models_vpc_settings_configured" + ] + }, + { + "Id": "AISF-ML-02", + "Description": "Ensure SageMaker notebook instances are secured with encryption, VPC settings, no direct internet access, and root access disabled.", + "Name": "SageMaker Notebook Security", + "Attributes": [ + { + "Section": "AI Application Security", + "SubSection": "ML Platform Security", + "Service": "sagemaker", + "Type": "Automated" + } + ], + "Checks": [ + "sagemaker_notebook_instance_encryption_enabled", + "sagemaker_notebook_instance_vpc_settings_configured", + "sagemaker_notebook_instance_without_direct_internet_access_configured", + "sagemaker_notebook_instance_root_access_disabled" + ] + }, + { + "Id": "AISF-ML-03", + "Description": "Ensure SageMaker training jobs have network isolation, VPC configuration, inter-container traffic encryption, and volume encryption enabled to protect training data and model weights.", + "Name": "ML Training Job Security", + "Attributes": [ + { + "Section": "AI Application Security", + "SubSection": "ML Platform Security", + "Service": "sagemaker", + "Type": "Automated" + } + ], + "Checks": [ + "sagemaker_training_jobs_network_isolation_enabled", + "sagemaker_training_jobs_vpc_settings_configured", + "sagemaker_training_jobs_intercontainer_encryption_enabled", + "sagemaker_training_jobs_volume_and_output_encryption_enabled" + ] + }, + { + "Id": "AISF-ML-04", + "Description": "Ensure SageMaker Model Registry is in use with approved model packages and SSO authentication is configured for SageMaker domains.", + "Name": "Model Governance and Registry", + "Attributes": [ + { + "Section": "AI Application Security", + "SubSection": "ML Platform Security", + "Service": "sagemaker", + "Type": "Automated" + } + ], + "Checks": [ + "sagemaker_models_registry_in_use", + "sagemaker_domain_sso_configured" + ] + }, + { + "Id": "AISF-ML-05", + "Description": "Ensure SageMaker Model Monitor is configured for continuous model quality and bias monitoring, and SageMaker Clarify is used for bias detection in AI/ML workloads.", + "Name": "Model Monitoring and Bias Detection", + "Attributes": [ + { + "Section": "AI Application Security", + "SubSection": "ML Platform Security", + "Service": "sagemaker", + "Type": "Manual" + } + ], + "Checks": [] + }, + { + "Id": "AISF-DETECT-01", + "Description": "Ensure CloudTrail is enabled in all regions with multi-region logging, management event recording, log file validation, and CloudWatch integration for comprehensive AI workload audit trails.", + "Name": "Comprehensive Audit Logging", + "Attributes": [ + { + "Section": "Threat Detection and Monitoring", + "SubSection": "Audit Logging", + "Service": "cloudtrail", + "Type": "Automated" + } + ], + "Checks": [ + "cloudtrail_multi_region_enabled", + "cloudtrail_multi_region_enabled_logging_management_events", + "cloudtrail_log_file_validation_enabled", + "cloudtrail_cloudwatch_logging_enabled", + "cloudtrail_kms_encryption_enabled", + "cloudtrail_logs_s3_bucket_is_not_publicly_accessible", + "cloudtrail_logs_s3_bucket_access_logging_enabled", + "cloudtrail_insights_exist" + ] + }, + { + "Id": "AISF-DETECT-02", + "Description": "Ensure GuardDuty is enabled across all regions with delegated admin, S3 protection, EKS monitoring, Lambda protection, and malware protection to detect AI-specific threat patterns.", + "Name": "GuardDuty Threat Detection", + "Attributes": [ + { + "Section": "Threat Detection and Monitoring", + "SubSection": "Threat Detection", + "Service": "guardduty", + "Type": "Automated" + } + ], + "Checks": [ + "guardduty_is_enabled", + "guardduty_no_high_severity_findings", + "guardduty_centrally_managed", + "guardduty_delegated_admin_enabled_all_regions", + "guardduty_s3_protection_enabled", + "guardduty_eks_audit_log_enabled", + "guardduty_eks_runtime_monitoring_enabled", + "guardduty_lambda_protection_enabled", + "guardduty_rds_protection_enabled", + "guardduty_ec2_malware_protection_enabled" + ] + }, + { + "Id": "AISF-DETECT-03", + "Description": "Ensure CloudTrail-based threat detection is monitoring for LLM jacking, privilege escalation, and enumeration activity that could indicate attacks against AI infrastructure.", + "Name": "AI-Specific Threat Detection", + "Attributes": [ + { + "Section": "Threat Detection and Monitoring", + "SubSection": "AI Threat Detection", + "Service": "cloudtrail", + "Type": "Automated" + } + ], + "Checks": [ + "cloudtrail_threat_detection_llm_jacking", + "cloudtrail_threat_detection_privilege_escalation", + "cloudtrail_threat_detection_enumeration" + ] + }, + { + "Id": "AISF-DETECT-04", + "Description": "Ensure Security Hub is enabled with standards and integrations configured to aggregate and prioritize security findings across all AI workload services.", + "Name": "Security Hub Centralized Findings", + "Attributes": [ + { + "Section": "Threat Detection and Monitoring", + "SubSection": "Security Aggregation", + "Service": "securityhub", + "Type": "Automated" + } + ], + "Checks": [ + "securityhub_enabled" + ] + }, + { + "Id": "AISF-DETECT-05", + "Description": "Ensure CloudWatch metric filters and alarms are configured for critical security events including IAM policy changes, unauthorized API calls, console sign-in without MFA, KMS key deletion, and network changes.", + "Name": "Security Event Alerting", + "Attributes": [ + { + "Section": "Threat Detection and Monitoring", + "SubSection": "Security Alerting", + "Service": "cloudwatch", + "Type": "Automated" + } + ], + "Checks": [ + "cloudwatch_log_metric_filter_policy_changes", + "cloudwatch_log_metric_filter_unauthorized_api_calls", + "cloudwatch_log_metric_filter_sign_in_without_mfa", + "cloudwatch_log_metric_filter_root_usage", + "cloudwatch_log_metric_filter_disable_or_scheduled_deletion_of_kms_cmk", + "cloudwatch_log_metric_filter_security_group_changes", + "cloudwatch_changes_to_network_acls_alarm_configured", + "cloudwatch_changes_to_vpcs_alarm_configured", + "cloudwatch_log_metric_filter_authentication_failures", + "cloudwatch_log_metric_filter_aws_organizations_changes" + ] + }, + { + "Id": "AISF-DETECT-06", + "Description": "Ensure Amazon Detective is enabled for AI security incident investigation, providing full decision chain reconstruction from prompt to data access to action.", + "Name": "AI Incident Investigation", + "Attributes": [ + { + "Section": "Threat Detection and Monitoring", + "SubSection": "Incident Investigation", + "Service": "detective", + "Type": "Manual" + } + ], + "Checks": [] + }, + { + "Id": "AISF-DETECT-07", + "Description": "Ensure GuardDuty Extended Threat Detection is enabled for AI-specific patterns including anomalous Bedrock API usage, model access from unusual locations, and potential data exfiltration through AI channels.", + "Name": "GuardDuty AI Threat Patterns", + "Attributes": [ + { + "Section": "Threat Detection and Monitoring", + "SubSection": "AI Threat Detection", + "Service": "guardduty", + "Type": "Manual" + } + ], + "Checks": [] + }, + { + "Id": "AISF-GOV-01", + "Description": "Ensure AWS Config recorder is enabled in all regions to continuously monitor and record AI resource configurations, detect drift, and enforce compliance rules.", + "Name": "Configuration Compliance Monitoring", + "Attributes": [ + { + "Section": "Governance and Compliance", + "SubSection": "Configuration Management", + "Service": "config", + "Type": "Automated" + } + ], + "Checks": [ + "config_recorder_all_regions_enabled", + "config_recorder_using_aws_service_role" + ] + }, + { + "Id": "AISF-GOV-02", + "Description": "Ensure the AWS account is part of an AWS Organization with proper governance controls including SCPs to restrict operations to approved regions and delegated administrators are trusted.", + "Name": "Organization Governance", + "Attributes": [ + { + "Section": "Governance and Compliance", + "SubSection": "Account Governance", + "Service": "organizations", + "Type": "Automated" + } + ], + "Checks": [ + "organizations_account_part_of_organizations", + "organizations_scp_check_deny_regions", + "organizations_delegated_administrators", + "organizations_tags_policies_enabled_and_attached" + ] + }, + { + "Id": "AISF-GOV-03", + "Description": "Ensure security contact information is registered and current for AI workload incident response communication.", + "Name": "Security Contact Information", + "Attributes": [ + { + "Section": "Governance and Compliance", + "SubSection": "Incident Response", + "Service": "account", + "Type": "Automated" + } + ], + "Checks": [ + "account_maintain_current_contact_details", + "account_maintain_different_contact_details_to_security_billing_and_operations", + "account_security_contact_information_is_registered" + ] + }, + { + "Id": "AISF-GOV-04", + "Description": "Ensure AWS Control Tower is enabled with landing zone configured to automate account governance and enforce security baselines across all accounts hosting AI workloads.", + "Name": "Control Tower Automated Governance", + "Attributes": [ + { + "Section": "Governance and Compliance", + "SubSection": "Automated Governance", + "Service": "controltower", + "Type": "Manual" + } + ], + "Checks": [] + }, + { + "Id": "AISF-GOV-05", + "Description": "Maintain an inventory of all AI workloads including approved and shadow AI usage. Document model selections, their governance requirements, and security evaluations.", + "Name": "AI Workload Inventory and Audit", + "Attributes": [ + { + "Section": "Governance and Compliance", + "SubSection": "AI Governance", + "Service": "multiple", + "Type": "Manual" + } + ], + "Checks": [] + }, + { + "Id": "AISF-GOV-06", + "Description": "Ensure AI-specific threat models are developed before production deployment, covering prompt injection, jailbreaks, data exfiltration, model poisoning, and adversarial attacks.", + "Name": "AI Threat Modeling", + "Attributes": [ + { + "Section": "Governance and Compliance", + "SubSection": "AI Governance", + "Service": "multiple", + "Type": "Manual" + } + ], + "Checks": [] + }, + { + "Id": "AISF-GOV-07", + "Description": "Ensure incident response plans include AI-specific scenarios covering prompt injection, model manipulation, data exfiltration through AI, LLM jacking, and agent scope violations.", + "Name": "AI Incident Response Planning", + "Attributes": [ + { + "Section": "Governance and Compliance", + "SubSection": "Incident Response", + "Service": "multiple", + "Type": "Manual" + } + ], + "Checks": [] + }, + { + "Id": "AISF-RUNTIME-01", + "Description": "Ensure EKS clusters used for AI agent runtimes are properly secured with private endpoints, network policies, supported versions, control plane logging, and secrets encryption.", + "Name": "EKS Security for AI Runtimes", + "Attributes": [ + { + "Section": "Infrastructure Security", + "SubSection": "Container Runtime Security", + "Service": "eks", + "Type": "Automated" + } + ], + "Checks": [ + "eks_cluster_not_publicly_accessible", + "eks_cluster_private_nodes_enabled", + "eks_cluster_network_policy_enabled", + "eks_cluster_uses_a_supported_version", + "eks_control_plane_logging_all_types_enabled", + "eks_cluster_kms_cmk_encryption_in_secrets_enabled", + "eks_cluster_deletion_protection_enabled" + ] + }, + { + "Id": "AISF-RUNTIME-02", + "Description": "Ensure ECS tasks used for AI workloads have no public IPs, no privileged containers, read-only root filesystems, logging enabled, and no secrets in environment variables.", + "Name": "ECS Security for AI Workloads", + "Attributes": [ + { + "Section": "Infrastructure Security", + "SubSection": "Container Runtime Security", + "Service": "ecs", + "Type": "Automated" + } + ], + "Checks": [ + "ecs_service_no_assign_public_ip", + "ecs_task_set_no_assign_public_ip", + "ecs_task_definitions_no_privileged_containers", + "ecs_task_definitions_containers_readonly_access", + "ecs_task_definitions_logging_enabled", + "ecs_task_definitions_no_environment_secrets", + "ecs_task_definitions_host_namespace_not_shared", + "ecs_cluster_container_insights_enabled" + ] + }, + { + "Id": "AISF-RUNTIME-03", + "Description": "Ensure Lambda functions used in AI architectures are deployed in VPCs, not publicly accessible, use supported runtimes, have no secrets in code or variables, and use CMK-encrypted environment variables.", + "Name": "Lambda Security for AI Functions", + "Attributes": [ + { + "Section": "Infrastructure Security", + "SubSection": "Serverless Runtime Security", + "Service": "awslambda", + "Type": "Automated" + } + ], + "Checks": [ + "awslambda_function_inside_vpc", + "awslambda_function_not_publicly_accessible", + "awslambda_function_url_public", + "awslambda_function_no_secrets_in_code", + "awslambda_function_no_secrets_in_variables", + "awslambda_function_using_supported_runtimes", + "awslambda_function_env_vars_not_encrypted_with_cmk", + "awslambda_function_url_cors_policy", + "awslambda_function_invoke_api_operations_cloudtrail_logging_enabled" + ] + }, + { + "Id": "AISF-MODEL-01", + "Description": "Perform security evaluation of foundation models before deployment, including assessment of input sanitization, access controls, bias audits, privacy disclosure, data poisoning resilience, adversarial resilience, and prompt injection defenses.", + "Name": "Model Security Evaluation", + "Attributes": [ + { + "Section": "AI Application Security", + "SubSection": "Model Governance", + "Service": "bedrock", + "Type": "Manual" + } + ], + "Checks": [] + }, + { + "Id": "AISF-MODEL-02", + "Description": "Ensure model selection is appropriate for the use case with CISO involvement in evaluation. Customer-facing agents require different model security profiles than internal summarization tools.", + "Name": "Model Selection Governance", + "Attributes": [ + { + "Section": "AI Application Security", + "SubSection": "Model Governance", + "Service": "bedrock", + "Type": "Manual" + } + ], + "Checks": [] + } + ] +} \ No newline at end of file diff --git a/prowler/compliance/aws/aws_well_architected_framework_security_pillar_aws.json b/prowler/compliance/aws/aws_well_architected_framework_security_pillar_aws.json index 7a04e4ddf7..94c50eb68a 100644 --- a/prowler/compliance/aws/aws_well_architected_framework_security_pillar_aws.json +++ b/prowler/compliance/aws/aws_well_architected_framework_security_pillar_aws.json @@ -550,6 +550,7 @@ "apigatewayv2_api_access_logging_enabled", "awslambda_function_invoke_api_operations_cloudtrail_logging_enabled", "cloudfront_distributions_logging_enabled", + "cloudtrail_bedrock_logging_enabled", "cloudtrail_cloudwatch_logging_enabled", "cloudtrail_logs_s3_bucket_access_logging_enabled", "directoryservice_directory_log_forwarding_enabled", diff --git a/prowler/compliance/aws/c5_aws.json b/prowler/compliance/aws/c5_aws.json index ddb4c8fce7..8847469154 100644 --- a/prowler/compliance/aws/c5_aws.json +++ b/prowler/compliance/aws/c5_aws.json @@ -3461,6 +3461,7 @@ ], "Checks": [ "kinesis_stream_data_retention_period", + "cloudtrail_bedrock_logging_enabled", "cloudtrail_multi_region_enabled_logging_management_events" ] }, @@ -3669,6 +3670,7 @@ "awslambda_function_invoke_api_operations_cloudtrail_logging_enabled", "bedrock_model_invocation_logging_enabled", "cloudfront_distributions_logging_enabled", + "cloudtrail_bedrock_logging_enabled", "cloudtrail_cloudwatch_logging_enabled", "cloudtrail_logs_s3_bucket_access_logging_enabled", "cloudtrail_multi_region_enabled_logging_management_events", @@ -5288,6 +5290,7 @@ "cognito_user_pool_blocks_compromised_credentials_sign_in_attempts", "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused", "secretsmanager_secret_unused" @@ -6359,6 +6362,7 @@ "iam_rotate_access_key_90_days", "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_administrator_access_policy", "iam_user_console_access_unused", @@ -6473,6 +6477,7 @@ "backup_recovery_point_encrypted", "backup_vaults_encrypted", "bedrock_model_invocation_logs_encryption_enabled", + "bedrock_prompt_encrypted_with_cmk", "cloudfront_distributions_field_level_encryption_enabled", "cloudfront_distributions_origin_traffic_encrypted", "cloudtrail_kms_encryption_enabled", @@ -6730,6 +6735,7 @@ "backup_recovery_point_encrypted", "backup_vaults_encrypted", "bedrock_model_invocation_logs_encryption_enabled", + "bedrock_prompt_encrypted_with_cmk", "cloudfront_distributions_field_level_encryption_enabled", "cloudfront_distributions_origin_traffic_encrypted", "cloudtrail_kms_encryption_enabled", diff --git a/prowler/compliance/aws/ccc_aws.json b/prowler/compliance/aws/ccc_aws.json index ea28875c5c..d1a20ee3d0 100644 --- a/prowler/compliance/aws/ccc_aws.json +++ b/prowler/compliance/aws/ccc_aws.json @@ -1958,6 +1958,7 @@ } ], "Checks": [ + "cloudtrail_bedrock_logging_enabled", "cloudtrail_multi_region_enabled", "cloudtrail_multi_region_enabled_logging_management_events", "cloudtrail_cloudwatch_logging_enabled", diff --git a/prowler/compliance/aws/csa_ccm_4.0_aws.json b/prowler/compliance/aws/csa_ccm_4.0_aws.json index d69ed6fe1d..98d87112c9 100644 --- a/prowler/compliance/aws/csa_ccm_4.0_aws.json +++ b/prowler/compliance/aws/csa_ccm_4.0_aws.json @@ -1311,6 +1311,7 @@ "glue_development_endpoints_job_bookmark_encryption_enabled", "glue_ml_transform_encrypted_at_rest", "bedrock_model_invocation_logs_encryption_enabled", + "bedrock_prompt_encrypted_with_cmk", "codebuild_project_s3_logs_encrypted", "codebuild_report_group_export_encrypted" ] @@ -3100,6 +3101,7 @@ "Checks": [ "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused", "iam_user_two_active_access_key" @@ -3442,6 +3444,7 @@ "Checks": [ "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused", "iam_user_no_setup_initial_access_key" @@ -3551,6 +3554,7 @@ "Checks": [ "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused", "iam_rotate_access_key_90_days", @@ -5853,6 +5857,7 @@ } ], "Checks": [ + "cloudtrail_bedrock_logging_enabled", "cloudtrail_multi_region_enabled", "cloudtrail_multi_region_enabled_logging_management_events", "cloudtrail_s3_dataevents_read_enabled", diff --git a/prowler/compliance/aws/ens_rd2022_aws.json b/prowler/compliance/aws/ens_rd2022_aws.json index 556f0f8868..f6c574daef 100644 --- a/prowler/compliance/aws/ens_rd2022_aws.json +++ b/prowler/compliance/aws/ens_rd2022_aws.json @@ -544,6 +544,7 @@ "Checks": [ "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused" ] @@ -2538,8 +2539,7 @@ } ], "Checks": [ - "vpc_subnet_separate_private_public", - "vpc_different_regions" + "vpc_subnet_separate_private_public" ] }, { @@ -2592,8 +2592,8 @@ } ], "Checks": [ - "vpc_subnet_different_az", - "vpc_different_regions" + "vpc_different_regions", + "vpc_subnet_different_az" ] }, { @@ -4261,6 +4261,29 @@ ], "Checks": [] }, + { + "Id": "op.cont.2.aws.vpc.1", + "Description": "Plan de continuidad", + "Attributes": [ + { + "IdGrupoControl": "op.cont.2", + "Marco": "operacional", + "Categoria": "continuidad del servicio", + "DescripcionControl": "Distribución de las VPCs entre múltiples regiones y zonas de disponibilidad de AWS para garantizar la continuidad del servicio ante fallos regionales o zonales.", + "Nivel": "alto", + "Tipo": "requisito", + "Dimensiones": [ + "disponibilidad" + ], + "ModoEjecucion": "automático", + "Dependencias": [] + } + ], + "Checks": [ + "vpc_different_regions", + "vpc_subnet_different_az" + ] + }, { "Id": "op.cont.3.aws.drs.1", "Description": "Pruebas periódicas", diff --git a/prowler/compliance/aws/fedramp_20x_ksi_low_aws.json b/prowler/compliance/aws/fedramp_20x_ksi_low_aws.json index c97c407e61..6c6c500582 100644 --- a/prowler/compliance/aws/fedramp_20x_ksi_low_aws.json +++ b/prowler/compliance/aws/fedramp_20x_ksi_low_aws.json @@ -109,6 +109,7 @@ "iam_rotate_access_key_90_days", "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused", "iam_user_hardware_mfa_enabled", @@ -325,6 +326,7 @@ "iam_rotate_access_key_90_days", "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused", "organizations_delegated_administrators" diff --git a/prowler/compliance/aws/fedramp_low_revision_4_aws.json b/prowler/compliance/aws/fedramp_low_revision_4_aws.json index cf2c0592e1..9824798552 100644 --- a/prowler/compliance/aws/fedramp_low_revision_4_aws.json +++ b/prowler/compliance/aws/fedramp_low_revision_4_aws.json @@ -39,6 +39,7 @@ "iam_user_hardware_mfa_enabled", "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused", "rds_instance_integration_cloudwatch_logs", diff --git a/prowler/compliance/aws/fedramp_moderate_revision_4_aws.json b/prowler/compliance/aws/fedramp_moderate_revision_4_aws.json index 76b6f9aa6c..c914a58b2c 100644 --- a/prowler/compliance/aws/fedramp_moderate_revision_4_aws.json +++ b/prowler/compliance/aws/fedramp_moderate_revision_4_aws.json @@ -32,6 +32,7 @@ "iam_user_mfa_enabled_console_access", "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused", "securityhub_enabled" @@ -109,6 +110,7 @@ "iam_user_mfa_enabled_console_access", "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused" ] @@ -165,6 +167,7 @@ "iam_user_mfa_enabled_console_access", "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused" ] @@ -185,6 +188,7 @@ "iam_password_policy_minimum_length_14", "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused" ] @@ -320,6 +324,7 @@ "iam_no_root_access_key", "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused", "awslambda_function_not_publicly_accessible", @@ -434,6 +439,7 @@ "cloudtrail_s3_dataevents_read_enabled", "cloudtrail_s3_dataevents_write_enabled", "cloudtrail_multi_region_enabled", + "cloudtrail_bedrock_logging_enabled", "cloudtrail_cloudwatch_logging_enabled", "elbv2_logging_enabled", "elb_logging_enabled", @@ -589,6 +595,7 @@ "cloudtrail_s3_dataevents_read_enabled", "cloudtrail_s3_dataevents_write_enabled", "cloudtrail_multi_region_enabled", + "cloudtrail_bedrock_logging_enabled", "cloudtrail_cloudwatch_logging_enabled", "elbv2_logging_enabled", "elb_logging_enabled", diff --git a/prowler/compliance/aws/ffiec_aws.json b/prowler/compliance/aws/ffiec_aws.json index 697d9ee49f..23ab8953b6 100644 --- a/prowler/compliance/aws/ffiec_aws.json +++ b/prowler/compliance/aws/ffiec_aws.json @@ -119,6 +119,7 @@ ], "Checks": [ "apigateway_restapi_logging_enabled", + "cloudtrail_bedrock_logging_enabled", "cloudtrail_multi_region_enabled", "cloudtrail_s3_dataevents_read_enabled", "cloudtrail_s3_dataevents_write_enabled", diff --git a/prowler/compliance/aws/hipaa_aws.json b/prowler/compliance/aws/hipaa_aws.json index 34de052b95..036489d712 100644 --- a/prowler/compliance/aws/hipaa_aws.json +++ b/prowler/compliance/aws/hipaa_aws.json @@ -87,6 +87,7 @@ ], "Checks": [ "apigateway_restapi_logging_enabled", + "cloudtrail_bedrock_logging_enabled", "cloudtrail_multi_region_enabled", "cloudtrail_s3_dataevents_read_enabled", "cloudtrail_s3_dataevents_write_enabled", @@ -632,6 +633,7 @@ ], "Checks": [ "apigateway_restapi_logging_enabled", + "cloudtrail_bedrock_logging_enabled", "cloudtrail_multi_region_enabled", "cloudtrail_s3_dataevents_read_enabled", "cloudtrail_s3_dataevents_write_enabled", diff --git a/prowler/compliance/aws/iso27001_2013_aws.json b/prowler/compliance/aws/iso27001_2013_aws.json index cf7e5bb0a6..ad31ea0af4 100644 --- a/prowler/compliance/aws/iso27001_2013_aws.json +++ b/prowler/compliance/aws/iso27001_2013_aws.json @@ -869,6 +869,7 @@ "Checks": [ "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused" ] diff --git a/prowler/compliance/aws/iso27001_2022_aws.json b/prowler/compliance/aws/iso27001_2022_aws.json index cb64f4bdd8..d47bfcf1d1 100644 --- a/prowler/compliance/aws/iso27001_2022_aws.json +++ b/prowler/compliance/aws/iso27001_2022_aws.json @@ -247,6 +247,7 @@ "iam_root_mfa_enabled", "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_rotate_access_key_90_days", "iam_user_accesskey_unused", "iam_user_console_access_unused", @@ -1293,6 +1294,7 @@ "bedrock_model_invocation_logging_enabled", "bedrock_model_invocation_logs_encryption_enabled", "cloudfront_distributions_logging_enabled", + "cloudtrail_bedrock_logging_enabled", "cloudtrail_cloudwatch_logging_enabled", "cloudtrail_kms_encryption_enabled", "cloudtrail_log_file_validation_enabled", @@ -1767,6 +1769,7 @@ "backup_recovery_point_encrypted", "backup_vaults_encrypted", "bedrock_model_invocation_logs_encryption_enabled", + "bedrock_prompt_encrypted_with_cmk", "cloudfront_distributions_field_level_encryption_enabled", "cloudfront_distributions_origin_traffic_encrypted", "cloudtrail_kms_encryption_enabled", diff --git a/prowler/compliance/aws/kisa_isms_p_2023_aws.json b/prowler/compliance/aws/kisa_isms_p_2023_aws.json index b2b71fa905..d172e615dd 100644 --- a/prowler/compliance/aws/kisa_isms_p_2023_aws.json +++ b/prowler/compliance/aws/kisa_isms_p_2023_aws.json @@ -2115,6 +2115,7 @@ "Checks": [ "backup_vaults_encrypted", "bedrock_model_invocation_logs_encryption_enabled", + "bedrock_prompt_encrypted_with_cmk", "cloudtrail_kms_encryption_enabled", "cloudwatch_log_group_kms_encryption_enabled", "dynamodb_tables_kms_cmk_encryption_enabled", @@ -2539,6 +2540,7 @@ "bedrock_model_invocation_logging_enabled", "bedrock_model_invocation_logs_encryption_enabled", "cloudfront_distributions_logging_enabled", + "cloudtrail_bedrock_logging_enabled", "cloudtrail_bucket_requires_mfa_delete", "cloudtrail_cloudwatch_logging_enabled", "cloudtrail_insights_exist", diff --git a/prowler/compliance/aws/kisa_isms_p_2023_korean_aws.json b/prowler/compliance/aws/kisa_isms_p_2023_korean_aws.json index a933fc8d27..1748d96442 100644 --- a/prowler/compliance/aws/kisa_isms_p_2023_korean_aws.json +++ b/prowler/compliance/aws/kisa_isms_p_2023_korean_aws.json @@ -2117,6 +2117,7 @@ "Checks": [ "backup_vaults_encrypted", "bedrock_model_invocation_logs_encryption_enabled", + "bedrock_prompt_encrypted_with_cmk", "cloudtrail_kms_encryption_enabled", "cloudwatch_log_group_kms_encryption_enabled", "dynamodb_tables_kms_cmk_encryption_enabled", diff --git a/prowler/compliance/aws/mitre_attack_aws.json b/prowler/compliance/aws/mitre_attack_aws.json index 93545b3870..3d1d5fd378 100644 --- a/prowler/compliance/aws/mitre_attack_aws.json +++ b/prowler/compliance/aws/mitre_attack_aws.json @@ -171,6 +171,7 @@ "iam_no_expired_server_certificates_stored", "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused", "iam_no_root_access_key", diff --git a/prowler/compliance/aws/nis2_aws.json b/prowler/compliance/aws/nis2_aws.json index 687068e26d..d7f193c6c0 100644 --- a/prowler/compliance/aws/nis2_aws.json +++ b/prowler/compliance/aws/nis2_aws.json @@ -1913,6 +1913,7 @@ "Checks": [ "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused" ], diff --git a/prowler/compliance/aws/nist_800_171_revision_2_aws.json b/prowler/compliance/aws/nist_800_171_revision_2_aws.json index bd0c84f327..8e28ee383d 100644 --- a/prowler/compliance/aws/nist_800_171_revision_2_aws.json +++ b/prowler/compliance/aws/nist_800_171_revision_2_aws.json @@ -32,6 +32,7 @@ "iam_user_mfa_enabled_console_access", "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused", "awslambda_function_not_publicly_accessible", @@ -76,6 +77,7 @@ "iam_user_mfa_enabled_console_access", "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused", "awslambda_function_not_publicly_accessible", @@ -164,6 +166,7 @@ "iam_no_root_access_key", "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused" ] @@ -589,6 +592,7 @@ "iam_password_policy_expires_passwords_within_90_days_or_less", "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused" ] diff --git a/prowler/compliance/aws/nist_800_53_revision_4_aws.json b/prowler/compliance/aws/nist_800_53_revision_4_aws.json index 24bf67fa31..deb2a3cc25 100644 --- a/prowler/compliance/aws/nist_800_53_revision_4_aws.json +++ b/prowler/compliance/aws/nist_800_53_revision_4_aws.json @@ -23,6 +23,7 @@ "iam_rotate_access_key_90_days", "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused", "securityhub_enabled" @@ -43,6 +44,7 @@ "Checks": [ "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused" ] @@ -116,6 +118,7 @@ "iam_rotate_access_key_90_days", "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused", "rds_instance_integration_cloudwatch_logs", @@ -240,6 +243,7 @@ "iam_no_root_access_key", "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused", "awslambda_function_url_public", diff --git a/prowler/compliance/aws/nist_800_53_revision_5_aws.json b/prowler/compliance/aws/nist_800_53_revision_5_aws.json index 12a55a9359..858df01fd9 100644 --- a/prowler/compliance/aws/nist_800_53_revision_5_aws.json +++ b/prowler/compliance/aws/nist_800_53_revision_5_aws.json @@ -31,6 +31,7 @@ "iam_user_mfa_enabled_console_access", "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused", "secretsmanager_automatic_rotation_enabled" @@ -53,6 +54,7 @@ "iam_password_policy_minimum_length_14", "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused" ] @@ -74,6 +76,7 @@ "iam_password_policy_minimum_length_14", "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused" ] @@ -95,6 +98,7 @@ "iam_password_policy_minimum_length_14", "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused" ] @@ -116,6 +120,7 @@ "iam_password_policy_minimum_length_14", "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused" ] @@ -136,6 +141,7 @@ "iam_password_policy_minimum_length_14", "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused" ] @@ -247,6 +253,7 @@ "Checks": [ "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused" ] @@ -285,6 +292,7 @@ "Checks": [ "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused" ] @@ -861,6 +869,7 @@ "iam_user_mfa_enabled_console_access", "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused", "secretsmanager_automatic_rotation_enabled" @@ -1199,6 +1208,7 @@ "iam_no_root_access_key", "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused", "awslambda_function_not_publicly_accessible", @@ -1594,6 +1604,7 @@ "cloudtrail_s3_dataevents_read_enabled", "cloudtrail_s3_dataevents_write_enabled", "cloudtrail_multi_region_enabled", + "cloudtrail_bedrock_logging_enabled", "cloudtrail_cloudwatch_logging_enabled", "elbv2_logging_enabled", "elb_logging_enabled", @@ -2152,6 +2163,7 @@ "cloudtrail_s3_dataevents_read_enabled", "cloudtrail_s3_dataevents_write_enabled", "cloudtrail_multi_region_enabled", + "cloudtrail_bedrock_logging_enabled", "cloudtrail_cloudwatch_logging_enabled", "elbv2_logging_enabled", "elb_logging_enabled", @@ -2179,6 +2191,7 @@ "cloudtrail_s3_dataevents_read_enabled", "cloudtrail_s3_dataevents_write_enabled", "cloudtrail_multi_region_enabled", + "cloudtrail_bedrock_logging_enabled", "cloudtrail_cloudwatch_logging_enabled", "elbv2_logging_enabled", "elb_logging_enabled", diff --git a/prowler/compliance/aws/nist_csf_1.1_aws.json b/prowler/compliance/aws/nist_csf_1.1_aws.json index cb21e19757..a55097e70c 100644 --- a/prowler/compliance/aws/nist_csf_1.1_aws.json +++ b/prowler/compliance/aws/nist_csf_1.1_aws.json @@ -577,6 +577,7 @@ "iam_rotate_access_key_90_days", "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused", "secretsmanager_automatic_rotation_enabled" @@ -638,6 +639,7 @@ "iam_no_root_access_key", "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused" ] diff --git a/prowler/compliance/aws/nist_csf_2.0_aws.json b/prowler/compliance/aws/nist_csf_2.0_aws.json index fb9b1da4f1..e890b08573 100644 --- a/prowler/compliance/aws/nist_csf_2.0_aws.json +++ b/prowler/compliance/aws/nist_csf_2.0_aws.json @@ -707,6 +707,7 @@ "iam_user_console_access_unused", "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_two_active_access_key", "iam_root_credentials_management_enabled", @@ -903,6 +904,7 @@ "Checks": [ "backup_vaults_encrypted", "backup_recovery_point_encrypted", + "bedrock_prompt_encrypted_with_cmk", "cloudtrail_kms_encryption_enabled", "cloudwatch_log_group_kms_encryption_enabled", "s3_bucket_kms_encryption", @@ -1310,6 +1312,7 @@ } ], "Checks": [ + "cloudtrail_bedrock_logging_enabled", "cloudtrail_kms_encryption_enabled", "cloudtrail_log_file_validation_enabled", "cloudtrail_logs_s3_bucket_access_logging_enabled", @@ -1473,6 +1476,7 @@ "cloudtrail_threat_detection_enumeration", "cloudtrail_threat_detection_privilege_escalation", "cloudtrail_threat_detection_llm_jacking", + "cloudtrail_bedrock_logging_enabled", "cloudtrail_cloudwatch_logging_enabled", "cloudtrail_multi_region_enabled_logging_management_events" ] @@ -1569,6 +1573,7 @@ "cloudtrail_threat_detection_llm_jacking", "cloudtrail_threat_detection_enumeration", "cloudtrail_multi_region_enabled_logging_management_events", + "cloudtrail_bedrock_logging_enabled", "cloudtrail_cloudwatch_logging_enabled", "cloudwatch_log_metric_filter_unauthorized_api_calls", "cloudwatch_log_metric_filter_authentication_failures", diff --git a/prowler/compliance/aws/pci_3.2.1_aws.json b/prowler/compliance/aws/pci_3.2.1_aws.json index a313c70ae8..c240548f6a 100644 --- a/prowler/compliance/aws/pci_3.2.1_aws.json +++ b/prowler/compliance/aws/pci_3.2.1_aws.json @@ -1563,6 +1563,7 @@ "Checks": [ "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_password_policy_reuse_24", "iam_user_accesskey_unused", "iam_user_console_access_unused" diff --git a/prowler/compliance/aws/secnumcloud_3.2_aws.json b/prowler/compliance/aws/secnumcloud_3.2_aws.json index 4f01db2f3f..1e157b47c7 100644 --- a/prowler/compliance/aws/secnumcloud_3.2_aws.json +++ b/prowler/compliance/aws/secnumcloud_3.2_aws.json @@ -295,6 +295,7 @@ "Checks": [ "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused", "iam_no_expired_server_certificates_stored" @@ -340,6 +341,7 @@ "iam_rotate_access_key_90_days", "iam_role_access_not_stale_to_bedrock", "iam_user_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused", "accessanalyzer_enabled_without_findings" @@ -816,6 +818,7 @@ } ], "Checks": [ + "cloudtrail_bedrock_logging_enabled", "cloudtrail_multi_region_enabled", "cloudtrail_multi_region_enabled_logging_management_events", "cloudtrail_s3_dataevents_read_enabled", diff --git a/prowler/compliance/aws/soc2_aws.json b/prowler/compliance/aws/soc2_aws.json index 472d20ab75..5a027d0416 100644 --- a/prowler/compliance/aws/soc2_aws.json +++ b/prowler/compliance/aws/soc2_aws.json @@ -346,6 +346,7 @@ } ], "Checks": [ + "cloudtrail_bedrock_logging_enabled", "cloudtrail_cloudwatch_logging_enabled", "cloudwatch_changes_to_network_acls_alarm_configured", "cloudwatch_changes_to_network_gateways_alarm_configured", diff --git a/prowler/compliance/azure/cis_2.1_azure.json b/prowler/compliance/azure/cis_2.1_azure.json index 805c81bbd8..a663e5be18 100644 --- a/prowler/compliance/azure/cis_2.1_azure.json +++ b/prowler/compliance/azure/cis_2.1_azure.json @@ -1383,7 +1383,7 @@ "Id": "3.7", "Description": "Ensure that 'Public Network Access' is `Disabled' for storage accounts", "Checks": [ - "storage_blob_public_access_level_is_disabled" + "storage_account_public_network_access_disabled" ], "Attributes": [ { diff --git a/prowler/compliance/azure/cis_3.0_azure.json b/prowler/compliance/azure/cis_3.0_azure.json index 47344c19ee..107c495d05 100644 --- a/prowler/compliance/azure/cis_3.0_azure.json +++ b/prowler/compliance/azure/cis_3.0_azure.json @@ -1651,7 +1651,7 @@ "Id": "4.6", "Description": "Ensure that 'Public Network Access' is 'Disabled' for storage accounts", "Checks": [ - "storage_blob_public_access_level_is_disabled" + "storage_account_public_network_access_disabled" ], "Attributes": [ { diff --git a/prowler/compliance/azure/cis_4.0_azure.json b/prowler/compliance/azure/cis_4.0_azure.json index ba15a661ef..c075ff4047 100644 --- a/prowler/compliance/azure/cis_4.0_azure.json +++ b/prowler/compliance/azure/cis_4.0_azure.json @@ -3021,7 +3021,7 @@ "Id": "10.3.2.2", "Description": "Ensure that 'Public Network Access' is 'Disabled' for storage accounts", "Checks": [ - "storage_blob_public_access_level_is_disabled" + "storage_account_public_network_access_disabled" ], "Attributes": [ { diff --git a/prowler/compliance/azure/cis_5.0_azure.json b/prowler/compliance/azure/cis_5.0_azure.json index 49b18f9326..21785d92b6 100644 --- a/prowler/compliance/azure/cis_5.0_azure.json +++ b/prowler/compliance/azure/cis_5.0_azure.json @@ -3182,7 +3182,7 @@ "Id": "9.3.2.2", "Description": "Ensure that 'Public Network Access' is 'Disabled' for storage accounts", "Checks": [ - "storage_blob_public_access_level_is_disabled" + "storage_account_public_network_access_disabled" ], "Attributes": [ { diff --git a/prowler/compliance/azure/prowler_threatscore_azure.json b/prowler/compliance/azure/prowler_threatscore_azure.json index a65a7f60e8..a030bb845b 100644 --- a/prowler/compliance/azure/prowler_threatscore_azure.json +++ b/prowler/compliance/azure/prowler_threatscore_azure.json @@ -459,7 +459,7 @@ "Id": "2.2.6", "Description": "Ensure that 'Public Network Access' is 'Disabled' for storage accounts", "Checks": [ - "storage_blob_public_access_level_is_disabled" + "storage_account_public_network_access_disabled" ], "Attributes": [ { diff --git a/prowler/compliance/gcp/cis_4.0_gcp.json b/prowler/compliance/gcp/cis_4.0_gcp.json index 7664744e05..5cd97a87da 100644 --- a/prowler/compliance/gcp/cis_4.0_gcp.json +++ b/prowler/compliance/gcp/cis_4.0_gcp.json @@ -914,7 +914,7 @@ ] }, { - "Id": "3.1", + "Id": "3.10", "Description": "Use Identity Aware Proxy (IAP) to Ensure Only Traffic From Google IP Addresses are 'Allowed'", "Checks": [], "Attributes": [ @@ -1132,7 +1132,7 @@ ] }, { - "Id": "4.1", + "Id": "4.10", "Description": "Ensure That App Engine Applications Enforce HTTPS Connections", "Checks": [], "Attributes": [ diff --git a/prowler/compliance/googleworkspace/cis_1.3_googleworkspace.json b/prowler/compliance/googleworkspace/cis_1.3_googleworkspace.json index c236a1f643..0867da103e 100644 --- a/prowler/compliance/googleworkspace/cis_1.3_googleworkspace.json +++ b/prowler/compliance/googleworkspace/cis_1.3_googleworkspace.json @@ -653,7 +653,9 @@ { "Id": "3.1.3.4.1.1", "Description": "Ensure protection against encrypted attachments from untrusted senders is enabled", - "Checks": [], + "Checks": [ + "gmail_encrypted_attachment_protection_enabled" + ], "Attributes": [ { "Section": "3 Apps", @@ -674,7 +676,9 @@ { "Id": "3.1.3.4.1.2", "Description": "Ensure protection against attachments with scripts from untrusted senders is enabled", - "Checks": [], + "Checks": [ + "gmail_script_attachment_protection_enabled" + ], "Attributes": [ { "Section": "3 Apps", @@ -695,7 +699,9 @@ { "Id": "3.1.3.4.1.3", "Description": "Ensure protection against anomalous attachment types in emails is enabled", - "Checks": [], + "Checks": [ + "gmail_anomalous_attachment_protection_enabled" + ], "Attributes": [ { "Section": "3 Apps", @@ -785,7 +791,9 @@ { "Id": "3.1.3.4.3.1", "Description": "Ensure protection against domain spoofing based on similar domain names is enabled", - "Checks": [], + "Checks": [ + "gmail_domain_spoofing_protection_enabled" + ], "Attributes": [ { "Section": "3 Apps", @@ -806,7 +814,9 @@ { "Id": "3.1.3.4.3.2", "Description": "Ensure protection against spoofing of employee names is enabled", - "Checks": [], + "Checks": [ + "gmail_employee_name_spoofing_protection_enabled" + ], "Attributes": [ { "Section": "3 Apps", @@ -827,7 +837,9 @@ { "Id": "3.1.3.4.3.3", "Description": "Ensure protection against inbound emails spoofing your domain is enabled", - "Checks": [], + "Checks": [ + "gmail_inbound_domain_spoofing_protection_enabled" + ], "Attributes": [ { "Section": "3 Apps", @@ -848,7 +860,9 @@ { "Id": "3.1.3.4.3.4", "Description": "Ensure protection against any unauthenticated emails is enabled", - "Checks": [], + "Checks": [ + "gmail_unauthenticated_email_protection_enabled" + ], "Attributes": [ { "Section": "3 Apps", @@ -869,7 +883,9 @@ { "Id": "3.1.3.4.3.5", "Description": "Ensure groups are protected from inbound emails spoofing your domain", - "Checks": [], + "Checks": [ + "gmail_groups_spoofing_protection_enabled" + ], "Attributes": [ { "Section": "3 Apps", @@ -1068,7 +1084,9 @@ { "Id": "3.1.4.1.1", "Description": "Ensure external filesharing in Google Chat and Hangouts is disabled", - "Checks": [], + "Checks": [ + "chat_external_file_sharing_disabled" + ], "Attributes": [ { "Section": "3 Apps", @@ -1089,7 +1107,9 @@ { "Id": "3.1.4.1.2", "Description": "Ensure internal filesharing in Google Chat and Hangouts is disabled", - "Checks": [], + "Checks": [ + "chat_internal_file_sharing_disabled" + ], "Attributes": [ { "Section": "3 Apps", @@ -1110,7 +1130,9 @@ { "Id": "3.1.4.2.1", "Description": "Ensure Google Chat externally is restricted to allowed domains", - "Checks": [], + "Checks": [ + "chat_external_messaging_restricted" + ], "Attributes": [ { "Section": "3 Apps", @@ -1131,7 +1153,9 @@ { "Id": "3.1.4.3.1", "Description": "Ensure external spaces in Google Chat and Hangouts are restricted", - "Checks": [], + "Checks": [ + "chat_external_spaces_restricted" + ], "Attributes": [ { "Section": "3 Apps", @@ -1152,7 +1176,9 @@ { "Id": "3.1.4.4.1", "Description": "Ensure allow users to install Chat apps is disabled", - "Checks": [], + "Checks": [ + "chat_apps_installation_disabled" + ], "Attributes": [ { "Section": "3 Apps", @@ -1173,7 +1199,9 @@ { "Id": "3.1.4.4.2", "Description": "Ensure allow users to add and use incoming webhooks is disabled", - "Checks": [], + "Checks": [ + "chat_incoming_webhooks_disabled" + ], "Attributes": [ { "Section": "3 Apps", @@ -1194,7 +1222,9 @@ { "Id": "3.1.6.1", "Description": "Ensure accessing groups from outside this organization is set to private", - "Checks": [], + "Checks": [ + "groups_external_access_restricted" + ], "Attributes": [ { "Section": "3 Apps", @@ -1215,7 +1245,9 @@ { "Id": "3.1.6.2", "Description": "Ensure creating groups is restricted", - "Checks": [], + "Checks": [ + "groups_creation_restricted" + ], "Attributes": [ { "Section": "3 Apps", @@ -1236,7 +1268,9 @@ { "Id": "3.1.6.3", "Description": "Ensure default for permission to view conversations is restricted", - "Checks": [], + "Checks": [ + "groups_view_conversations_restricted" + ], "Attributes": [ { "Section": "3 Apps", @@ -1257,7 +1291,9 @@ { "Id": "3.1.7.1", "Description": "Ensure service status for Google Sites is set to off", - "Checks": [], + "Checks": [ + "sites_service_disabled" + ], "Attributes": [ { "Section": "3 Apps", @@ -1278,7 +1314,9 @@ { "Id": "3.1.8.1", "Description": "Ensure access to external Google Groups is OFF for Everyone", - "Checks": [], + "Checks": [ + "additionalservices_external_groups_disabled" + ], "Attributes": [ { "Section": "3 Apps", @@ -1299,7 +1337,9 @@ { "Id": "3.1.9.1.1", "Description": "Ensure users access to Google Workspace Marketplace apps is restricted", - "Checks": [], + "Checks": [ + "marketplace_apps_access_restricted" + ], "Attributes": [ { "Section": "3 Apps", diff --git a/prowler/compliance/googleworkspace/cisa_scuba_0.6_googleworkspace.json b/prowler/compliance/googleworkspace/cisa_scuba_0.6_googleworkspace.json index 607d701818..0ce4d2ef6a 100644 --- a/prowler/compliance/googleworkspace/cisa_scuba_0.6_googleworkspace.json +++ b/prowler/compliance/googleworkspace/cisa_scuba_0.6_googleworkspace.json @@ -374,7 +374,9 @@ { "Id": "GWS.COMMONCONTROLS.11.1", "Description": "Only approved Marketplace apps SHALL be allowed for installation", - "Checks": [], + "Checks": [ + "marketplace_apps_access_restricted" + ], "Attributes": [ { "Section": "Common Controls", @@ -649,7 +651,9 @@ { "Id": "GWS.GMAIL.5.1", "Description": "Protect against encrypted attachments from untrusted senders SHALL be enabled", - "Checks": [], + "Checks": [ + "gmail_encrypted_attachment_protection_enabled" + ], "Attributes": [ { "Section": "Gmail", @@ -662,7 +666,9 @@ { "Id": "GWS.GMAIL.5.2", "Description": "Protect against attachments with scripts from untrusted senders SHALL be enabled", - "Checks": [], + "Checks": [ + "gmail_script_attachment_protection_enabled" + ], "Attributes": [ { "Section": "Gmail", @@ -675,7 +681,9 @@ { "Id": "GWS.GMAIL.5.3", "Description": "Protect against anomalous attachment types in emails SHALL be enabled", - "Checks": [], + "Checks": [ + "gmail_anomalous_attachment_protection_enabled" + ], "Attributes": [ { "Section": "Gmail", @@ -798,7 +806,9 @@ { "Id": "GWS.GMAIL.7.1", "Description": "Protect against domain spoofing based on similar domain names SHALL be enabled", - "Checks": [], + "Checks": [ + "gmail_domain_spoofing_protection_enabled" + ], "Attributes": [ { "Section": "Gmail", @@ -811,7 +821,9 @@ { "Id": "GWS.GMAIL.7.2", "Description": "Protect against spoofing of employee names SHALL be enabled", - "Checks": [], + "Checks": [ + "gmail_employee_name_spoofing_protection_enabled" + ], "Attributes": [ { "Section": "Gmail", @@ -824,7 +836,9 @@ { "Id": "GWS.GMAIL.7.3", "Description": "Protect against inbound emails spoofing your domain SHALL be enabled", - "Checks": [], + "Checks": [ + "gmail_inbound_domain_spoofing_protection_enabled" + ], "Attributes": [ { "Section": "Gmail", @@ -837,7 +851,9 @@ { "Id": "GWS.GMAIL.7.4", "Description": "Protect against any unauthenticated emails SHALL be enabled", - "Checks": [], + "Checks": [ + "gmail_unauthenticated_email_protection_enabled" + ], "Attributes": [ { "Section": "Gmail", @@ -850,7 +866,9 @@ { "Id": "GWS.GMAIL.7.5", "Description": "Protect your Groups from inbound emails spoofing your domain SHALL be enabled", - "Checks": [], + "Checks": [ + "gmail_groups_spoofing_protection_enabled" + ], "Attributes": [ { "Section": "Gmail", @@ -1450,7 +1468,9 @@ { "Id": "GWS.CHAT.2.1", "Description": "External file sharing SHALL be disabled to protect sensitive information from unauthorized or accidental sharing", - "Checks": [], + "Checks": [ + "chat_external_file_sharing_disabled" + ], "Attributes": [ { "Section": "Chat", @@ -1476,7 +1496,9 @@ { "Id": "GWS.CHAT.4.1", "Description": "External chat messaging SHALL be restricted to allowlisted domains only", - "Checks": [], + "Checks": [ + "chat_external_messaging_restricted" + ], "Attributes": [ { "Section": "Chat", @@ -1606,7 +1628,9 @@ { "Id": "GWS.GROUPS.1.1", "Description": "Group access from outside the organization SHALL be disabled unless explicitly granted by the group owner", - "Checks": [], + "Checks": [ + "groups_external_access_restricted" + ], "Attributes": [ { "Section": "Groups", @@ -1619,7 +1643,9 @@ { "Id": "GWS.GROUPS.1.2", "Description": "Group owners' ability to add external members to groups SHOULD be disabled unless necessary for agency mission fulfillment", - "Checks": [], + "Checks": [ + "groups_creation_restricted" + ], "Attributes": [ { "Section": "Groups", @@ -1632,7 +1658,9 @@ { "Id": "GWS.GROUPS.1.3", "Description": "Group owners' ability to allow posting to a group by an external, non-group member SHOULD be disabled unless necessary for agency mission fulfillment", - "Checks": [], + "Checks": [ + "groups_creation_restricted" + ], "Attributes": [ { "Section": "Groups", @@ -1645,7 +1673,9 @@ { "Id": "GWS.GROUPS.2.1", "Description": "Group creation SHOULD be restricted to admins within the organization unless necessary for agency mission fulfillment", - "Checks": [], + "Checks": [ + "groups_creation_restricted" + ], "Attributes": [ { "Section": "Groups", @@ -1658,7 +1688,9 @@ { "Id": "GWS.GROUPS.3.1", "Description": "The default permission to view conversations SHOULD be set to All Group Members", - "Checks": [], + "Checks": [ + "groups_view_conversations_restricted" + ], "Attributes": [ { "Section": "Groups", @@ -1684,7 +1716,9 @@ { "Id": "GWS.SITES.1.1", "Description": "Sites Service SHOULD be disabled for all users", - "Checks": [], + "Checks": [ + "sites_service_disabled" + ], "Attributes": [ { "Section": "Sites", diff --git a/prowler/compliance/m365/iso27001_2022_m365.json b/prowler/compliance/m365/iso27001_2022_m365.json index 1a3419a12e..238002d0b6 100644 --- a/prowler/compliance/m365/iso27001_2022_m365.json +++ b/prowler/compliance/m365/iso27001_2022_m365.json @@ -251,6 +251,7 @@ "entra_break_glass_account_fido2_security_key_registered", "entra_conditional_access_policy_mfa_enforced_for_guest_users", "entra_default_app_management_policy_enabled", + "entra_emergency_access_exclusion", "entra_all_apps_conditional_access_coverage", "entra_conditional_access_policy_device_registration_mfa_required", "entra_intune_enrollment_sign_in_frequency_every_time", @@ -260,6 +261,7 @@ "entra_legacy_authentication_blocked", "entra_managed_device_required_for_authentication", "entra_seamless_sso_disabled", + "entra_service_principal_no_secrets_for_permanent_tier0_roles", "entra_users_mfa_enabled", "exchange_organization_modern_authentication_enabled", "exchange_transport_config_smtp_auth_disabled", @@ -282,6 +284,7 @@ "entra_admin_portals_access_restriction", "entra_app_registration_no_unused_privileged_permissions", "entra_policy_guest_users_access_restrictions", + "entra_service_principal_no_secrets_for_permanent_tier0_roles", "sharepoint_external_sharing_managed", "sharepoint_external_sharing_restricted", "sharepoint_guest_sharing_restricted" @@ -671,10 +674,12 @@ "entra_admin_users_phishing_resistant_mfa_enabled", "entra_admin_users_sign_in_frequency_enabled", "entra_break_glass_account_fido2_security_key_registered", + "entra_emergency_access_exclusion", "entra_app_registration_no_unused_privileged_permissions", "entra_policy_ensure_default_user_cannot_create_tenants", "entra_policy_guest_invite_only_for_admin_roles", - "entra_seamless_sso_disabled" + "entra_seamless_sso_disabled", + "entra_service_principal_no_secrets_for_permanent_tier0_roles" ] }, { @@ -727,9 +732,11 @@ "entra_conditional_access_policy_device_code_flow_blocked", "entra_conditional_access_policy_directory_sync_account_excluded", "entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced", + "entra_emergency_access_exclusion", "entra_identity_protection_sign_in_risk_enabled", "entra_managed_device_required_for_authentication", "entra_seamless_sso_disabled", + "entra_service_principal_no_secrets_for_permanent_tier0_roles", "entra_users_mfa_enabled" ] }, diff --git a/prowler/config/config.py b/prowler/config/config.py index d651211801..337ada1f2c 100644 --- a/prowler/config/config.py +++ b/prowler/config/config.py @@ -49,7 +49,7 @@ class _MutableTimestamp: timestamp = _MutableTimestamp(datetime.today()) timestamp_utc = _MutableTimestamp(datetime.now(timezone.utc)) -prowler_version = "5.26.0" +prowler_version = "5.29.0" html_logo_url = "https://github.com/prowler-cloud/prowler/" square_logo_img = "https://raw.githubusercontent.com/prowler-cloud/prowler/dc7d2d5aeb92fdf12e8604f42ef6472cd3e8e889/docs/img/prowler-logo-black.png" aws_logo = "https://user-images.githubusercontent.com/38561120/235953920-3e3fba08-0795-41dc-b480-9bea57db9f2e.png" @@ -76,7 +76,9 @@ class Provider(str, Enum): ALIBABACLOUD = "alibabacloud" OPENSTACK = "openstack" IMAGE = "image" + SCALEWAY = "scaleway" VERCEL = "vercel" + OKTA = "okta" # Compliance diff --git a/prowler/config/config.yaml b/prowler/config/config.yaml index 0a6ed0d749..28f07c2051 100644 --- a/prowler/config/config.yaml +++ b/prowler/config/config.yaml @@ -26,6 +26,8 @@ aws: max_unused_access_keys_days: 45 # aws.iam_user_console_access_unused --> CIS recommends 45 days max_console_access_days: 45 + # aws.iam_user_access_not_stale_to_sagemaker --> default 90 days + max_unused_sagemaker_access_days: 90 # AWS EC2 Configuration # aws.ec2_elastic_ip_shodan @@ -465,6 +467,18 @@ azure: "1.3", ] + # Azure Storage + # azure.storage_smb_channel_encryption_with_secure_algorithm + # List of SMB channel encryption algorithms allowed on file shares. A storage + # account passes only if every enabled algorithm is in this list. Defaults to + # the value required by CIS (AES-256-GCM only, excluding weaker AES-128 ciphers). + recommended_smb_channel_encryption_algorithms: + [ + "AES-256-GCM", + # "AES-128-CCM", + # "AES-128-GCM", + ] + # Azure Virtual Machines # azure.vm_desired_sku_size # List of desired VM SKU sizes that are allowed in the organization @@ -647,3 +661,17 @@ vercel: - "_PASSWORD" - "_API_KEY" - "_PRIVATE_KEY" + +okta: + # Okta Sign-On Policies + # okta.signon_global_session_idle_timeout_15min + # Maximum acceptable Global Session idle timeout, in minutes. Defaults to + # 15 per DISA STIG V-273186 (OKTA-APP-000020); raise it only with an + # explicit risk acceptance. + okta_max_session_idle_minutes: 15 + # Okta Applications + # okta.application_admin_console_session_idle_timeout_15min + # Maximum acceptable Okta Admin Console app idle timeout, in minutes. + # Defaults to 15 per DISA STIG V-273187 (OKTA-APP-000025); raise it only + # with an explicit risk acceptance. + okta_admin_console_idle_timeout_max_minutes: 15 diff --git a/prowler/config/okta_mutelist_example.yaml b/prowler/config/okta_mutelist_example.yaml new file mode 100644 index 0000000000..1c6ff2ca8c --- /dev/null +++ b/prowler/config/okta_mutelist_example.yaml @@ -0,0 +1,19 @@ +### Account, Check and/or Region can be * to apply for all the cases. +### Account == +### Bare domain only — no scheme, no path, no trailing slash. +### Region is always "*" — Okta has no regional concept. +### Resources matches against the policy name (e.g. "Default Policy"), not the id. +### Resources and tags are lists that can have either Regex or Keywords. +### Tags is an optional list that matches on tuples of 'key=value' and are "ANDed" together. +### Use an alternation Regex to match one of multiple tags with "ORed" logic. +### For each check you can except Accounts, Regions, Resources and/or Tags. +########################### MUTELIST EXAMPLE ########################### +Mutelist: + Accounts: + "acme.okta.com": + Checks: + "signon_global_session_idle_timeout_15min": + Regions: + - "*" + Resources: + - "Default Policy" diff --git a/prowler/lib/check/check.py b/prowler/lib/check/check.py index 90956c79ef..0db2ce444a 100644 --- a/prowler/lib/check/check.py +++ b/prowler/lib/check/check.py @@ -785,19 +785,31 @@ def execute( is_finding_muted_args["team_id"] = ( team.id if team else global_provider.identity.user_id ) + elif global_provider.type == "scaleway": + is_finding_muted_args["organization_id"] = ( + global_provider.identity.organization_id + ) elif global_provider.type == "oraclecloud": is_finding_muted_args["tenancy_id"] = ( global_provider.identity.tenancy_id ) + elif global_provider.type == "okta": + is_finding_muted_args["org_domain"] = ( + global_provider.identity.org_domain + ) else: # External/custom provider — delegate identity args is_finding_muted_args = global_provider.get_mutelist_finding_args() + for finding in check_findings: if global_provider.type == "cloudflare": is_finding_muted_args["account_id"] = finding.account_id if global_provider.type == "azure": - is_finding_muted_args["subscription_id"] = ( - global_provider.identity.subscriptions.get(finding.subscription) + is_finding_muted_args["subscription_id"] = finding.subscription + is_finding_muted_args["subscription_name"] = ( + global_provider.identity.subscriptions.get( + finding.subscription, finding.subscription + ) ) is_finding_muted_args["finding"] = finding finding.muted = global_provider.mutelist.is_finding_muted( diff --git a/prowler/lib/check/compliance_models.py b/prowler/lib/check/compliance_models.py index ef11986662..86404e7030 100644 --- a/prowler/lib/check/compliance_models.py +++ b/prowler/lib/check/compliance_models.py @@ -103,7 +103,7 @@ class CIS_Requirement_Attribute(BaseModel): References: str -class EssentialEight_Requirement_Attribute_MaturityLevel(str, Enum): +class ASDEssentialEight_Requirement_Attribute_MaturityLevel(str, Enum): """ASD Essential Eight Maturity Level""" ML1 = "ML1" @@ -111,14 +111,14 @@ class EssentialEight_Requirement_Attribute_MaturityLevel(str, Enum): ML3 = "ML3" -class EssentialEight_Requirement_Attribute_AssessmentStatus(str, Enum): +class ASDEssentialEight_Requirement_Attribute_AssessmentStatus(str, Enum): """Essential Eight Requirement Attribute Assessment Status""" Manual = "Manual" Automated = "Automated" -class EssentialEight_Requirement_Attribute_CloudApplicability(str, Enum): +class ASDEssentialEight_Requirement_Attribute_CloudApplicability(str, Enum): """How well the ASD control maps to AWS cloud infrastructure.""" Full = "full" @@ -128,13 +128,13 @@ class EssentialEight_Requirement_Attribute_CloudApplicability(str, Enum): # Essential Eight Requirement Attribute -class EssentialEight_Requirement_Attribute(BaseModel): +class ASDEssentialEight_Requirement_Attribute(BaseModel): """ASD Essential Eight Requirement Attribute""" Section: str - MaturityLevel: EssentialEight_Requirement_Attribute_MaturityLevel - AssessmentStatus: EssentialEight_Requirement_Attribute_AssessmentStatus - CloudApplicability: EssentialEight_Requirement_Attribute_CloudApplicability + MaturityLevel: ASDEssentialEight_Requirement_Attribute_MaturityLevel + AssessmentStatus: ASDEssentialEight_Requirement_Attribute_AssessmentStatus + CloudApplicability: ASDEssentialEight_Requirement_Attribute_CloudApplicability MitigatedThreats: list[str] Description: str RationaleStatement: str @@ -293,7 +293,7 @@ class Compliance_Requirement(BaseModel): Name: Optional[str] = None Attributes: list[ Union[ - EssentialEight_Requirement_Attribute, + ASDEssentialEight_Requirement_Attribute, CIS_Requirement_Attribute, ENS_Requirement_Attribute, ISO27001_2013_Requirement_Attribute, diff --git a/prowler/lib/check/models.py b/prowler/lib/check/models.py index 5eb8ae50da..5acfd58a3f 100644 --- a/prowler/lib/check/models.py +++ b/prowler/lib/check/models.py @@ -958,6 +958,41 @@ class CheckReportGithub(Check_Report): ) +@dataclass +class CheckReportOkta(Check_Report): + """Contains the Okta Check's finding information.""" + + resource_name: str + resource_id: str + org_domain: str + region: str + + def __init__( + self, + metadata: Dict, + resource: Any, + resource_name: str = None, + resource_id: str = None, + org_domain: str = None, + region: str = "global", + ) -> None: + """Initialize the Okta Check's finding information. + + Args: + metadata: The metadata of the check. + resource: Basic information about the resource. + resource_name: The name of the resource related with the finding. + resource_id: The id of the resource related with the finding. + org_domain: The Okta organization domain related with the finding. + region: Always "global" — Okta has no regional concept. + """ + super().__init__(metadata, resource) + self.resource_name = resource_name or getattr(resource, "name", "") + self.resource_id = resource_id or getattr(resource, "id", "") + self.org_domain = org_domain or getattr(resource, "org_domain", "") + self.region = region + + @dataclass class CheckReportGoogleWorkspace(Check_Report): """Contains the Google Workspace Check's finding information.""" @@ -1308,6 +1343,54 @@ class CheckReportVercel(Check_Report): return "global" +@dataclass +class CheckReportScaleway(Check_Report): + """Contains the Scaleway Check's finding information. + + Scaleway scans run at the organization level. Most IAM/account-level + resources are global; regional resources expose a ``region`` attribute + on the underlying object, which we surface as the report ``region``. + """ + + resource_name: str + resource_id: str + organization_id: str + + def __init__( + self, + metadata: Dict, + resource: Any, + resource_name: str = None, + resource_id: str = None, + organization_id: str = None, + ) -> None: + """Initialize the Scaleway Check's finding information. + + Args: + metadata: Check metadata dictionary. + resource: The Scaleway resource being checked. + resource_name: Override for resource name. + resource_id: Override for resource ID. + organization_id: Override for the organization ID. + """ + super().__init__(metadata, resource) + self.resource_name = resource_name or getattr( + resource, "name", getattr(resource, "resource_name", "") + ) + self.resource_id = resource_id or getattr( + resource, "id", getattr(resource, "resource_id", "") + ) + self.organization_id = organization_id or getattr( + resource, "organization_id", "" + ) + self._region = getattr(resource, "region", None) or "global" + + @property + def region(self) -> str: + """Scaleway regional resources expose their own region; IAM is global.""" + return self._region + + # Testing Pending def load_check_metadata(metadata_file: str) -> CheckMetadata: """ diff --git a/prowler/lib/cli/parser.py b/prowler/lib/cli/parser.py index fc75e9618d..f36748e090 100644 --- a/prowler/lib/cli/parser.py +++ b/prowler/lib/cli/parser.py @@ -68,10 +68,10 @@ class ProwlerArgumentParser: self.parser = argparse.ArgumentParser( prog="prowler", formatter_class=RawTextHelpFormatter, - usage=f"prowler [-h] [--version] {{aws,azure,gcp,kubernetes,m365,github,googleworkspace,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack,vercel,dashboard,iac,image,llm{extra_providers_csv}}} ...", + usage=f"prowler [-h] [--version] {{aws,azure,gcp,kubernetes,m365,github,googleworkspace,okta,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack,scaleway,vercel,dashboard,iac,image,llm{extra_providers_csv}}} ...", epilog=f""" Available Cloud Providers: - {{aws,azure,gcp,kubernetes,m365,github,googleworkspace,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack,vercel,dashboard,iac,image,llm{extra_providers_csv}}} + {{aws,azure,gcp,kubernetes,m365,github,googleworkspace,okta,iac,llm,image,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack,scaleway,vercel{extra_providers_csv}}} aws AWS Provider azure Azure Provider gcp GCP Provider @@ -79,6 +79,7 @@ Available Cloud Providers: m365 Microsoft 365 Provider github GitHub Provider googleworkspace Google Workspace Provider + okta Okta Provider cloudflare Cloudflare Provider oraclecloud Oracle Cloud Infrastructure Provider openstack OpenStack Provider @@ -88,8 +89,10 @@ Available Cloud Providers: image Container Image Provider nhn NHN Provider (Unofficial) mongodbatlas MongoDB Atlas Provider + scaleway Scaleway Provider vercel Vercel Provider{extra_providers_text} + Available components: dashboard Local dashboard diff --git a/tests/providers/googleworkspace/services/gmail/gmail_auto_forwarding_disabled/__init__.py b/prowler/lib/outputs/compliance/asd_essential_eight/__init__.py similarity index 100% rename from tests/providers/googleworkspace/services/gmail/gmail_auto_forwarding_disabled/__init__.py rename to prowler/lib/outputs/compliance/asd_essential_eight/__init__.py diff --git a/prowler/lib/outputs/compliance/essential_eight/essential_eight.py b/prowler/lib/outputs/compliance/asd_essential_eight/asd_essential_eight.py similarity index 85% rename from prowler/lib/outputs/compliance/essential_eight/essential_eight.py rename to prowler/lib/outputs/compliance/asd_essential_eight/asd_essential_eight.py index 9438a613f8..75481fb6aa 100644 --- a/prowler/lib/outputs/compliance/essential_eight/essential_eight.py +++ b/prowler/lib/outputs/compliance/asd_essential_eight/asd_essential_eight.py @@ -4,7 +4,7 @@ from tabulate import tabulate from prowler.config.config import orange_color -def get_essential_eight_table( +def get_asd_essential_eight_table( findings: list, bulk_checks_metadata: dict, compliance_framework: str, @@ -13,7 +13,7 @@ def get_essential_eight_table( compliance_overview: bool, ): sections = {} - essential_eight_compliance_table = { + asd_essential_eight_compliance_table = { "Provider": [], "Section": [], "Status": [], @@ -26,7 +26,7 @@ def get_essential_eight_table( check = bulk_checks_metadata[finding.check_metadata.CheckID] check_compliances = check.Compliance for compliance in check_compliances: - if compliance.Framework == "Essential-Eight": + if compliance.Framework == "ASD-Essential-Eight": for requirement in compliance.Requirements: for attribute in requirement.Attributes: section = attribute.Section @@ -50,19 +50,19 @@ def get_essential_eight_table( sections = dict(sorted(sections.items())) for section in sections: - essential_eight_compliance_table["Provider"].append(compliance.Provider) - essential_eight_compliance_table["Section"].append(section) + asd_essential_eight_compliance_table["Provider"].append(compliance.Provider) + asd_essential_eight_compliance_table["Section"].append(section) if sections[section]["FAIL"] > 0: - essential_eight_compliance_table["Status"].append( + asd_essential_eight_compliance_table["Status"].append( f"{Fore.RED}FAIL({sections[section]['FAIL']}){Style.RESET_ALL}" ) elif sections[section]["PASS"] > 0: - essential_eight_compliance_table["Status"].append( + asd_essential_eight_compliance_table["Status"].append( f"{Fore.GREEN}PASS({sections[section]['PASS']}){Style.RESET_ALL}" ) else: - essential_eight_compliance_table["Status"].append("-") - essential_eight_compliance_table["Muted"].append( + asd_essential_eight_compliance_table["Status"].append("-") + asd_essential_eight_compliance_table["Muted"].append( f"{orange_color}{sections[section]['Muted']}{Style.RESET_ALL}" ) if len(fail_count) + len(pass_count) + len(muted_count) > 1: @@ -84,7 +84,7 @@ def get_essential_eight_table( ) print( tabulate( - essential_eight_compliance_table, + asd_essential_eight_compliance_table, headers="keys", tablefmt="rounded_grid", ) diff --git a/prowler/lib/outputs/compliance/essential_eight/essential_eight_aws.py b/prowler/lib/outputs/compliance/asd_essential_eight/asd_essential_eight_aws.py similarity index 92% rename from prowler/lib/outputs/compliance/essential_eight/essential_eight_aws.py rename to prowler/lib/outputs/compliance/asd_essential_eight/asd_essential_eight_aws.py index 198e3ab331..c264c81505 100644 --- a/prowler/lib/outputs/compliance/essential_eight/essential_eight_aws.py +++ b/prowler/lib/outputs/compliance/asd_essential_eight/asd_essential_eight_aws.py @@ -1,13 +1,13 @@ from prowler.config.config import timestamp from prowler.lib.check.compliance_models import Compliance -from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput -from prowler.lib.outputs.compliance.essential_eight.models import ( - EssentialEightAWSModel, +from prowler.lib.outputs.compliance.asd_essential_eight.models import ( + ASDEssentialEightAWSModel, ) +from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput from prowler.lib.outputs.finding import Finding -class EssentialEightAWS(ComplianceOutput): +class ASDEssentialEightAWS(ComplianceOutput): """ This class represents the AWS ASD Essential Eight compliance output. @@ -37,11 +37,11 @@ class EssentialEightAWS(ComplianceOutput): - None """ for finding in findings: - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: - compliance_row = EssentialEightAWSModel( + compliance_row = ASDEssentialEightAWSModel( Provider=finding.provider, Description=compliance.Description, AccountId=finding.account_uid, @@ -77,7 +77,7 @@ class EssentialEightAWS(ComplianceOutput): for requirement in compliance.Requirements: if not requirement.Checks: for attribute in requirement.Attributes: - compliance_row = EssentialEightAWSModel( + compliance_row = ASDEssentialEightAWSModel( Provider=compliance.Provider.lower(), Description=compliance.Description, AccountId="", diff --git a/prowler/lib/outputs/compliance/essential_eight/models.py b/prowler/lib/outputs/compliance/asd_essential_eight/models.py similarity index 86% rename from prowler/lib/outputs/compliance/essential_eight/models.py rename to prowler/lib/outputs/compliance/asd_essential_eight/models.py index 89e0191dac..f0760832e1 100644 --- a/prowler/lib/outputs/compliance/essential_eight/models.py +++ b/prowler/lib/outputs/compliance/asd_essential_eight/models.py @@ -1,9 +1,9 @@ from pydantic.v1 import BaseModel -class EssentialEightAWSModel(BaseModel): +class ASDEssentialEightAWSModel(BaseModel): """ - EssentialEightAWSModel generates a finding's output in AWS ASD Essential Eight Compliance format. + ASDEssentialEightAWSModel generates a finding's output in AWS ASD Essential Eight Compliance format. """ Provider: str diff --git a/prowler/lib/outputs/compliance/aws_well_architected/aws_well_architected.py b/prowler/lib/outputs/compliance/aws_well_architected/aws_well_architected.py index 38352484c2..4a157b0324 100644 --- a/prowler/lib/outputs/compliance/aws_well_architected/aws_well_architected.py +++ b/prowler/lib/outputs/compliance/aws_well_architected/aws_well_architected.py @@ -37,10 +37,9 @@ class AWSWellArchitected(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: compliance_row = AWSWellArchitectedModel( Provider=finding.provider, diff --git a/prowler/lib/outputs/compliance/c5/c5_aws.py b/prowler/lib/outputs/compliance/c5/c5_aws.py index 24d5239602..5f13b77d7a 100644 --- a/prowler/lib/outputs/compliance/c5/c5_aws.py +++ b/prowler/lib/outputs/compliance/c5/c5_aws.py @@ -35,10 +35,9 @@ class AWSC5(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: compliance_row = AWSC5Model( Provider=finding.provider, diff --git a/prowler/lib/outputs/compliance/c5/c5_azure.py b/prowler/lib/outputs/compliance/c5/c5_azure.py index 3c050a6581..1b3a9e6b90 100644 --- a/prowler/lib/outputs/compliance/c5/c5_azure.py +++ b/prowler/lib/outputs/compliance/c5/c5_azure.py @@ -35,10 +35,9 @@ class AzureC5(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: compliance_row = AzureC5Model( Provider=finding.provider, diff --git a/prowler/lib/outputs/compliance/c5/c5_gcp.py b/prowler/lib/outputs/compliance/c5/c5_gcp.py index 85ee9c25e9..84e66a141f 100644 --- a/prowler/lib/outputs/compliance/c5/c5_gcp.py +++ b/prowler/lib/outputs/compliance/c5/c5_gcp.py @@ -35,10 +35,9 @@ class GCPC5(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: compliance_row = GCPC5Model( Provider=finding.provider, diff --git a/prowler/lib/outputs/compliance/ccc/ccc_aws.py b/prowler/lib/outputs/compliance/ccc/ccc_aws.py index a9de0aeca7..1114425574 100644 --- a/prowler/lib/outputs/compliance/ccc/ccc_aws.py +++ b/prowler/lib/outputs/compliance/ccc/ccc_aws.py @@ -35,10 +35,9 @@ class CCC_AWS(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: compliance_row = CCC_AWSModel( Provider=finding.provider, diff --git a/prowler/lib/outputs/compliance/ccc/ccc_azure.py b/prowler/lib/outputs/compliance/ccc/ccc_azure.py index 2801b91f97..e0cdf32330 100644 --- a/prowler/lib/outputs/compliance/ccc/ccc_azure.py +++ b/prowler/lib/outputs/compliance/ccc/ccc_azure.py @@ -35,10 +35,9 @@ class CCC_Azure(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: compliance_row = CCC_AzureModel( Provider=finding.provider, diff --git a/prowler/lib/outputs/compliance/ccc/ccc_gcp.py b/prowler/lib/outputs/compliance/ccc/ccc_gcp.py index 9793834776..326a15e5c3 100644 --- a/prowler/lib/outputs/compliance/ccc/ccc_gcp.py +++ b/prowler/lib/outputs/compliance/ccc/ccc_gcp.py @@ -35,10 +35,9 @@ class CCC_GCP(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: compliance_row = CCC_GCPModel( Provider=finding.provider, diff --git a/prowler/lib/outputs/compliance/cis/cis_alibabacloud.py b/prowler/lib/outputs/compliance/cis/cis_alibabacloud.py index adf8ef2af1..a1880d3af9 100644 --- a/prowler/lib/outputs/compliance/cis/cis_alibabacloud.py +++ b/prowler/lib/outputs/compliance/cis/cis_alibabacloud.py @@ -35,10 +35,9 @@ class AlibabaCloudCIS(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: compliance_row = AlibabaCloudCISModel( Provider=finding.provider, diff --git a/prowler/lib/outputs/compliance/cis/cis_aws.py b/prowler/lib/outputs/compliance/cis/cis_aws.py index 749a3b7463..ac0250150c 100644 --- a/prowler/lib/outputs/compliance/cis/cis_aws.py +++ b/prowler/lib/outputs/compliance/cis/cis_aws.py @@ -35,10 +35,9 @@ class AWSCIS(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: compliance_row = AWSCISModel( Provider=finding.provider, diff --git a/prowler/lib/outputs/compliance/cis/cis_azure.py b/prowler/lib/outputs/compliance/cis/cis_azure.py index 5c5e4cd486..b1e09ca453 100644 --- a/prowler/lib/outputs/compliance/cis/cis_azure.py +++ b/prowler/lib/outputs/compliance/cis/cis_azure.py @@ -35,10 +35,9 @@ class AzureCIS(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: compliance_row = AzureCISModel( Provider=finding.provider, diff --git a/prowler/lib/outputs/compliance/cis/cis_gcp.py b/prowler/lib/outputs/compliance/cis/cis_gcp.py index 328726aa9b..b2889333be 100644 --- a/prowler/lib/outputs/compliance/cis/cis_gcp.py +++ b/prowler/lib/outputs/compliance/cis/cis_gcp.py @@ -35,10 +35,9 @@ class GCPCIS(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: compliance_row = GCPCISModel( Provider=finding.provider, diff --git a/prowler/lib/outputs/compliance/cis/cis_github.py b/prowler/lib/outputs/compliance/cis/cis_github.py index 0082b891e5..2ce5b64480 100644 --- a/prowler/lib/outputs/compliance/cis/cis_github.py +++ b/prowler/lib/outputs/compliance/cis/cis_github.py @@ -35,10 +35,9 @@ class GithubCIS(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: compliance_row = GithubCISModel( Provider=finding.provider, diff --git a/prowler/lib/outputs/compliance/cis/cis_googleworkspace.py b/prowler/lib/outputs/compliance/cis/cis_googleworkspace.py index a4b58bb3b4..d74375bf85 100644 --- a/prowler/lib/outputs/compliance/cis/cis_googleworkspace.py +++ b/prowler/lib/outputs/compliance/cis/cis_googleworkspace.py @@ -35,10 +35,9 @@ class GoogleWorkspaceCIS(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: compliance_row = GoogleWorkspaceCISModel( Provider=finding.provider, diff --git a/prowler/lib/outputs/compliance/cis/cis_kubernetes.py b/prowler/lib/outputs/compliance/cis/cis_kubernetes.py index e8fee839fc..9607123e44 100644 --- a/prowler/lib/outputs/compliance/cis/cis_kubernetes.py +++ b/prowler/lib/outputs/compliance/cis/cis_kubernetes.py @@ -35,10 +35,9 @@ class KubernetesCIS(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: compliance_row = KubernetesCISModel( Provider=finding.provider, diff --git a/prowler/lib/outputs/compliance/cis/cis_m365.py b/prowler/lib/outputs/compliance/cis/cis_m365.py index 3c7de43542..1a00166946 100644 --- a/prowler/lib/outputs/compliance/cis/cis_m365.py +++ b/prowler/lib/outputs/compliance/cis/cis_m365.py @@ -35,10 +35,9 @@ class M365CIS(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: compliance_row = M365CISModel( Provider=finding.provider, diff --git a/prowler/lib/outputs/compliance/cis/cis_oraclecloud.py b/prowler/lib/outputs/compliance/cis/cis_oraclecloud.py index 19e6e1d8c8..117c3ca004 100644 --- a/prowler/lib/outputs/compliance/cis/cis_oraclecloud.py +++ b/prowler/lib/outputs/compliance/cis/cis_oraclecloud.py @@ -35,10 +35,9 @@ class OracleCloudCIS(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: compliance_row = OracleCloudCISModel( Provider=finding.provider, diff --git a/prowler/lib/outputs/compliance/cisa_scuba/cisa_scuba_googleworkspace.py b/prowler/lib/outputs/compliance/cisa_scuba/cisa_scuba_googleworkspace.py index 8250d1bcce..9f1336e832 100644 --- a/prowler/lib/outputs/compliance/cisa_scuba/cisa_scuba_googleworkspace.py +++ b/prowler/lib/outputs/compliance/cisa_scuba/cisa_scuba_googleworkspace.py @@ -37,10 +37,9 @@ class GoogleWorkspaceCISASCuBA(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: compliance_row = GoogleWorkspaceCISASCuBAModel( Provider=finding.provider, diff --git a/prowler/lib/outputs/compliance/compliance.py b/prowler/lib/outputs/compliance/compliance.py index b3885b57c9..97fb98e11e 100644 --- a/prowler/lib/outputs/compliance/compliance.py +++ b/prowler/lib/outputs/compliance/compliance.py @@ -1,6 +1,9 @@ import sys from prowler.lib.logger import logger +from prowler.lib.outputs.compliance.asd_essential_eight.asd_essential_eight import ( + get_asd_essential_eight_table, +) from prowler.lib.outputs.compliance.c5.c5 import get_c5_table from prowler.lib.outputs.compliance.ccc.ccc import get_ccc_table from prowler.lib.outputs.compliance.cis.cis import get_cis_table @@ -9,9 +12,6 @@ from prowler.lib.outputs.compliance.compliance_check import ( # noqa: F401 - re ) from prowler.lib.outputs.compliance.csa.csa import get_csa_table from prowler.lib.outputs.compliance.ens.ens import get_ens_table -from prowler.lib.outputs.compliance.essential_eight.essential_eight import ( - get_essential_eight_table, -) from prowler.lib.outputs.compliance.generic.generic_table import ( get_generic_compliance_table, ) @@ -233,8 +233,8 @@ def display_compliance_table( output_directory, compliance_overview, ) - elif "essential_eight" in compliance_framework: - get_essential_eight_table( + elif "asd_essential_eight" in compliance_framework: + get_asd_essential_eight_table( findings, bulk_checks_metadata, compliance_framework, diff --git a/prowler/lib/outputs/compliance/csa/csa_alibabacloud.py b/prowler/lib/outputs/compliance/csa/csa_alibabacloud.py index 084edbaa4b..e0867ab5f1 100644 --- a/prowler/lib/outputs/compliance/csa/csa_alibabacloud.py +++ b/prowler/lib/outputs/compliance/csa/csa_alibabacloud.py @@ -35,10 +35,9 @@ class AlibabaCloudCSA(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: compliance_row = AlibabaCloudCSAModel( Provider=finding.provider, diff --git a/prowler/lib/outputs/compliance/csa/csa_aws.py b/prowler/lib/outputs/compliance/csa/csa_aws.py index 0f371bfb3a..6309dbe287 100644 --- a/prowler/lib/outputs/compliance/csa/csa_aws.py +++ b/prowler/lib/outputs/compliance/csa/csa_aws.py @@ -35,10 +35,9 @@ class AWSCSA(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: compliance_row = AWSCSAModel( Provider=finding.provider, diff --git a/prowler/lib/outputs/compliance/csa/csa_azure.py b/prowler/lib/outputs/compliance/csa/csa_azure.py index 9bb16e6f11..cf9a1064e6 100644 --- a/prowler/lib/outputs/compliance/csa/csa_azure.py +++ b/prowler/lib/outputs/compliance/csa/csa_azure.py @@ -35,10 +35,9 @@ class AzureCSA(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: compliance_row = AzureCSAModel( Provider=finding.provider, diff --git a/prowler/lib/outputs/compliance/csa/csa_gcp.py b/prowler/lib/outputs/compliance/csa/csa_gcp.py index 5c1b400525..4a829295db 100644 --- a/prowler/lib/outputs/compliance/csa/csa_gcp.py +++ b/prowler/lib/outputs/compliance/csa/csa_gcp.py @@ -35,10 +35,9 @@ class GCPCSA(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: compliance_row = GCPCSAModel( Provider=finding.provider, diff --git a/prowler/lib/outputs/compliance/csa/csa_oraclecloud.py b/prowler/lib/outputs/compliance/csa/csa_oraclecloud.py index 9b3d36c168..107589c0f7 100644 --- a/prowler/lib/outputs/compliance/csa/csa_oraclecloud.py +++ b/prowler/lib/outputs/compliance/csa/csa_oraclecloud.py @@ -35,10 +35,9 @@ class OracleCloudCSA(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: compliance_row = OracleCloudCSAModel( Provider=finding.provider, diff --git a/prowler/lib/outputs/compliance/ens/ens_aws.py b/prowler/lib/outputs/compliance/ens/ens_aws.py index 01a212d9af..40d185294b 100644 --- a/prowler/lib/outputs/compliance/ens/ens_aws.py +++ b/prowler/lib/outputs/compliance/ens/ens_aws.py @@ -35,10 +35,9 @@ class AWSENS(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: compliance_row = AWSENSModel( Provider=finding.provider, diff --git a/prowler/lib/outputs/compliance/ens/ens_azure.py b/prowler/lib/outputs/compliance/ens/ens_azure.py index 35385ab673..20d727fed0 100644 --- a/prowler/lib/outputs/compliance/ens/ens_azure.py +++ b/prowler/lib/outputs/compliance/ens/ens_azure.py @@ -35,10 +35,9 @@ class AzureENS(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: compliance_row = AzureENSModel( Provider=finding.provider, diff --git a/prowler/lib/outputs/compliance/ens/ens_gcp.py b/prowler/lib/outputs/compliance/ens/ens_gcp.py index 81d6d33b14..8a2baaca66 100644 --- a/prowler/lib/outputs/compliance/ens/ens_gcp.py +++ b/prowler/lib/outputs/compliance/ens/ens_gcp.py @@ -35,10 +35,9 @@ class GCPENS(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: compliance_row = GCPENSModel( Provider=finding.provider, diff --git a/prowler/lib/outputs/compliance/generic/generic.py b/prowler/lib/outputs/compliance/generic/generic.py index d9dbe8abe9..c7217db5b3 100644 --- a/prowler/lib/outputs/compliance/generic/generic.py +++ b/prowler/lib/outputs/compliance/generic/generic.py @@ -35,10 +35,9 @@ class GenericCompliance(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: compliance_row = GenericComplianceModel( Provider=finding.provider, diff --git a/prowler/lib/outputs/compliance/iso27001/iso27001_aws.py b/prowler/lib/outputs/compliance/iso27001/iso27001_aws.py index b6da583af2..282f27a218 100644 --- a/prowler/lib/outputs/compliance/iso27001/iso27001_aws.py +++ b/prowler/lib/outputs/compliance/iso27001/iso27001_aws.py @@ -35,10 +35,9 @@ class AWSISO27001(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: compliance_row = AWSISO27001Model( Provider=finding.provider, diff --git a/prowler/lib/outputs/compliance/iso27001/iso27001_azure.py b/prowler/lib/outputs/compliance/iso27001/iso27001_azure.py index 1e36005440..0112bbd7e1 100644 --- a/prowler/lib/outputs/compliance/iso27001/iso27001_azure.py +++ b/prowler/lib/outputs/compliance/iso27001/iso27001_azure.py @@ -35,10 +35,9 @@ class AzureISO27001(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: compliance_row = AzureISO27001Model( Provider=finding.provider, diff --git a/prowler/lib/outputs/compliance/iso27001/iso27001_gcp.py b/prowler/lib/outputs/compliance/iso27001/iso27001_gcp.py index 9f08e3d920..f60a30e67e 100644 --- a/prowler/lib/outputs/compliance/iso27001/iso27001_gcp.py +++ b/prowler/lib/outputs/compliance/iso27001/iso27001_gcp.py @@ -35,10 +35,9 @@ class GCPISO27001(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: compliance_row = GCPISO27001Model( Provider=finding.provider, diff --git a/prowler/lib/outputs/compliance/iso27001/iso27001_kubernetes.py b/prowler/lib/outputs/compliance/iso27001/iso27001_kubernetes.py index b0e8904ebe..59fd593ce6 100644 --- a/prowler/lib/outputs/compliance/iso27001/iso27001_kubernetes.py +++ b/prowler/lib/outputs/compliance/iso27001/iso27001_kubernetes.py @@ -35,10 +35,9 @@ class KubernetesISO27001(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: compliance_row = KubernetesISO27001Model( Provider=finding.provider, diff --git a/prowler/lib/outputs/compliance/iso27001/iso27001_m365.py b/prowler/lib/outputs/compliance/iso27001/iso27001_m365.py index cf12bbd841..cf6712a4a6 100644 --- a/prowler/lib/outputs/compliance/iso27001/iso27001_m365.py +++ b/prowler/lib/outputs/compliance/iso27001/iso27001_m365.py @@ -35,9 +35,9 @@ class M365ISO27001(ComplianceOutput): - None """ for finding in findings: - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: compliance_row = M365ISO27001Model( Provider=finding.provider, diff --git a/prowler/lib/outputs/compliance/iso27001/iso27001_nhn.py b/prowler/lib/outputs/compliance/iso27001/iso27001_nhn.py index 6c89b7333f..2ad5a0bda4 100644 --- a/prowler/lib/outputs/compliance/iso27001/iso27001_nhn.py +++ b/prowler/lib/outputs/compliance/iso27001/iso27001_nhn.py @@ -35,9 +35,9 @@ class NHNISO27001(ComplianceOutput): - None """ for finding in findings: - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: compliance_row = NHNISO27001Model( Provider=finding.provider, diff --git a/prowler/lib/outputs/compliance/kisa_ismsp/kisa_ismsp_aws.py b/prowler/lib/outputs/compliance/kisa_ismsp/kisa_ismsp_aws.py index f65430b1a3..f927c2d4b1 100644 --- a/prowler/lib/outputs/compliance/kisa_ismsp/kisa_ismsp_aws.py +++ b/prowler/lib/outputs/compliance/kisa_ismsp/kisa_ismsp_aws.py @@ -35,10 +35,9 @@ class AWSKISAISMSP(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: compliance_row = AWSKISAISMSPModel( Provider=finding.provider, diff --git a/prowler/lib/outputs/compliance/mitre_attack/mitre_attack_aws.py b/prowler/lib/outputs/compliance/mitre_attack/mitre_attack_aws.py index 9a4c4fc2c3..33a7aca74b 100644 --- a/prowler/lib/outputs/compliance/mitre_attack/mitre_attack_aws.py +++ b/prowler/lib/outputs/compliance/mitre_attack/mitre_attack_aws.py @@ -36,10 +36,9 @@ class AWSMitreAttack(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: compliance_row = AWSMitreAttackModel( Provider=finding.provider, Description=compliance.Description, diff --git a/prowler/lib/outputs/compliance/mitre_attack/mitre_attack_azure.py b/prowler/lib/outputs/compliance/mitre_attack/mitre_attack_azure.py index 5787bbfba6..0254aad86e 100644 --- a/prowler/lib/outputs/compliance/mitre_attack/mitre_attack_azure.py +++ b/prowler/lib/outputs/compliance/mitre_attack/mitre_attack_azure.py @@ -36,10 +36,9 @@ class AzureMitreAttack(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: compliance_row = AzureMitreAttackModel( Provider=finding.provider, Description=compliance.Description, diff --git a/prowler/lib/outputs/compliance/mitre_attack/mitre_attack_gcp.py b/prowler/lib/outputs/compliance/mitre_attack/mitre_attack_gcp.py index bae7920e43..4634273494 100644 --- a/prowler/lib/outputs/compliance/mitre_attack/mitre_attack_gcp.py +++ b/prowler/lib/outputs/compliance/mitre_attack/mitre_attack_gcp.py @@ -36,10 +36,9 @@ class GCPMitreAttack(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: compliance_row = GCPMitreAttackModel( Provider=finding.provider, Description=compliance.Description, diff --git a/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_alibaba.py b/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_alibaba.py index 66fabe51b3..510c098dff 100644 --- a/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_alibaba.py +++ b/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_alibaba.py @@ -37,10 +37,9 @@ class ProwlerThreatScoreAlibaba(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: compliance_row = ProwlerThreatScoreAlibabaModel( Provider=finding.provider, diff --git a/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_aws.py b/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_aws.py index b896e55528..ae992f8a75 100644 --- a/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_aws.py +++ b/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_aws.py @@ -37,10 +37,9 @@ class ProwlerThreatScoreAWS(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: compliance_row = ProwlerThreatScoreAWSModel( Provider=finding.provider, diff --git a/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_azure.py b/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_azure.py index bbc3749843..dd0a3b9a56 100644 --- a/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_azure.py +++ b/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_azure.py @@ -37,10 +37,9 @@ class ProwlerThreatScoreAzure(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: compliance_row = ProwlerThreatScoreAzureModel( Provider=finding.provider, diff --git a/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_gcp.py b/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_gcp.py index 4b03e269a3..c3bad98ade 100644 --- a/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_gcp.py +++ b/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_gcp.py @@ -37,10 +37,9 @@ class ProwlerThreatScoreGCP(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: compliance_row = ProwlerThreatScoreGCPModel( Provider=finding.provider, diff --git a/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_kubernetes.py b/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_kubernetes.py index 7b35dab312..51f88348f0 100644 --- a/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_kubernetes.py +++ b/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_kubernetes.py @@ -37,10 +37,9 @@ class ProwlerThreatScoreKubernetes(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: compliance_row = ProwlerThreatScoreKubernetesModel( Provider=finding.provider, diff --git a/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_m365.py b/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_m365.py index 6b0cc88a15..d0b2ad635c 100644 --- a/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_m365.py +++ b/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_m365.py @@ -37,10 +37,9 @@ class ProwlerThreatScoreM365(ComplianceOutput): - None """ for finding in findings: - # Get the compliance requirements for the finding - finding_requirements = finding.compliance.get(compliance_name, []) for requirement in compliance.Requirements: - if requirement.Id in finding_requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: for attribute in requirement.Attributes: compliance_row = ProwlerThreatScoreM365Model( Provider=finding.provider, diff --git a/prowler/lib/outputs/finding.py b/prowler/lib/outputs/finding.py index e119d5ce15..204c60de52 100644 --- a/prowler/lib/outputs/finding.py +++ b/prowler/lib/outputs/finding.py @@ -187,9 +187,11 @@ class Finding(BaseModel): output_data["account_uid"] = ( output_data["account_organization_uid"] if "Tenant:" in check_output.subscription - else provider.identity.subscriptions[check_output.subscription] + else check_output.subscription + ) + output_data["account_name"] = provider.identity.subscriptions.get( + check_output.subscription, check_output.subscription ) - output_data["account_name"] = check_output.subscription output_data["resource_name"] = check_output.resource_name output_data["resource_uid"] = check_output.resource_id output_data["region"] = check_output.location @@ -425,6 +427,33 @@ class Finding(BaseModel): output_data["resource_uid"] = check_output.resource_id output_data["region"] = "global" + elif provider.type == "okta": + output_data["auth_method"] = provider.auth_method + output_data["account_uid"] = get_nested_attribute( + provider, "identity.org_domain" + ) + output_data["account_name"] = get_nested_attribute( + provider, "identity.org_domain" + ) + output_data["account_organization_uid"] = get_nested_attribute( + provider, "identity.client_id" + ) + output_data["resource_name"] = check_output.resource_name + output_data["resource_uid"] = check_output.resource_id + output_data["region"] = "global" + + elif provider.type == "scaleway": + output_data["auth_method"] = "api_key" + output_data["account_uid"] = get_nested_attribute( + provider, "identity.organization_id" + ) + output_data["account_name"] = get_nested_attribute( + provider, "identity.bearer_email" + ) or get_nested_attribute(provider, "identity.organization_id") + output_data["resource_name"] = check_output.resource_name + output_data["resource_uid"] = check_output.resource_id + output_data["region"] = check_output.region + elif provider.type == "alibabacloud": output_data["auth_method"] = get_nested_attribute( provider, "identity.identity_arn" diff --git a/prowler/lib/outputs/html/html.py b/prowler/lib/outputs/html/html.py index 1cd097815b..9164a9a56d 100644 --- a/prowler/lib/outputs/html/html.py +++ b/prowler/lib/outputs/html/html.py @@ -492,8 +492,11 @@ class HTML(Output): """ try: printed_subscriptions = [] - for key, value in provider.identity.subscriptions.items(): - intermediate = f"{key} : {value}" + for ( + subscription_id, + display_name, + ) in provider.identity.subscriptions.items(): + intermediate = f"{display_name} : {subscription_id}" printed_subscriptions.append(intermediate) # check if identity is str(coming from SP) or dict(coming from browser or) @@ -1397,6 +1400,127 @@ class HTML(Output): ) return "" + @staticmethod + def get_okta_assessment_summary(provider: Provider) -> str: + """ + get_okta_assessment_summary gets the HTML assessment summary for the Okta provider + + Args: + provider (Provider): the Okta provider object + + Returns: + str: HTML assessment summary for the Okta provider + """ + try: + assessment_items = f""" +
  • + Okta Domain: {provider.identity.org_domain} +
  • """ + + credentials_items = f""" +
  • + Authentication: {provider.auth_method} +
  • +
  • + Client ID: {provider.identity.client_id} +
  • """ + + return f""" +
    +
    +
    + Okta Assessment Summary +
    +
      {assessment_items} +
    +
    +
    +
    +
    +
    + Okta Credentials +
    +
      {credentials_items} +
    +
    +
    """ + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + return "" + + @staticmethod + def get_scaleway_assessment_summary(provider: Provider) -> str: + """ + get_scaleway_assessment_summary gets the HTML assessment summary for the Scaleway provider + + Args: + provider (Provider): the Scaleway provider object + + Returns: + str: HTML assessment summary for the Scaleway provider + """ + try: + assessment_items = f""" +
  • + Organization ID: {provider.identity.organization_id} +
  • """ + + credentials_items = """ +
  • + Authentication: API Key +
  • """ + + access_key = getattr(provider.session, "access_key", None) + if access_key: + credentials_items += f""" +
  • + Access Key: {access_key} +
  • """ + + bearer_type = getattr(provider.identity, "bearer_type", None) + bearer_email = getattr(provider.identity, "bearer_email", None) + bearer_id = getattr(provider.identity, "bearer_id", None) + if bearer_type: + bearer_label = bearer_email or bearer_id or "-" + credentials_items += f""" +
  • + Bearer: {bearer_type} ({bearer_label}) +
  • """ + + region = getattr(provider.session, "default_region", None) + if region: + credentials_items += f""" +
  • + Default Region: {region} +
  • """ + + return f""" +
    +
    +
    + Scaleway Assessment Summary +
    +
      {assessment_items} +
    +
    +
    +
    +
    +
    + Scaleway Credentials +
    +
      {credentials_items} +
    +
    +
    """ + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + return "" + @staticmethod def get_assessment_summary(provider: Provider) -> str: """ diff --git a/prowler/lib/outputs/outputs.py b/prowler/lib/outputs/outputs.py index 7bb9a396da..b504129b3e 100644 --- a/prowler/lib/outputs/outputs.py +++ b/prowler/lib/outputs/outputs.py @@ -40,6 +40,10 @@ def stdout_report(finding, color, verbose, status, fix, provider=None): details = finding.location elif finding.check_metadata.Provider == "vercel": details = finding.region + elif finding.check_metadata.Provider == "okta": + details = finding.region + elif finding.check_metadata.Provider == "scaleway": + details = finding.region else: # Dynamic fallback: any external/custom provider if provider is None: diff --git a/prowler/lib/outputs/slack/slack.py b/prowler/lib/outputs/slack/slack.py index 1b1773dc92..d32214cc7c 100644 --- a/prowler/lib/outputs/slack/slack.py +++ b/prowler/lib/outputs/slack/slack.py @@ -82,8 +82,11 @@ class Slack: logo = gcp_logo elif provider.type == "azure": printed_subscriptions = [] - for key, value in provider.identity.subscriptions.items(): - intermediate = f"- *{key}: {value}*\n" + for ( + subscription_id, + display_name, + ) in provider.identity.subscriptions.items(): + intermediate = f"- *{subscription_id}: {display_name}*\n" printed_subscriptions.append(intermediate) identity = f"Azure Subscriptions:\n{''.join(printed_subscriptions)}" logo = azure_logo diff --git a/prowler/lib/outputs/summary_table.py b/prowler/lib/outputs/summary_table.py index 06d520ab32..5d227d06e4 100644 --- a/prowler/lib/outputs/summary_table.py +++ b/prowler/lib/outputs/summary_table.py @@ -108,6 +108,12 @@ def display_summary_table( ) else: audited_entities = provider.identity.username or "Personal Account" + elif provider.type == "okta": + entity_type = "Okta Org" + audited_entities = provider.identity.org_domain + elif provider.type == "scaleway": + entity_type = "Organization" + audited_entities = provider.identity.organization_id else: # Dynamic fallback: any external/custom provider entity_type, audited_entities = provider.get_summary_entity() @@ -188,9 +194,13 @@ def display_summary_table( print( f"\n{entity_type} {Fore.YELLOW}{audited_entities}{Style.RESET_ALL} Scan Results (severity columns are for fails only):" ) - if provider == "azure": + if provider.type == "azure": + scanned_subscriptions = ", ".join( + f"{display_name} ({subscription_id})" + for subscription_id, display_name in provider.identity.subscriptions.items() + ) print( - f"\nSubscriptions scanned: {Fore.YELLOW}{' '.join(provider.identity.subscriptions.keys())}{Style.RESET_ALL}" + f"\nSubscriptions scanned: {Fore.YELLOW}{scanned_subscriptions}{Style.RESET_ALL}" ) print(tabulate(findings_table, headers="keys", tablefmt="rounded_grid")) print( diff --git a/prowler/lib/utils/vulnerability_references.py b/prowler/lib/utils/vulnerability_references.py new file mode 100644 index 0000000000..63afe3c2c6 --- /dev/null +++ b/prowler/lib/utils/vulnerability_references.py @@ -0,0 +1,90 @@ +import re +from urllib.parse import parse_qs, urlparse + +AQUA_REFERENCE_HOST = "avd.aquasec.com" +GITHUB_ADVISORY_URL = "https://github.com/advisories/{advisory_id}" +PROWLER_HUB_CHECK_URL = "https://hub.prowler.com/check/{check_id}" +_CVE_ID_PATTERN = re.compile(r"^CVE-\d{4}-\d+$", re.IGNORECASE) +_GHSA_ID_PATTERN = re.compile(r"^GHSA(?:-[a-z0-9]{4}){3}$", re.IGNORECASE) + + +def _dedupe_preserve_order(urls: list[str]) -> list[str]: + seen: set[str] = set() + ordered_urls: list[str] = [] + + for url in urls: + if not url or not url.strip(): + continue + + normalized_url = url.strip() + if normalized_url in seen: + continue + + seen.add(normalized_url) + ordered_urls.append(normalized_url) + + return ordered_urls + + +def _is_aqua_reference(url: str) -> bool: + return AQUA_REFERENCE_HOST in urlparse(url).netloc.lower() + + +def _build_cve_org_url(vulnerability_id: str) -> str: + return f"https://www.cve.org/CVERecord?id={vulnerability_id.upper()}" + + +def build_finding_reference_url(finding_id: str) -> str: + """Map a Trivy finding ID to a stable, real reference URL. + + - CVE-XXXX-NNNN → cve.org record + - GHSA-… → github.com/advisories + - everything else → hub.prowler.com/check/, stripping a leading + "AVD-" prefix because Prowler Hub indexes Trivy rules by the + non-prefixed ID (e.g., "AWS-0001" not "AVD-AWS-0001"). + """ + normalized = finding_id.strip().upper() + if _CVE_ID_PATTERN.match(normalized): + return _build_cve_org_url(normalized) + if _GHSA_ID_PATTERN.match(normalized): + return GITHUB_ADVISORY_URL.format(advisory_id=normalized) + hub_id = normalized[4:] if normalized.startswith("AVD-") else normalized + return PROWLER_HUB_CHECK_URL.format(check_id=hub_id) + + +def _is_cve_org_url(url: str, vulnerability_id: str) -> bool: + parsed_url = urlparse(url) + if parsed_url.netloc.lower() != "www.cve.org": + return False + + query_value = parse_qs(parsed_url.query).get("id", [""])[0] + return query_value.upper() == vulnerability_id.upper() + + +def resolve_vulnerability_reference_urls( + vulnerability_id: str, + references: list[str] | None = None, + primary_url: str = "", +) -> tuple[str, list[str]]: + """Resolve non-Aqua vulnerability URLs, prioritizing official CVE destinations.""" + + candidate_urls = list(references or []) + if primary_url and primary_url not in candidate_urls: + candidate_urls.append(primary_url) + + filtered_urls = _dedupe_preserve_order( + [url for url in candidate_urls if not _is_aqua_reference(url)] + ) + + if not _CVE_ID_PATTERN.match(vulnerability_id): + return "", filtered_urls + + cve_org_urls = [ + url for url in filtered_urls if _is_cve_org_url(url, vulnerability_id) + ] + + recommendation_url = ( + cve_org_urls[0] if cve_org_urls else _build_cve_org_url(vulnerability_id) + ) + + return recommendation_url, [recommendation_url] diff --git a/prowler/providers/aws/aws_regions_by_service.json b/prowler/providers/aws/aws_regions_by_service.json index f525ce2155..2e451910fb 100644 --- a/prowler/providers/aws/aws_regions_by_service.json +++ b/prowler/providers/aws/aws_regions_by_service.json @@ -482,6 +482,7 @@ "ap-southeast-3", "ap-southeast-4", "ap-southeast-5", + "ap-southeast-7", "ca-central-1", "ca-west-1", "eu-central-1", @@ -506,7 +507,9 @@ "cn-north-1", "cn-northwest-1" ], - "aws-eusc": [], + "aws-eusc": [ + "eusc-de-east-1" + ], "aws-us-gov": [ "us-gov-east-1", "us-gov-west-1" @@ -1231,6 +1234,21 @@ "aws-us-gov": [] } }, + "aws-devops-agent": { + "regions": { + "aws": [ + "ap-northeast-1", + "ap-southeast-2", + "eu-central-1", + "eu-west-1", + "us-east-1", + "us-west-2" + ], + "aws-cn": [], + "aws-eusc": [], + "aws-us-gov": [] + } + }, "awshealthdashboard": { "regions": { "aws": [ @@ -1590,6 +1608,7 @@ "ap-southeast-2", "ca-central-1", "eu-central-1", + "eu-south-2", "eu-west-1", "eu-west-2", "us-east-1", @@ -2193,6 +2212,7 @@ "ap-southeast-2", "ap-southeast-3", "ap-southeast-5", + "ap-southeast-6", "ap-southeast-7", "ca-central-1", "ca-west-1", @@ -2255,6 +2275,8 @@ "ap-southeast-2", "ap-southeast-3", "ap-southeast-4", + "ap-southeast-5", + "ap-southeast-7", "ca-central-1", "ca-west-1", "eu-central-1", @@ -2444,6 +2466,9 @@ "ap-southeast-2", "ap-southeast-3", "ap-southeast-4", + "ap-southeast-5", + "ap-southeast-6", + "ap-southeast-7", "ca-central-1", "eu-central-1", "eu-central-2", @@ -2589,7 +2614,9 @@ "cn-north-1", "cn-northwest-1" ], - "aws-eusc": [], + "aws-eusc": [ + "eusc-de-east-1" + ], "aws-us-gov": [ "us-gov-east-1", "us-gov-west-1" @@ -2796,6 +2823,7 @@ "aws": [ "af-south-1", "ap-east-1", + "ap-east-2", "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", @@ -2806,6 +2834,7 @@ "ap-southeast-3", "ap-southeast-4", "ap-southeast-5", + "ap-southeast-6", "ap-southeast-7", "ca-central-1", "ca-west-1", @@ -3767,6 +3796,7 @@ "ap-southeast-5", "ap-southeast-7", "ca-central-1", + "ca-west-1", "eu-central-1", "eu-central-2", "eu-north-1", @@ -3829,7 +3859,9 @@ "us-west-2" ], "aws-cn": [], - "aws-eusc": [], + "aws-eusc": [ + "eusc-de-east-1" + ], "aws-us-gov": [ "us-gov-east-1", "us-gov-west-1" @@ -3890,17 +3922,22 @@ "dsql": { "regions": { "aws": [ + "ap-east-1", "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", + "ap-south-1", + "ap-southeast-1", "ap-southeast-2", "ap-southeast-4", "ca-central-1", "ca-west-1", "eu-central-1", + "eu-north-1", "eu-west-1", "eu-west-2", "eu-west-3", + "sa-east-1", "us-east-1", "us-east-2", "us-west-2" @@ -4681,15 +4718,19 @@ "aws": [ "af-south-1", "ap-east-1", + "ap-east-2", "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", "ap-south-1", + "ap-south-2", "ap-southeast-1", "ap-southeast-2", "ap-southeast-3", "ap-southeast-4", "ap-southeast-5", + "ap-southeast-6", + "ap-southeast-7", "ca-central-1", "ca-west-1", "eu-central-1", @@ -4703,6 +4744,7 @@ "il-central-1", "me-central-1", "me-south-1", + "mx-central-1", "sa-east-1", "us-east-1", "us-east-2", @@ -5260,6 +5302,7 @@ "ap-southeast-3", "ap-southeast-4", "ap-southeast-5", + "ap-southeast-6", "ap-southeast-7", "ca-central-1", "ca-west-1", @@ -5310,6 +5353,7 @@ "ap-southeast-3", "ap-southeast-4", "ap-southeast-5", + "ap-southeast-6", "ap-southeast-7", "ca-central-1", "ca-west-1", @@ -5360,6 +5404,7 @@ "ap-southeast-3", "ap-southeast-4", "ap-southeast-5", + "ap-southeast-6", "ap-southeast-7", "ca-central-1", "ca-west-1", @@ -5410,6 +5455,7 @@ "ap-southeast-3", "ap-southeast-4", "ap-southeast-5", + "ap-southeast-6", "ap-southeast-7", "ca-central-1", "ca-west-1", @@ -5460,6 +5506,7 @@ "ap-southeast-3", "ap-southeast-4", "ap-southeast-5", + "ap-southeast-6", "ap-southeast-7", "ca-central-1", "ca-west-1", @@ -6034,6 +6081,7 @@ "aws": [ "af-south-1", "ap-east-1", + "ap-east-2", "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", @@ -6081,6 +6129,7 @@ "aws": [ "af-south-1", "ap-east-1", + "ap-east-2", "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", @@ -7681,6 +7730,8 @@ "ap-south-1", "ap-southeast-1", "ap-southeast-2", + "ap-southeast-5", + "ap-southeast-7", "ca-central-1", "eu-central-1", "eu-north-1", @@ -8065,6 +8116,7 @@ "aws": [ "af-south-1", "ap-east-1", + "ap-east-2", "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", @@ -8075,8 +8127,10 @@ "ap-southeast-3", "ap-southeast-4", "ap-southeast-5", + "ap-southeast-6", "ap-southeast-7", "ca-central-1", + "ca-west-1", "eu-central-1", "eu-central-2", "eu-north-1", @@ -8088,6 +8142,7 @@ "il-central-1", "me-central-1", "me-south-1", + "mx-central-1", "sa-east-1", "us-east-1", "us-east-2", @@ -8279,22 +8334,31 @@ "aws": [ "af-south-1", "ap-east-1", + "ap-east-2", "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", "ap-south-1", + "ap-south-2", "ap-southeast-1", "ap-southeast-2", + "ap-southeast-3", + "ap-southeast-4", "ap-southeast-5", + "ap-southeast-6", "ap-southeast-7", "ca-central-1", + "ca-west-1", "eu-central-1", "eu-central-2", "eu-north-1", + "eu-south-1", "eu-south-2", "eu-west-1", "eu-west-2", "eu-west-3", + "il-central-1", + "mx-central-1", "sa-east-1", "us-east-1", "us-east-2", @@ -8315,6 +8379,7 @@ "ap-northeast-2", "ap-northeast-3", "ap-south-1", + "ap-south-2", "ap-southeast-1", "ap-southeast-2", "ap-southeast-3", @@ -8574,6 +8639,7 @@ "il-central-1", "me-central-1", "me-south-1", + "mx-central-1", "sa-east-1", "us-east-1", "us-east-2", @@ -8706,13 +8772,19 @@ "aws": [ "ap-northeast-1", "ap-northeast-2", + "ap-northeast-3", "ap-south-1", "ap-south-2", "ap-southeast-2", + "ap-southeast-4", "ca-central-1", "eu-central-1", + "eu-central-2", + "eu-south-1", + "eu-south-2", "eu-west-1", "eu-west-2", + "sa-east-1", "us-east-1", "us-east-2", "us-west-2" @@ -9034,6 +9106,7 @@ "eu-west-1", "eu-west-2", "eu-west-3", + "sa-east-1", "us-east-1", "us-east-2", "us-west-2" @@ -9136,11 +9209,14 @@ "regions": { "aws": [ "ap-northeast-1", + "ap-northeast-3", "ap-south-1", "ap-southeast-1", "ap-southeast-2", "eu-central-1", "eu-north-1", + "eu-south-1", + "eu-south-2", "eu-west-1", "eu-west-2", "eu-west-3", @@ -9377,6 +9453,7 @@ "ap-southeast-1", "ap-southeast-2", "ap-southeast-5", + "ap-southeast-7", "ca-central-1", "eu-central-1", "eu-central-2", @@ -9395,7 +9472,9 @@ "aws-cn": [ "cn-northwest-1" ], - "aws-eusc": [], + "aws-eusc": [ + "eusc-de-east-1" + ], "aws-us-gov": [ "us-gov-west-1" ] @@ -9865,10 +9944,12 @@ "ap-southeast-1", "ap-southeast-2", "ap-southeast-3", + "ap-southeast-4", "ap-southeast-5", "ap-southeast-6", "ap-southeast-7", "ca-central-1", + "ca-west-1", "eu-central-1", "eu-central-2", "eu-north-1", @@ -10012,7 +10093,10 @@ ], "aws-cn": [], "aws-eusc": [], - "aws-us-gov": [] + "aws-us-gov": [ + "us-gov-east-1", + "us-gov-west-1" + ] } }, "resource-groups": { @@ -10693,7 +10777,10 @@ "us-west-1", "us-west-2" ], - "aws-cn": [], + "aws-cn": [ + "cn-north-1", + "cn-northwest-1" + ], "aws-eusc": [], "aws-us-gov": [] } @@ -11315,7 +11402,9 @@ "ap-southeast-2", "ap-southeast-3", "ap-southeast-4", + "ap-southeast-6", "ca-central-1", + "ca-west-1", "eu-central-1", "eu-central-2", "eu-north-1", @@ -11647,26 +11736,6 @@ ] } }, - "simspaceweaver": { - "regions": { - "aws": [ - "ap-southeast-1", - "ap-southeast-2", - "eu-central-1", - "eu-north-1", - "eu-west-1", - "us-east-1", - "us-east-2", - "us-west-2" - ], - "aws-cn": [], - "aws-eusc": [], - "aws-us-gov": [ - "us-gov-east-1", - "us-gov-west-1" - ] - } - }, "sms": { "regions": { "aws": [ @@ -13063,6 +13132,7 @@ "eu-west-3", "me-central-1", "me-south-1", + "mx-central-1", "sa-east-1", "us-east-1", "us-east-2", @@ -13414,6 +13484,7 @@ "ap-south-1", "ap-southeast-1", "ap-southeast-2", + "ap-southeast-5", "ca-central-1", "eu-central-1", "eu-west-1", @@ -13422,6 +13493,7 @@ "il-central-1", "sa-east-1", "us-east-1", + "us-east-2", "us-west-2" ], "aws-cn": [ diff --git a/tests/providers/googleworkspace/services/gmail/gmail_comprehensive_mail_storage_enabled/__init__.py b/prowler/providers/aws/services/bedrock/bedrock_prompt_encrypted_with_cmk/__init__.py similarity index 100% rename from tests/providers/googleworkspace/services/gmail/gmail_comprehensive_mail_storage_enabled/__init__.py rename to prowler/providers/aws/services/bedrock/bedrock_prompt_encrypted_with_cmk/__init__.py diff --git a/prowler/providers/aws/services/bedrock/bedrock_prompt_encrypted_with_cmk/bedrock_prompt_encrypted_with_cmk.metadata.json b/prowler/providers/aws/services/bedrock/bedrock_prompt_encrypted_with_cmk/bedrock_prompt_encrypted_with_cmk.metadata.json new file mode 100644 index 0000000000..d01de76f2f --- /dev/null +++ b/prowler/providers/aws/services/bedrock/bedrock_prompt_encrypted_with_cmk/bedrock_prompt_encrypted_with_cmk.metadata.json @@ -0,0 +1,43 @@ +{ + "Provider": "aws", + "CheckID": "bedrock_prompt_encrypted_with_cmk", + "CheckTitle": "Amazon Bedrock prompt is encrypted at rest with a customer-managed KMS key", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "bedrock", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "Other", + "ResourceGroup": "ai_ml", + "Description": "Bedrock prompts should be encrypted at rest with a **customer-managed KMS key (CMK)** rather than the AWS-owned default key. Prompts can contain sensitive instructions, business logic, and references to downstream tooling that warrant tenant-controlled key material and auditable access via AWS KMS.", + "Risk": "A prompt encrypted only with the AWS-owned default key offers limited tenant control over key access and lifecycle: no customer KMS key policy to govern decrypt permissions, no control over rotation cadence or scheduled deletion, and gaps against frameworks (ISO 27001 A.8.24, NIST CSF PR.DS, KISA-ISMS-P 2.7.2) that require customer-managed keys for sensitive data at rest.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-management.html", + "https://docs.aws.amazon.com/bedrock/latest/APIReference/API_agent_CreatePrompt.html", + "https://docs.aws.amazon.com/bedrock/latest/APIReference/API_agent_UpdatePrompt.html" + ], + "Remediation": { + "Code": { + "CLI": "# Retrieve the current DRAFT prompt first and note the existing fields you want to preserve, such as description, defaultVariant, and variants:\naws bedrock-agent get-prompt --prompt-identifier --prompt-version DRAFT --output json\n# Then update the prompt and include the existing fields you want to keep alongside the CMK change:\naws bedrock-agent update-prompt --prompt-identifier --name --description --default-variant --variants --customer-encryption-key-arn ", + "NativeIaC": "", + "Other": "1. Open the Amazon Bedrock console\n2. Navigate to Prompt management\n3. Select the prompt\n4. Edit the prompt and choose a customer-managed KMS key for encryption\n5. Save the prompt", + "Terraform": "" + }, + "Recommendation": { + "Text": "Encrypt every Bedrock prompt with a **customer-managed KMS key** to retain control over key access, rotation, and lifecycle. When using `update-prompt`, first retrieve the current draft and carry forward the fields you want to preserve, such as the existing description, `defaultVariant`, and `variants`, so the encryption change does not unintentionally overwrite prompt configuration.", + "Url": "https://hub.prowler.com/check/bedrock_prompt_encrypted_with_cmk" + } + }, + "Categories": [ + "gen-ai", + "encryption" + ], + "DependsOn": [], + "RelatedTo": [ + "bedrock_prompt_management_exists" + ], + "Notes": "" +} diff --git a/prowler/providers/aws/services/bedrock/bedrock_prompt_encrypted_with_cmk/bedrock_prompt_encrypted_with_cmk.py b/prowler/providers/aws/services/bedrock/bedrock_prompt_encrypted_with_cmk/bedrock_prompt_encrypted_with_cmk.py new file mode 100644 index 0000000000..1e5bbb4e50 --- /dev/null +++ b/prowler/providers/aws/services/bedrock/bedrock_prompt_encrypted_with_cmk/bedrock_prompt_encrypted_with_cmk.py @@ -0,0 +1,32 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.bedrock.bedrock_agent_client import ( + bedrock_agent_client, +) + + +class bedrock_prompt_encrypted_with_cmk(Check): + """Ensure that Bedrock prompts are encrypted with a customer-managed KMS key. + + This check evaluates whether each Bedrock prompt is encrypted at rest using + a customer-managed KMS key (CMK) rather than the AWS-owned default key. + - PASS: The Bedrock prompt is encrypted with a customer-managed KMS key. + - FAIL: The Bedrock prompt is not encrypted with a customer-managed KMS key. + """ + + def execute(self) -> list[Check_Report_AWS]: + """Execute the Bedrock prompt CMK encryption check. + + Returns: + A list of reports containing the result of the check. + """ + findings = [] + for prompt in bedrock_agent_client.prompts.values(): + report = Check_Report_AWS(metadata=self.metadata(), resource=prompt) + if prompt.customer_encryption_key_arn: + report.status = "PASS" + report.status_extended = f"Bedrock Prompt {prompt.name} is encrypted with a customer-managed KMS key." + else: + report.status = "FAIL" + report.status_extended = f"Bedrock Prompt {prompt.name} is not encrypted with a customer-managed KMS key." + findings.append(report) + return findings diff --git a/prowler/providers/aws/services/bedrock/bedrock_prompt_management_exists/bedrock_prompt_management_exists.metadata.json b/prowler/providers/aws/services/bedrock/bedrock_prompt_management_exists/bedrock_prompt_management_exists.metadata.json index 195cefccd1..96e88bda3a 100644 --- a/prowler/providers/aws/services/bedrock/bedrock_prompt_management_exists/bedrock_prompt_management_exists.metadata.json +++ b/prowler/providers/aws/services/bedrock/bedrock_prompt_management_exists/bedrock_prompt_management_exists.metadata.json @@ -34,6 +34,8 @@ "gen-ai" ], "DependsOn": [], - "RelatedTo": [], + "RelatedTo": [ + "bedrock_prompt_encrypted_with_cmk" + ], "Notes": "Results are generated per scanned region. Regions where `ListPrompts` cannot be queried are omitted from the findings." } diff --git a/prowler/providers/aws/services/bedrock/bedrock_service.py b/prowler/providers/aws/services/bedrock/bedrock_service.py index e04319fc57..7222456341 100644 --- a/prowler/providers/aws/services/bedrock/bedrock_service.py +++ b/prowler/providers/aws/services/bedrock/bedrock_service.py @@ -136,7 +136,10 @@ class Guardrail(BaseModel): class BedrockAgent(AWSService): + """Bedrock Agent service class for managing agents and prompts.""" + def __init__(self, provider): + """Initialize the BedrockAgent service.""" # Call AWSService's __init__ super().__init__("bedrock-agent", provider) self.agents = {} @@ -144,6 +147,7 @@ class BedrockAgent(AWSService): self.prompt_scanned_regions: set = set() self.__threading_call__(self._list_agents) self.__threading_call__(self._list_prompts) + self.__threading_call__(self._get_prompt, self.prompts.values()) self.__threading_call__(self._list_tags_for_resource, self.agents.values()) def _list_agents(self, regional_client): @@ -171,29 +175,43 @@ class BedrockAgent(AWSService): ) def _list_prompts(self, regional_client): - """List all prompts in a region. - - Prompt Management is evaluated as a region-level adoption signal, so - prompt collection is intentionally not filtered by audit_resources. - """ + """List all prompts in a region.""" logger.info("Bedrock Agent - Listing Prompts...") try: paginator = regional_client.get_paginator("list_prompts") for page in paginator.paginate(): for prompt in page.get("promptSummaries", []): prompt_arn = prompt.get("arn", "") - self.prompts[prompt_arn] = Prompt( - id=prompt.get("id", ""), - name=prompt.get("name", ""), - arn=prompt_arn, - region=regional_client.region, - ) + if not self.audit_resources or ( + is_resource_filtered(prompt_arn, self.audit_resources) + ): + self.prompts[prompt_arn] = Prompt( + id=prompt.get("id", ""), + name=prompt.get("name", ""), + arn=prompt_arn, + region=regional_client.region, + ) self.prompt_scanned_regions.add(regional_client.region) except Exception as error: logger.error( f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) + def _get_prompt(self, prompt): + """Get detailed prompt information including encryption configuration.""" + logger.info("Bedrock Agent - Getting Prompt...") + try: + prompt_info = self.regional_clients[prompt.region].get_prompt( + promptIdentifier=prompt.id + ) + prompt.customer_encryption_key_arn = prompt_info.get( + "customerEncryptionKeyArn" + ) + except Exception as error: + logger.error( + f"{prompt.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + def _list_tags_for_resource(self, resource): """List tags for a Bedrock Agent resource.""" logger.info("Bedrock Agent - Listing Tags for Resource...") @@ -212,6 +230,8 @@ class BedrockAgent(AWSService): class Agent(BaseModel): + """Model for a Bedrock Agent resource.""" + id: str name: str arn: str @@ -227,3 +247,4 @@ class Prompt(BaseModel): name: str arn: str region: str + customer_encryption_key_arn: Optional[str] = None diff --git a/tests/providers/googleworkspace/services/gmail/gmail_enhanced_pre_delivery_scanning_enabled/__init__.py b/prowler/providers/aws/services/cloudtrail/cloudtrail_bedrock_logging_enabled/__init__.py similarity index 100% rename from tests/providers/googleworkspace/services/gmail/gmail_enhanced_pre_delivery_scanning_enabled/__init__.py rename to prowler/providers/aws/services/cloudtrail/cloudtrail_bedrock_logging_enabled/__init__.py diff --git a/prowler/providers/aws/services/cloudtrail/cloudtrail_bedrock_logging_enabled/cloudtrail_bedrock_logging_enabled.metadata.json b/prowler/providers/aws/services/cloudtrail/cloudtrail_bedrock_logging_enabled/cloudtrail_bedrock_logging_enabled.metadata.json new file mode 100644 index 0000000000..8f27089444 --- /dev/null +++ b/prowler/providers/aws/services/cloudtrail/cloudtrail_bedrock_logging_enabled/cloudtrail_bedrock_logging_enabled.metadata.json @@ -0,0 +1,44 @@ +{ + "Provider": "aws", + "CheckID": "cloudtrail_bedrock_logging_enabled", + "CheckTitle": "CloudTrail logs Amazon Bedrock API calls for security auditing", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "cloudtrail", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "AwsCloudTrailTrail", + "ResourceGroup": "monitoring", + "Description": "**At least one actively logging CloudTrail trail** records **Amazon Bedrock API activity** through management events or advanced event selectors targeting Bedrock resources.\n\nThis check covers **control-plane** operations such as configuration changes through CloudTrail management events and can also cover **data-plane** Bedrock events when advanced event selectors target Bedrock resource types.", + "Risk": "Without CloudTrail logging for Bedrock control-plane operations, changes to prompts, guardrails, agents, flows, or knowledge bases can become invisible, weakening forensics and incident response. Management events do not capture `InvokeModel`; pair this control with `bedrock_model_invocation_logging_enabled` or Bedrock data event selectors for invocation visibility.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/bedrock/latest/userguide/logging-using-cloudtrail.html", + "https://docs.aws.amazon.com/awscloudtrail/latest/userguide/logging-data-events-with-cloudtrail.html" + ], + "Remediation": { + "Code": { + "CLI": "aws cloudtrail put-event-selectors --trail-name --advanced-event-selectors '[{\"Name\":\"Bedrock data events\",\"FieldSelectors\":[{\"Field\":\"eventCategory\",\"Equals\":[\"Data\"]},{\"Field\":\"resources.type\",\"Equals\":[\"AWS::Bedrock::Model\",\"AWS::Bedrock::Guardrail\",\"AWS::Bedrock::AgentAlias\",\"AWS::Bedrock::FlowAlias\",\"AWS::Bedrock::InlineAgent\",\"AWS::Bedrock::KnowledgeBase\",\"AWS::Bedrock::Prompt\"]}]}]'", + "NativeIaC": "```yaml\n# CloudFormation: enable Bedrock data event logging on an actively logging trail\nResources:\n ExampleTrail:\n Type: AWS::CloudTrail::Trail\n Properties:\n TrailName: \n S3BucketName: \n IsLogging: true\n AdvancedEventSelectors:\n - Name: Bedrock data events\n FieldSelectors:\n - Field: eventCategory\n Equals:\n - Data\n - Field: resources.type # CRITICAL: target Bedrock resources\n Equals:\n - AWS::Bedrock::Model\n - AWS::Bedrock::Guardrail\n - AWS::Bedrock::AgentAlias\n - AWS::Bedrock::FlowAlias\n - AWS::Bedrock::InlineAgent\n - AWS::Bedrock::KnowledgeBase\n - AWS::Bedrock::Prompt\n```", + "Other": "1. In the AWS Console, open CloudTrail and select a trail that is actively logging\n2. Edit the trail and enable Management events to capture Bedrock control-plane operations, or add Bedrock advanced data event selectors for data-plane visibility\n3. If using data events, select the Bedrock resource types you want to log\n4. Save changes and confirm the trail remains in logging state", + "Terraform": "```hcl\n# Terraform: enable Bedrock data event logging on an actively logging trail\nresource \"aws_cloudtrail\" \"example_resource\" {\n name = \"example_resource\"\n s3_bucket_name = \"example_resource\"\n\n advanced_event_selector {\n name = \"Bedrock data events\"\n field_selector {\n field = \"eventCategory\"\n equals = [\"Data\"]\n }\n field_selector {\n field = \"resources.type\" # CRITICAL: target Bedrock resources\n equals = [\"AWS::Bedrock::Model\", \"AWS::Bedrock::Guardrail\", \"AWS::Bedrock::AgentAlias\", \"AWS::Bedrock::FlowAlias\", \"AWS::Bedrock::InlineAgent\", \"AWS::Bedrock::KnowledgeBase\", \"AWS::Bedrock::Prompt\"]\n }\n }\n}\n```" + }, + "Recommendation": { + "Text": "Enable CloudTrail logging for Amazon Bedrock on **at least one actively logging trail**. At minimum, enable **management events** to capture Bedrock control-plane operations. For invocation-level and other data-plane visibility, add **advanced event selectors** targeting Bedrock resource types or pair this control with `bedrock_model_invocation_logging_enabled`.\n\nFor broader region coverage, pair this control with a separate multi-region CloudTrail check. Centralize logs in an encrypted bucket or CloudWatch Logs to support **defense in depth** and forensic readiness for AI workloads.", + "Url": "https://hub.prowler.com/check/cloudtrail_bedrock_logging_enabled" + } + }, + "Categories": [ + "logging", + "forensics-ready", + "gen-ai" + ], + "DependsOn": [], + "RelatedTo": [ + "cloudtrail_multi_region_enabled_logging_management_events", + "bedrock_model_invocation_logging_enabled" + ], + "Notes": "This check passes when CloudTrail captures Bedrock control-plane activity via management events or Bedrock data events via advanced selectors. It does not require multi-region coverage, and it does not by itself guarantee `InvokeModel` visibility unless Bedrock data events are selected; use `bedrock_model_invocation_logging_enabled` for model invocation logs. Additional advanced selector filters such as `eventName` or `resources.ARN` can further narrow effective coverage and should be reviewed explicitly." +} diff --git a/prowler/providers/aws/services/cloudtrail/cloudtrail_bedrock_logging_enabled/cloudtrail_bedrock_logging_enabled.py b/prowler/providers/aws/services/cloudtrail/cloudtrail_bedrock_logging_enabled/cloudtrail_bedrock_logging_enabled.py new file mode 100644 index 0000000000..d66d10f7c6 --- /dev/null +++ b/prowler/providers/aws/services/cloudtrail/cloudtrail_bedrock_logging_enabled/cloudtrail_bedrock_logging_enabled.py @@ -0,0 +1,213 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.cloudtrail.cloudtrail_client import ( + cloudtrail_client, +) +from prowler.providers.aws.services.cloudtrail.cloudtrail_service import ( + Event_Selector, +) + + +class cloudtrail_bedrock_logging_enabled(Check): + """Ensure CloudTrail is configured to log Amazon Bedrock API calls. + + This check verifies whether at least one CloudTrail trail is configured to + capture Amazon Bedrock control-plane API calls through management events or + Bedrock data events through advanced event selectors. + + - PASS: A trail logs Bedrock control-plane API calls via management events + or Bedrock data events via Bedrock-specific advanced event selectors. + - FAIL: No CloudTrail trail is configured to log Bedrock API calls. + """ + + # Bedrock resource types supported by CloudTrail advanced event selectors. + BEDROCK_RESOURCE_TYPES = frozenset( + { + "AWS::Bedrock::AgentAlias", + "AWS::Bedrock::FlowAlias", + "AWS::Bedrock::Guardrail", + "AWS::Bedrock::InlineAgent", + "AWS::Bedrock::KnowledgeBase", + "AWS::Bedrock::Model", + "AWS::Bedrock::Prompt", + } + ) + # Bedrock control-plane event sources, including Bedrock Data Automation. + BEDROCK_EVENT_SOURCES = frozenset( + { + "bedrock.amazonaws.com", + "bedrock-agent.amazonaws.com", + "bedrock-runtime.amazonaws.com", + "bedrock-agent-runtime.amazonaws.com", + "bedrock-data-automation.amazonaws.com", + "bedrock-data-automation-runtime.amazonaws.com", + } + ) + + def execute(self) -> list[Check_Report_AWS]: + """Execute the check logic. + + Returns: + A list of reports containing the result of the check. + """ + findings = [] + if cloudtrail_client.trails is not None: + for trail in cloudtrail_client.trails.values(): + if trail.is_logging: + for data_event in trail.data_events: + match_type = self._get_bedrock_match_type(data_event) + if match_type: + report = Check_Report_AWS( + metadata=self.metadata(), resource=trail + ) + report.region = trail.home_region + report.status = "PASS" + if match_type == "classic_management": + report.status_extended = ( + f"Trail {trail.name} from home region " + f"{trail.home_region} has management events " + "enabled to log Amazon Bedrock control-plane " + "API calls." + ) + elif match_type == "advanced_management": + report.status_extended = ( + f"Trail {trail.name} from home region " + f"{trail.home_region} has an advanced " + "management event selector to log Amazon " + "Bedrock control-plane API calls." + ) + else: + report.status_extended = ( + f"Trail {trail.name} from home region " + f"{trail.home_region} has an advanced data " + "event selector to log Amazon Bedrock API " + "calls." + ) + findings.append(report) + break + if not findings: + report = Check_Report_AWS( + metadata=self.metadata(), resource=cloudtrail_client.trails + ) + report.region = cloudtrail_client.region + report.resource_arn = cloudtrail_client.trail_arn_template + report.resource_id = cloudtrail_client.audited_account + report.status = "FAIL" + report.status_extended = "No CloudTrail trails are configured to log Amazon Bedrock API calls." + findings.append(report) + return findings + + def _get_bedrock_match_type(self, data_event: Event_Selector) -> str | None: + """Return the Bedrock logging match type for an event selector. + + Args: + data_event: An Event_Selector object from the trail. + + Returns: + The matching selector type, or None if the selector does not log + the Bedrock events covered by this check. + """ + if not data_event.is_advanced: + if self._logs_classic_management_events(data_event.event_selector): + return "classic_management" + return None + + field_selectors = data_event.event_selector.get("FieldSelectors", []) + if self._logs_advanced_management_events(field_selectors): + return "advanced_management" + if self._logs_advanced_bedrock_data_events(field_selectors): + return "advanced_data" + + return None + + @staticmethod + def _logs_classic_management_events(event_selector: dict) -> bool: + """Check whether a classic selector logs Bedrock control-plane events.""" + return event_selector.get( + "IncludeManagementEvents", True + ) and event_selector.get("ReadWriteType", "All") in ("All", "WriteOnly") + + def _logs_advanced_management_events(self, field_selectors: list[dict]) -> bool: + """Check whether advanced selectors log Bedrock control-plane events.""" + event_category_selectors = [ + field for field in field_selectors if field.get("Field") == "eventCategory" + ] + if not self._selectors_match_value("Management", event_category_selectors): + return False + + read_only_selectors = [ + field for field in field_selectors if field.get("Field") == "readOnly" + ] + has_read_only_restriction = bool(read_only_selectors) and not any( + self._field_selector_matches_value("false", selector) + for selector in read_only_selectors + ) + + return not has_read_only_restriction and self._logs_bedrock_management_events( + field_selectors + ) + + def _logs_advanced_bedrock_data_events(self, field_selectors: list[dict]) -> bool: + """Check whether advanced selectors log Bedrock data events.""" + event_category_selectors = [ + field for field in field_selectors if field.get("Field") == "eventCategory" + ] + if not self._selectors_match_value("Data", event_category_selectors): + return False + + resource_type_selectors = [ + field for field in field_selectors if field.get("Field") == "resources.type" + ] + return any( + self._selectors_match_value(resource_type, resource_type_selectors) + for resource_type in self.BEDROCK_RESOURCE_TYPES + ) + + def _logs_bedrock_management_events(self, field_selectors: list[dict]) -> bool: + """Check whether advanced management selectors include Bedrock sources.""" + event_source_selectors = [ + field for field in field_selectors if field.get("Field") == "eventSource" + ] + if not event_source_selectors: + return True + + return any( + self._selectors_match_value(event_source, event_source_selectors) + for event_source in self.BEDROCK_EVENT_SOURCES + ) + + def _selectors_match_value(self, value: str, selectors: list[dict]) -> bool: + """Check whether a candidate value satisfies all selectors for a field.""" + return bool(selectors) and all( + self._field_selector_matches_value(value, selector) + for selector in selectors + ) + + @staticmethod + def _field_selector_matches_value(value: str, selector: dict) -> bool: + """Evaluate a CloudTrail advanced field selector against a candidate value.""" + conditions = [] + + if "Equals" in selector: + conditions.append(value in selector["Equals"]) + if "NotEquals" in selector: + conditions.append(value not in selector["NotEquals"]) + if "StartsWith" in selector: + conditions.append( + any(value.startswith(prefix) for prefix in selector["StartsWith"]) + ) + if "NotStartsWith" in selector: + conditions.append( + all( + not value.startswith(prefix) for prefix in selector["NotStartsWith"] + ) + ) + if "EndsWith" in selector: + conditions.append( + any(value.endswith(suffix) for suffix in selector["EndsWith"]) + ) + if "NotEndsWith" in selector: + conditions.append( + all(not value.endswith(suffix) for suffix in selector["NotEndsWith"]) + ) + + return all(conditions) if conditions else True diff --git a/prowler/providers/aws/services/ec2/ec2_instance_account_imdsv2_enabled/ec2_instance_account_imdsv2_enabled.metadata.json b/prowler/providers/aws/services/ec2/ec2_instance_account_imdsv2_enabled/ec2_instance_account_imdsv2_enabled.metadata.json index bc074fe730..7228d5dd59 100644 --- a/prowler/providers/aws/services/ec2/ec2_instance_account_imdsv2_enabled/ec2_instance_account_imdsv2_enabled.metadata.json +++ b/prowler/providers/aws/services/ec2/ec2_instance_account_imdsv2_enabled/ec2_instance_account_imdsv2_enabled.metadata.json @@ -34,7 +34,8 @@ } }, "Categories": [ - "secrets" + "secrets", + "ec2-imdsv1" ], "DependsOn": [], "RelatedTo": [], diff --git a/prowler/providers/aws/services/ec2/ec2_instance_imdsv2_enabled/ec2_instance_imdsv2_enabled.metadata.json b/prowler/providers/aws/services/ec2/ec2_instance_imdsv2_enabled/ec2_instance_imdsv2_enabled.metadata.json index 5637f40e23..77d24a4820 100644 --- a/prowler/providers/aws/services/ec2/ec2_instance_imdsv2_enabled/ec2_instance_imdsv2_enabled.metadata.json +++ b/prowler/providers/aws/services/ec2/ec2_instance_imdsv2_enabled/ec2_instance_imdsv2_enabled.metadata.json @@ -36,7 +36,8 @@ }, "Categories": [ "identity-access", - "secrets" + "secrets", + "ec2-imdsv1" ], "DependsOn": [], "RelatedTo": [], diff --git a/prowler/providers/aws/services/iam/iam_no_custom_policy_permissive_role_assumption/iam_no_custom_policy_permissive_role_assumption.py b/prowler/providers/aws/services/iam/iam_no_custom_policy_permissive_role_assumption/iam_no_custom_policy_permissive_role_assumption.py index 6e310d7ca9..fa0058f401 100644 --- a/prowler/providers/aws/services/iam/iam_no_custom_policy_permissive_role_assumption/iam_no_custom_policy_permissive_role_assumption.py +++ b/prowler/providers/aws/services/iam/iam_no_custom_policy_permissive_role_assumption/iam_no_custom_policy_permissive_role_assumption.py @@ -16,6 +16,8 @@ class iam_no_custom_policy_permissive_role_assumption(Check): for policy in iam_client.policies.values(): # Check only custom policies if policy.type == "Custom": + if not policy.attached and not iam_client.provider.scan_unused_services: + continue report = Check_Report_AWS(metadata=self.metadata(), resource=policy) report.region = iam_client.region report.status = "PASS" diff --git a/prowler/providers/aws/services/iam/iam_policy_allows_privilege_escalation/iam_policy_allows_privilege_escalation.py b/prowler/providers/aws/services/iam/iam_policy_allows_privilege_escalation/iam_policy_allows_privilege_escalation.py index c867292b22..7406ec8332 100644 --- a/prowler/providers/aws/services/iam/iam_policy_allows_privilege_escalation/iam_policy_allows_privilege_escalation.py +++ b/prowler/providers/aws/services/iam/iam_policy_allows_privilege_escalation/iam_policy_allows_privilege_escalation.py @@ -11,6 +11,8 @@ class iam_policy_allows_privilege_escalation(Check): for policy in iam_client.policies.values(): if policy.type == "Custom": + if not policy.attached and not iam_client.provider.scan_unused_services: + continue report = Check_Report_AWS(metadata=self.metadata(), resource=policy) report.region = iam_client.region report.status = "PASS" diff --git a/prowler/providers/aws/services/iam/iam_policy_no_full_access_to_cloudtrail/iam_policy_no_full_access_to_cloudtrail.py b/prowler/providers/aws/services/iam/iam_policy_no_full_access_to_cloudtrail/iam_policy_no_full_access_to_cloudtrail.py index 4887bbcf6b..844be7f87a 100644 --- a/prowler/providers/aws/services/iam/iam_policy_no_full_access_to_cloudtrail/iam_policy_no_full_access_to_cloudtrail.py +++ b/prowler/providers/aws/services/iam/iam_policy_no_full_access_to_cloudtrail/iam_policy_no_full_access_to_cloudtrail.py @@ -11,6 +11,8 @@ class iam_policy_no_full_access_to_cloudtrail(Check): for policy in iam_client.policies.values(): # Check only custom policies if policy.type == "Custom": + if not policy.attached and not iam_client.provider.scan_unused_services: + continue report = Check_Report_AWS(metadata=self.metadata(), resource=policy) report.region = iam_client.region report.status = "PASS" diff --git a/prowler/providers/aws/services/iam/iam_policy_no_full_access_to_kms/iam_policy_no_full_access_to_kms.py b/prowler/providers/aws/services/iam/iam_policy_no_full_access_to_kms/iam_policy_no_full_access_to_kms.py index adad5d0d1d..e4bf1151da 100644 --- a/prowler/providers/aws/services/iam/iam_policy_no_full_access_to_kms/iam_policy_no_full_access_to_kms.py +++ b/prowler/providers/aws/services/iam/iam_policy_no_full_access_to_kms/iam_policy_no_full_access_to_kms.py @@ -11,6 +11,8 @@ class iam_policy_no_full_access_to_kms(Check): for policy in iam_client.policies.values(): # Check only custom policies if policy.type == "Custom": + if not policy.attached and not iam_client.provider.scan_unused_services: + continue report = Check_Report_AWS(metadata=self.metadata(), resource=policy) report.region = iam_client.region report.status = "PASS" diff --git a/prowler/providers/aws/services/iam/iam_policy_no_wildcard_marketplace_subscribe/iam_policy_no_wildcard_marketplace_subscribe.py b/prowler/providers/aws/services/iam/iam_policy_no_wildcard_marketplace_subscribe/iam_policy_no_wildcard_marketplace_subscribe.py index fe0e740a5e..a1299f3d70 100644 --- a/prowler/providers/aws/services/iam/iam_policy_no_wildcard_marketplace_subscribe/iam_policy_no_wildcard_marketplace_subscribe.py +++ b/prowler/providers/aws/services/iam/iam_policy_no_wildcard_marketplace_subscribe/iam_policy_no_wildcard_marketplace_subscribe.py @@ -10,6 +10,8 @@ class iam_policy_no_wildcard_marketplace_subscribe(Check): findings = [] for policy in iam_client.policies.values(): if policy.type == "Custom": + if not policy.attached and not iam_client.provider.scan_unused_services: + continue report = Check_Report_AWS(metadata=self.metadata(), resource=policy) report.region = iam_client.region report.status = "PASS" diff --git a/prowler/providers/aws/services/iam/iam_role_access_not_stale_to_bedrock/iam_role_access_not_stale_to_bedrock.py b/prowler/providers/aws/services/iam/iam_role_access_not_stale_to_bedrock/iam_role_access_not_stale_to_bedrock.py index d0287ab970..b29b3de916 100644 --- a/prowler/providers/aws/services/iam/iam_role_access_not_stale_to_bedrock/iam_role_access_not_stale_to_bedrock.py +++ b/prowler/providers/aws/services/iam/iam_role_access_not_stale_to_bedrock/iam_role_access_not_stale_to_bedrock.py @@ -1,9 +1,10 @@ +from datetime import datetime, timezone +from typing import Optional + +from dateutil.parser import parse + from prowler.lib.check.models import Check, Check_Report_AWS from prowler.providers.aws.services.iam.iam_client import iam_client -from prowler.providers.aws.services.iam.lib.policy import ( - evaluate_bedrock_staleness, - find_bedrock_service, -) class iam_role_access_not_stale_to_bedrock(Check): @@ -33,33 +34,73 @@ class iam_role_access_not_stale_to_bedrock(Check): "max_unused_bedrock_access_days", 60 ) - for ( - role_data, - last_accessed_services, - ) in iam_client.role_last_accessed_services.items(): - role_name = role_data[0] - role_arn = role_data[1] + if iam_client.roles is None: + return findings - bedrock_service = find_bedrock_service(last_accessed_services) + for role in iam_client.roles: + last_accessed_services = iam_client.role_last_accessed_services.get( + (role.name, role.arn), [] + ) + bedrock_service = self._find_bedrock_service(last_accessed_services) if bedrock_service is None: continue - report = Check_Report_AWS( - metadata=self.metadata(), - resource={"name": role_name, "arn": role_arn}, - ) - report.resource_id = role_name - report.resource_arn = role_arn + report = Check_Report_AWS(metadata=self.metadata(), resource=role) report.region = iam_client.region - if iam_client.roles is not None: - for iam_role in iam_client.roles: - if iam_role.arn == role_arn: - report.resource_tags = iam_role.tags - break - evaluate_bedrock_staleness( - report, bedrock_service, max_unused_bedrock_days, role_name, "Role" + self._evaluate_bedrock_staleness( + report, + bedrock_service, + max_unused_bedrock_days, + role.name, + "Role", ) findings.append(report) return findings + + @staticmethod + def _find_bedrock_service( + last_accessed_services: list[dict], + ) -> Optional[dict]: + """Return the Bedrock entry from a service last accessed list.""" + for service in last_accessed_services: + if service.get("ServiceNamespace") == "bedrock": + return service + return None + + @staticmethod + def _evaluate_bedrock_staleness( + report: Check_Report_AWS, + bedrock_service: dict, + max_days: int, + identity_name: str, + identity_type: str, + ) -> None: + """Populate a check report based on Bedrock access recency.""" + last_authenticated = bedrock_service.get("LastAuthenticated") + if last_authenticated is None: + report.status = "FAIL" + report.status_extended = ( + f"IAM {identity_type} {identity_name} has Bedrock permissions " + f"but has never used them." + ) + return + + if isinstance(last_authenticated, str): + last_authenticated = parse(last_authenticated) + + days_since_access = (datetime.now(timezone.utc) - last_authenticated).days + + if days_since_access > max_days: + report.status = "FAIL" + report.status_extended = ( + f"IAM {identity_type} {identity_name} has not accessed Bedrock " + f"in {days_since_access} days (threshold: {max_days} days)." + ) + else: + report.status = "PASS" + report.status_extended = ( + f"IAM {identity_type} {identity_name} accessed Bedrock " + f"{days_since_access} days ago (threshold: {max_days} days)." + ) diff --git a/prowler/providers/aws/services/iam/iam_user_access_not_stale_to_bedrock/iam_user_access_not_stale_to_bedrock.py b/prowler/providers/aws/services/iam/iam_user_access_not_stale_to_bedrock/iam_user_access_not_stale_to_bedrock.py index 09fb215782..582d5ebb06 100644 --- a/prowler/providers/aws/services/iam/iam_user_access_not_stale_to_bedrock/iam_user_access_not_stale_to_bedrock.py +++ b/prowler/providers/aws/services/iam/iam_user_access_not_stale_to_bedrock/iam_user_access_not_stale_to_bedrock.py @@ -1,9 +1,10 @@ +from datetime import datetime, timezone +from typing import Optional + +from dateutil.parser import parse + from prowler.lib.check.models import Check, Check_Report_AWS from prowler.providers.aws.services.iam.iam_client import iam_client -from prowler.providers.aws.services.iam.lib.policy import ( - evaluate_bedrock_staleness, - find_bedrock_service, -) class iam_user_access_not_stale_to_bedrock(Check): @@ -33,32 +34,70 @@ class iam_user_access_not_stale_to_bedrock(Check): "max_unused_bedrock_access_days", 60 ) - for ( - user_data, - last_accessed_services, - ) in iam_client.last_accessed_services.items(): - user_name = user_data[0] - user_arn = user_data[1] - - bedrock_service = find_bedrock_service(last_accessed_services) + for user in iam_client.users: + last_accessed_services = iam_client.last_accessed_services.get( + (user.name, user.arn), [] + ) + bedrock_service = self._find_bedrock_service(last_accessed_services) if bedrock_service is None: continue - report = Check_Report_AWS( - metadata=self.metadata(), - resource={"name": user_name, "arn": user_arn}, - ) - report.resource_id = user_name - report.resource_arn = user_arn + report = Check_Report_AWS(metadata=self.metadata(), resource=user) report.region = iam_client.region - for iam_user in iam_client.users: - if iam_user.arn == user_arn: - report.resource_tags = iam_user.tags - break - evaluate_bedrock_staleness( - report, bedrock_service, max_unused_bedrock_days, user_name, "User" + self._evaluate_bedrock_staleness( + report, + bedrock_service, + max_unused_bedrock_days, + user.name, + "User", ) findings.append(report) return findings + + @staticmethod + def _find_bedrock_service( + last_accessed_services: list[dict], + ) -> Optional[dict]: + """Return the Bedrock entry from a service last accessed list.""" + for service in last_accessed_services: + if service.get("ServiceNamespace") == "bedrock": + return service + return None + + @staticmethod + def _evaluate_bedrock_staleness( + report: Check_Report_AWS, + bedrock_service: dict, + max_days: int, + identity_name: str, + identity_type: str, + ) -> None: + """Populate a check report based on Bedrock access recency.""" + last_authenticated = bedrock_service.get("LastAuthenticated") + if last_authenticated is None: + report.status = "FAIL" + report.status_extended = ( + f"IAM {identity_type} {identity_name} has Bedrock permissions " + f"but has never used them." + ) + return + + if isinstance(last_authenticated, str): + last_authenticated = parse(last_authenticated) + + days_since_access = (datetime.now(timezone.utc) - last_authenticated).days + + if days_since_access > max_days: + report.status = "FAIL" + report.status_extended = ( + f"IAM {identity_type} {identity_name} has not accessed Bedrock " + f"in {days_since_access} days (threshold: {max_days} days)." + ) + else: + report.status = "PASS" + report.status_extended = ( + f"IAM {identity_type} {identity_name} accessed Bedrock " + f"{days_since_access} days ago (threshold: {max_days} days)." + ) diff --git a/tests/providers/googleworkspace/services/gmail/gmail_external_image_scanning_enabled/__init__.py b/prowler/providers/aws/services/iam/iam_user_access_not_stale_to_sagemaker/__init__.py similarity index 100% rename from tests/providers/googleworkspace/services/gmail/gmail_external_image_scanning_enabled/__init__.py rename to prowler/providers/aws/services/iam/iam_user_access_not_stale_to_sagemaker/__init__.py diff --git a/prowler/providers/aws/services/iam/iam_user_access_not_stale_to_sagemaker/iam_user_access_not_stale_to_sagemaker.metadata.json b/prowler/providers/aws/services/iam/iam_user_access_not_stale_to_sagemaker/iam_user_access_not_stale_to_sagemaker.metadata.json new file mode 100644 index 0000000000..75dad3dd70 --- /dev/null +++ b/prowler/providers/aws/services/iam/iam_user_access_not_stale_to_sagemaker/iam_user_access_not_stale_to_sagemaker.metadata.json @@ -0,0 +1,42 @@ +{ + "Provider": "aws", + "CheckID": "iam_user_access_not_stale_to_sagemaker", + "CheckTitle": "Regular SageMaker access ensures IAM users retain only actively used permissions", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "iam", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "AwsIamUser", + "ResourceGroup": "IAM", + "Description": "IAM users granted **SageMaker** permissions are evaluated for recent service usage.\n\nUsers whose last SageMaker access exceeds the configured threshold (default **90 days**) or that have **never** accessed SageMaker are flagged, indicating stale permissions that should be reviewed.", + "Risk": "Stale SageMaker permissions widen the **blast radius** of a credential compromise.\n\nAn attacker who gains access to a user with unused SageMaker permissions can access ML training data, models, endpoints, and notebooks — all without triggering expected usage patterns. Removing or scoping down stale permissions enforces least privilege and limits blast radius.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_access-advisor.html", + "https://docs.aws.amazon.com/sagemaker/latest/dg/security-iam.html", + "https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#remove-credentials" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Open the IAM console and select the user\n2. Review the **Access Advisor** tab to confirm SageMaker has not been accessed recently\n3. Remove or detach any policies granting SageMaker permissions that are no longer needed\n4. If the user still requires SageMaker access, verify usage and reduce scope to least privilege", + "Terraform": "" + }, + "Recommendation": { + "Text": "Apply the **principle of least privilege** by regularly reviewing IAM Access Advisor data and revoking SageMaker permissions that are no longer actively used.\n\nEstablish a periodic access review process and automate alerts for stale permissions to maintain a minimal attack surface.", + "Url": "https://hub.prowler.com/check/iam_user_access_not_stale_to_sagemaker" + } + }, + "Categories": [ + "identity-access" + ], + "DependsOn": [], + "RelatedTo": [ + "iam_user_access_not_stale_to_bedrock" + ], + "Notes": "The staleness threshold is configurable via the `max_unused_sagemaker_access_days` audit config key (default: 90 days)." +} diff --git a/prowler/providers/aws/services/iam/iam_user_access_not_stale_to_sagemaker/iam_user_access_not_stale_to_sagemaker.py b/prowler/providers/aws/services/iam/iam_user_access_not_stale_to_sagemaker/iam_user_access_not_stale_to_sagemaker.py new file mode 100644 index 0000000000..089d08a317 --- /dev/null +++ b/prowler/providers/aws/services/iam/iam_user_access_not_stale_to_sagemaker/iam_user_access_not_stale_to_sagemaker.py @@ -0,0 +1,103 @@ +from datetime import datetime, timezone +from typing import Optional + +from dateutil.parser import parse + +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.iam.iam_client import iam_client + + +class iam_user_access_not_stale_to_sagemaker(Check): + """Detect IAM users with stale SageMaker permissions. + + This check evaluates whether IAM users with SageMaker service permissions + have actively used those permissions within the configured threshold + (default 90 days). + + - PASS: The user has accessed SageMaker within the allowed period. + - FAIL: The user has SageMaker permissions but has not used them within + the allowed period or has never used them. + """ + + def execute(self) -> list[Check_Report_AWS]: + """Execute the SageMaker access staleness check for IAM users. + + Iterates over IAM users, inspecting service last accessed data for + the ``sagemaker`` namespace. Users whose last SageMaker access exceeds + the configured threshold are reported as non-compliant. + + Returns: + A list of reports containing the result of the check. + """ + findings = [] + max_unused_sagemaker_days = iam_client.audit_config.get( + "max_unused_sagemaker_access_days", 90 + ) + + for user in iam_client.users: + last_accessed_services = iam_client.last_accessed_services.get( + (user.name, user.arn), [] + ) + sagemaker_service = self._find_sagemaker_service(last_accessed_services) + if sagemaker_service is None: + continue + + report = Check_Report_AWS(metadata=self.metadata(), resource=user) + report.region = iam_client.region + + self._evaluate_sagemaker_staleness( + report, + sagemaker_service, + max_unused_sagemaker_days, + user.name, + "User", + ) + findings.append(report) + + return findings + + @staticmethod + def _find_sagemaker_service( + last_accessed_services: list[dict], + ) -> Optional[dict]: + """Return the SageMaker entry from a service last accessed list.""" + for service in last_accessed_services: + if service.get("ServiceNamespace") == "sagemaker": + return service + return None + + @staticmethod + def _evaluate_sagemaker_staleness( + report: Check_Report_AWS, + sagemaker_service: dict, + max_days: int, + identity_name: str, + identity_type: str, + ) -> None: + """Populate a check report based on SageMaker access recency.""" + last_authenticated = sagemaker_service.get("LastAuthenticated") + if last_authenticated is None: + report.status = "FAIL" + report.status_extended = ( + f"IAM {identity_type} {identity_name} has SageMaker permissions " + f"but has never used them." + ) + return + + if isinstance(last_authenticated, str): + last_authenticated = parse(last_authenticated) + + days_since_access = (datetime.now(timezone.utc) - last_authenticated).days + + if days_since_access > max_days: + report.status = "FAIL" + report.status_extended = ( + f"IAM {identity_type} {identity_name} has not accessed SageMaker " + f"in {days_since_access} days (threshold: {max_days} days)." + ) + else: + report.status = "PASS" + report.status_extended = ( + f"IAM {identity_type} {identity_name} accessed SageMaker " + f"{days_since_access} days ago (threshold: {max_days} days)." + ) diff --git a/prowler/providers/aws/services/iam/lib/policy.py b/prowler/providers/aws/services/iam/lib/policy.py index 562e270df6..333fbad993 100644 --- a/prowler/providers/aws/services/iam/lib/policy.py +++ b/prowler/providers/aws/services/iam/lib/policy.py @@ -1,12 +1,9 @@ import re -from datetime import datetime, timezone from ipaddress import ip_address, ip_network from typing import Optional, Tuple -from dateutil.parser import parse from py_iam_expand.actions import InvalidActionHandling, expand_actions -from prowler.lib.check.models import Check_Report_AWS from prowler.lib.logger import logger from prowler.providers.aws.aws_provider import read_aws_regions_file @@ -1121,47 +1118,3 @@ def has_codebuild_trusted_principal(trust_policy: dict) -> bool: ) for s in statements ) - - -def find_bedrock_service(last_accessed_services: list[dict]) -> Optional[dict]: - """Return the Bedrock entry from a service last accessed list.""" - for service in last_accessed_services: - if service.get("ServiceNamespace") == "bedrock": - return service - return None - - -def evaluate_bedrock_staleness( - report: Check_Report_AWS, - bedrock_service: dict, - max_days: int, - identity_name: str, - identity_type: str, -) -> None: - """Populate a check report based on Bedrock access recency.""" - last_authenticated = bedrock_service.get("LastAuthenticated") - if last_authenticated is None: - report.status = "FAIL" - report.status_extended = ( - f"IAM {identity_type} {identity_name} has Bedrock permissions " - f"but has never used them." - ) - return - - if isinstance(last_authenticated, str): - last_authenticated = parse(last_authenticated) - - days_since_access = (datetime.now(timezone.utc) - last_authenticated).days - - if days_since_access > max_days: - report.status = "FAIL" - report.status_extended = ( - f"IAM {identity_type} {identity_name} has not accessed Bedrock " - f"in {days_since_access} days (threshold: {max_days} days)." - ) - else: - report.status = "PASS" - report.status_extended = ( - f"IAM {identity_type} {identity_name} accessed Bedrock " - f"{days_since_access} days ago (threshold: {max_days} days)." - ) diff --git a/prowler/providers/aws/services/s3/s3_bucket_shadow_resource_vulnerability/s3_bucket_shadow_resource_vulnerability.py b/prowler/providers/aws/services/s3/s3_bucket_shadow_resource_vulnerability/s3_bucket_shadow_resource_vulnerability.py index eb509b1c85..ce14eca587 100644 --- a/prowler/providers/aws/services/s3/s3_bucket_shadow_resource_vulnerability/s3_bucket_shadow_resource_vulnerability.py +++ b/prowler/providers/aws/services/s3/s3_bucket_shadow_resource_vulnerability/s3_bucket_shadow_resource_vulnerability.py @@ -24,30 +24,30 @@ class s3_bucket_shadow_resource_vulnerability(Check): # First, check buckets in the current account for bucket in s3_client.buckets.values(): - report = Check_Report_AWS(self.metadata(), resource=bucket) - report.region = bucket.region - report.resource_id = bucket.name - report.resource_arn = bucket.arn - report.resource_tags = bucket.tags - report.status = "PASS" - report.status_extended = ( - f"S3 bucket {bucket.name} is not a known shadow resource." - ) - - # Check if this bucket matches any predictable pattern + # Only emit a finding when the bucket name actually matches one of + # the predictable service patterns. A bucket whose name does not + # match any pattern is, by definition, not a shadow resource, so a + # PASS finding for it would be tautological and add no signal. for service, pattern_format in predictable_patterns.items(): pattern = pattern_format.replace("", bucket.region) if re.match(pattern, bucket.name): + report = Check_Report_AWS(self.metadata(), resource=bucket) + report.region = bucket.region + report.resource_id = bucket.name + report.resource_arn = bucket.arn + report.resource_tags = bucket.tags + if bucket.owner_id != s3_client.audited_canonical_id: report.status = "FAIL" report.status_extended = f"S3 bucket {bucket.name} for service {service} is a known shadow resource and it is owned by another account ({bucket.owner_id})." else: report.status = "PASS" report.status_extended = f"S3 bucket {bucket.name} for service {service} is a known shadow resource but it is correctly owned by the audited account." + + findings.append(report) + reported_buckets.add(bucket.name) break - findings.append(report) - reported_buckets.add(bucket.name) # Now check for shadow resources in other accounts by testing predictable patterns # We'll test different regions to see if shadow resources exist diff --git a/tests/providers/googleworkspace/services/gmail/gmail_mail_delegation_disabled/__init__.py b/prowler/providers/aws/services/sagemaker/sagemaker_domain_sso_configured/__init__.py similarity index 100% rename from tests/providers/googleworkspace/services/gmail/gmail_mail_delegation_disabled/__init__.py rename to prowler/providers/aws/services/sagemaker/sagemaker_domain_sso_configured/__init__.py diff --git a/prowler/providers/aws/services/sagemaker/sagemaker_domain_sso_configured/sagemaker_domain_sso_configured.metadata.json b/prowler/providers/aws/services/sagemaker/sagemaker_domain_sso_configured/sagemaker_domain_sso_configured.metadata.json new file mode 100644 index 0000000000..2c9d52dc11 --- /dev/null +++ b/prowler/providers/aws/services/sagemaker/sagemaker_domain_sso_configured/sagemaker_domain_sso_configured.metadata.json @@ -0,0 +1,39 @@ +{ + "Provider": "aws", + "CheckID": "sagemaker_domain_sso_configured", + "CheckTitle": "SageMaker domains use SSO authentication instead of IAM mode", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "sagemaker", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "Other", + "ResourceGroup": "ai_ml", + "Description": "**SageMaker Domain** configured with **IAM Identity Center (SSO) authentication**. The check validates that each SageMaker Domain uses SSO mode (`AuthMode: SSO`) and is associated with an IAM Identity Center instance (`SingleSignOnManagedApplicationInstanceId` or `SingleSignOnApplicationArn` present), ensuring user access is centrally managed through AWS IAM Identity Center.", + "Risk": "IAM-mode domains create per-user IAM users or roles managed locally to SageMaker, drifting from the organization's identity provider and weakening lifecycle controls such as offboarding, MFA enforcement, and session policies. SSO-mode domains without an IAM Identity Center association leave authentication in an inconsistent state and bypass centralized access governance.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/sagemaker/latest/dg/onboard-sso-users.html" + ], + "Remediation": { + "Code": { + "CLI": "aws sagemaker describe-domain --domain-id --query '{AuthMode:AuthMode,SingleSignOnManagedApplicationInstanceId:SingleSignOnManagedApplicationInstanceId,SingleSignOnApplicationArn:SingleSignOnApplicationArn}'", + "NativeIaC": "```yaml\n# CloudFormation: Create a SageMaker Domain with SSO authentication\nResources:\n SageMakerDomain:\n Type: AWS::SageMaker::Domain\n Properties:\n DomainName: \n AuthMode: SSO # Critical: enables IAM Identity Center authentication\n DefaultUserSettings:\n ExecutionRole: \n VpcId: \n SubnetIds:\n - \n```", + "Other": "SageMaker Domains cannot be switched from IAM to SSO mode after creation. To remediate, create a new Domain with AuthMode set to SSO and migrate user profiles.", + "Terraform": "```hcl\n# Terraform: Create a SageMaker Domain with SSO authentication\nresource \"aws_sagemaker_domain\" \"example\" {\n domain_name = \"\"\n auth_mode = \"SSO\" # Critical: enables IAM Identity Center authentication\n vpc_id = \"\"\n subnet_ids = [\"\"]\n\n default_user_settings {\n execution_role = \"\"\n }\n}\n```" + }, + "Recommendation": { + "Text": "Configure SageMaker Domains with SSO authentication mode to anchor user access to AWS IAM Identity Center. This enforces centralized identity lifecycle management, MFA policies, and session controls. Domains created with IAM mode must be recreated with SSO mode since the auth mode cannot be changed after creation.", + "Url": "https://hub.prowler.com/check/sagemaker_domain_sso_configured" + } + }, + "Categories": [ + "identity-access", + "gen-ai" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/sagemaker/sagemaker_domain_sso_configured/sagemaker_domain_sso_configured.py b/prowler/providers/aws/services/sagemaker/sagemaker_domain_sso_configured/sagemaker_domain_sso_configured.py new file mode 100644 index 0000000000..54672cfdb7 --- /dev/null +++ b/prowler/providers/aws/services/sagemaker/sagemaker_domain_sso_configured/sagemaker_domain_sso_configured.py @@ -0,0 +1,27 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.sagemaker.sagemaker_client import sagemaker_client + + +class sagemaker_domain_sso_configured(Check): + def execute(self): + findings = [] + for domain in sagemaker_client.sagemaker_domains: + report = Check_Report_AWS(metadata=self.metadata(), resource=domain) + if domain.auth_mode == "SSO": + if ( + domain.single_sign_on_managed_application_instance_id + or domain.single_sign_on_application_arn + ): + report.status = "PASS" + report.status_extended = f"SageMaker domain {domain.name} is configured with SSO authentication and is associated with an IAM Identity Center instance." + else: + report.status = "FAIL" + report.status_extended = f"SageMaker domain {domain.name} is configured with SSO authentication but is not associated with an IAM Identity Center instance." + else: + report.status = "FAIL" + current_mode = domain.auth_mode if domain.auth_mode else "unknown" + report.status_extended = f"SageMaker domain {domain.name} is not configured with SSO authentication, current mode is {current_mode}." + + findings.append(report) + + return findings diff --git a/tests/providers/googleworkspace/services/gmail/gmail_per_user_outbound_gateway_disabled/__init__.py b/prowler/providers/aws/services/sagemaker/sagemaker_models_registry_in_use/__init__.py similarity index 100% rename from tests/providers/googleworkspace/services/gmail/gmail_per_user_outbound_gateway_disabled/__init__.py rename to prowler/providers/aws/services/sagemaker/sagemaker_models_registry_in_use/__init__.py diff --git a/prowler/providers/aws/services/sagemaker/sagemaker_models_registry_in_use/sagemaker_models_registry_in_use.metadata.json b/prowler/providers/aws/services/sagemaker/sagemaker_models_registry_in_use/sagemaker_models_registry_in_use.metadata.json new file mode 100644 index 0000000000..fe9bd5db95 --- /dev/null +++ b/prowler/providers/aws/services/sagemaker/sagemaker_models_registry_in_use/sagemaker_models_registry_in_use.metadata.json @@ -0,0 +1,42 @@ +{ + "Provider": "aws", + "CheckID": "sagemaker_models_registry_in_use", + "CheckTitle": "Amazon SageMaker Model Registry should have at least one approved model package", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "sagemaker", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "low", + "ResourceType": "Other", + "ResourceGroup": "ai_ml", + "Description": "**SageMaker Model Registry** is evaluated to verify that at least one Model Package Group exists and contains at least one model package with **ModelApprovalStatus = Approved**. This confirms that the ML governance workflow (register → review → approve → deploy) is actively in use.", + "Risk": "An empty Model Registry, or one with no approved packages, indicates that models are being deployed outside any review process. This breaks provenance and accountability for production ML workloads, making it impossible to enforce governance controls such as auditing, versioning, and approval workflows.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/sagemaker/latest/dg/model-registry.html", + "https://docs.aws.amazon.com/sagemaker/latest/dg/model-registry-approve.html", + "https://docs.aws.amazon.com/sagemaker/latest/APIReference/API_ListModelPackageGroups.html", + "https://docs.aws.amazon.com/sagemaker/latest/APIReference/API_ListModelPackages.html" + ], + "Remediation": { + "Code": { + "CLI": "aws sagemaker list-model-package-groups\naws sagemaker list-model-packages --model-package-group-name \naws sagemaker update-model-package --model-package-arn --model-approval-status Approved", + "NativeIaC": "", + "Other": "1. In the AWS console, navigate to SageMaker > Models > Model Registry.\n2. Create a Model Package Group if none exists.\n3. Register a model version in the group.\n4. Review and approve at least one model package by setting its approval status to Approved.", + "Terraform": "" + }, + "Recommendation": { + "Text": "Register all production models in the **SageMaker Model Registry** and enforce an approval workflow before deployment. Ensure at least one model package per group reaches **Approved** status. Use **IAM policies** to restrict who can approve model packages and integrate with **CI/CD pipelines** to automate registration.", + "Url": "https://hub.prowler.com/check/sagemaker_models_registry_in_use" + } + }, + "Categories": [ + "gen-ai", + "software-supply-chain" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/sagemaker/sagemaker_models_registry_in_use/sagemaker_models_registry_in_use.py b/prowler/providers/aws/services/sagemaker/sagemaker_models_registry_in_use/sagemaker_models_registry_in_use.py new file mode 100644 index 0000000000..5c7ff31fa5 --- /dev/null +++ b/prowler/providers/aws/services/sagemaker/sagemaker_models_registry_in_use/sagemaker_models_registry_in_use.py @@ -0,0 +1,28 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.sagemaker.sagemaker_client import sagemaker_client + + +class sagemaker_models_registry_in_use(Check): + """Ensure that SageMaker Model Registry has at least one approved model package.""" + + def execute(self) -> list[Check_Report_AWS]: + """Execute the check logic. + + Returns: + A list of reports indicating whether the SageMaker Model Registry + in each region contains at least one approved model package. + """ + findings = [] + for registry in sagemaker_client.sagemaker_model_registries: + report = Check_Report_AWS(metadata=self.metadata(), resource=registry) + if not registry.has_groups: + report.status = "FAIL" + report.status_extended = f"SageMaker Model Registry in region {registry.region} has no Model Package Groups." + elif registry.has_approved_packages: + report.status = "PASS" + report.status_extended = f"SageMaker Model Registry in region {registry.region} has at least one approved model package." + else: + report.status = "FAIL" + report.status_extended = f"SageMaker Model Registry in region {registry.region} has Model Package Groups but no approved model packages." + findings.append(report) + return findings diff --git a/prowler/providers/aws/services/sagemaker/sagemaker_service.py b/prowler/providers/aws/services/sagemaker/sagemaker_service.py index c44307a70d..0f73062452 100644 --- a/prowler/providers/aws/services/sagemaker/sagemaker_service.py +++ b/prowler/providers/aws/services/sagemaker/sagemaker_service.py @@ -15,13 +15,17 @@ class SageMaker(AWSService): self.sagemaker_notebook_instances = [] self.sagemaker_models = [] self.sagemaker_training_jobs = [] + self.sagemaker_domains = [] self.endpoint_configs = {} + self.sagemaker_model_registries = [] # Retrieve resources concurrently self.__threading_call__(self._list_notebook_instances) self.__threading_call__(self._list_models) self.__threading_call__(self._list_training_jobs) self.__threading_call__(self._list_endpoint_configs) + self.__threading_call__(self._list_domains) + self.__threading_call__(self._list_model_package_groups) # Describe resources concurrently self.__threading_call__(self._describe_model, self.sagemaker_models) @@ -34,6 +38,7 @@ class SageMaker(AWSService): self.__threading_call__( self._describe_endpoint_config, list(self.endpoint_configs.values()) ) + self.__threading_call__(self._describe_domain, self.sagemaker_domains) # List tags concurrently for each resource collection # This replaces the previous sequential sequential execution to improve performance @@ -47,6 +52,7 @@ class SageMaker(AWSService): self.__threading_call__( self._list_tags_for_resource, list(self.endpoint_configs.values()) ) + self.__threading_call__(self._list_tags_for_resource, self.sagemaker_domains) def _list_notebook_instances(self, regional_client): logger.info("SageMaker - listing notebook instances...") @@ -203,6 +209,71 @@ class SageMaker(AWSService): f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) + def _list_model_package_groups(self, regional_client): + logger.info("SageMaker - listing model package groups...") + registry_arn = self.get_unknown_arn( + region=regional_client.region, + resource_type="model-registry", + ) + has_groups = False + has_approved = False + try: + paginator = regional_client.get_paginator("list_model_package_groups") + for page in paginator.paginate(): + for group in page["ModelPackageGroupSummaryList"]: + has_groups = True + if not has_approved: + group_name = group["ModelPackageGroupName"] + try: + pkg_paginator = regional_client.get_paginator( + "list_model_packages" + ) + for pkg_page in pkg_paginator.paginate( + ModelPackageGroupName=group_name, + ModelApprovalStatus="Approved", + ): + if pkg_page["ModelPackageSummaryList"]: + has_approved = True + break + except ClientError as pkg_error: + if pkg_error.response["Error"]["Code"] in ( + "AccessDeniedException", + "UnrecognizedClientException", + ): + raise + logger.error( + f"{regional_client.region} -- {pkg_error.__class__.__name__}[{pkg_error.__traceback__.tb_lineno}]: {pkg_error}" + ) + except Exception as pkg_error: + logger.error( + f"{regional_client.region} -- {pkg_error.__class__.__name__}[{pkg_error.__traceback__.tb_lineno}]: {pkg_error}" + ) + except ClientError as error: + if error.response["Error"]["Code"] in ( + "AccessDeniedException", + "UnrecognizedClientException", + ): + logger.warning( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return + logger.error( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + except Exception as error: + logger.error( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + self.sagemaker_model_registries.append( + ModelRegistry( + name="SageMaker Model Registry", + arn=registry_arn, + region=regional_client.region, + has_groups=has_groups, + has_approved_packages=has_approved, + ) + ) + def _list_tags_for_resource(self, resource): """ Lists tags for a specific SageMaker resource. @@ -218,6 +289,46 @@ class SageMaker(AWSService): f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) + def _list_domains(self, regional_client): + logger.info("SageMaker - listing domains...") + try: + list_domains_paginator = regional_client.get_paginator("list_domains") + for page in list_domains_paginator.paginate(): + for domain in page["Domains"]: + if not self.audit_resources or ( + is_resource_filtered(domain["DomainArn"], self.audit_resources) + ): + self.sagemaker_domains.append( + Domain( + domain_id=domain["DomainId"], + name=domain["DomainName"], + region=regional_client.region, + arn=domain["DomainArn"], + ) + ) + except Exception as error: + logger.error( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + def _describe_domain(self, domain): + logger.info("SageMaker - describing domain...") + try: + regional_client = self.regional_clients[domain.region] + describe_domain = regional_client.describe_domain(DomainId=domain.domain_id) + if "AuthMode" in describe_domain: + domain.auth_mode = describe_domain["AuthMode"] + domain.single_sign_on_managed_application_instance_id = describe_domain.get( + "SingleSignOnManagedApplicationInstanceId" + ) + domain.single_sign_on_application_arn = describe_domain.get( + "SingleSignOnApplicationArn" + ) + except Exception as error: + logger.error( + f"{domain.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + def _list_endpoint_configs(self, regional_client): logger.info("SageMaker - listing endpoint configs...") try: @@ -303,9 +414,30 @@ class ProductionVariant(BaseModel): initial_instance_count: int +class Domain(BaseModel): + domain_id: str + name: str + region: str + arn: str + auth_mode: Optional[str] = None + single_sign_on_managed_application_instance_id: Optional[str] = None + single_sign_on_application_arn: Optional[str] = None + tags: Optional[list] = [] + + class EndpointConfig(BaseModel): name: str region: str arn: str production_variants: list[ProductionVariant] = [] tags: Optional[list] = [] + + +class ModelRegistry(BaseModel): + """Represents the SageMaker Model Registry state for a specific region.""" + + name: str + arn: str + region: str + has_groups: bool = False + has_approved_packages: bool = False diff --git a/tests/providers/googleworkspace/services/gmail/gmail_pop_imap_access_disabled/__init__.py b/prowler/providers/aws/services/ses/ses_identity_dkim_enabled/__init__.py similarity index 100% rename from tests/providers/googleworkspace/services/gmail/gmail_pop_imap_access_disabled/__init__.py rename to prowler/providers/aws/services/ses/ses_identity_dkim_enabled/__init__.py diff --git a/prowler/providers/aws/services/ses/ses_identity_dkim_enabled/ses_identity_dkim_enabled.metadata.json b/prowler/providers/aws/services/ses/ses_identity_dkim_enabled/ses_identity_dkim_enabled.metadata.json new file mode 100644 index 0000000000..83353b0caa --- /dev/null +++ b/prowler/providers/aws/services/ses/ses_identity_dkim_enabled/ses_identity_dkim_enabled.metadata.json @@ -0,0 +1,40 @@ +{ + "Provider": "aws", + "CheckID": "ses_identity_dkim_enabled", + "CheckTitle": "SES identity has DKIM signing enabled", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "ses", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "Other", + "ResourceGroup": "messaging", + "Description": "**Amazon SES identities** are evaluated for **DKIM (DomainKeys Identified Mail)** signing enabled and verified. DKIM adds a cryptographic signature to outgoing emails, allowing recipients to verify that the email was sent by the domain owner and was not altered in transit.", + "Risk": "Without DKIM signing, emails sent from SES identities are vulnerable to **spoofing and tampering**. Attackers can forge emails that appear to come from your domain, leading to phishing attacks, brand impersonation, and loss of email deliverability. Email providers are more likely to reject or mark unsigned emails as spam, impacting business communication and reputation.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/ses/latest/dg/send-email-authentication-dkim.html", + "https://docs.aws.amazon.com/ses/latest/dg/send-email-authentication.html" + ], + "Remediation": { + "Code": { + "CLI": "aws sesv2 put-email-identity-dkim-signing-attributes --email-identity --signing-attributes-origin AWS_SES", + "NativeIaC": "", + "Other": "1. In the AWS Console, go to Simple Email Service (SES)\n2. Open Verified identities and select the affected identity\n3. Click the Authentication tab\n4. Under DKIM, click Edit\n5. Enable DKIM signatures and select 'Provide DKIM authentication token (Easy DKIM)'\n6. Save changes and add the provided CNAME records to your DNS provider", + "Terraform": "```hcl\nresource \"aws_ses_domain_dkim\" \"\" {\n domain = \"\"\n}\n\n# Add the CNAME records to Route53 (or your DNS provider)\nresource \"aws_route53_record\" \"_dkim\" {\n count = 3\n zone_id = \"\"\n name = \"${aws_ses_domain_dkim..dkim_tokens[count.index]}._domainkey.\"\n type = \"CNAME\"\n ttl = 600\n records = [\"${aws_ses_domain_dkim..dkim_tokens[count.index]}.dkim.amazonses.com\"]\n}\n```" + }, + "Recommendation": { + "Text": "Enable **DKIM signing** for all SES identities and ensure the DKIM status is **SUCCESS**. Add the required CNAME records to your DNS provider to complete verification. Combine DKIM with **SPF** and **DMARC** for comprehensive email authentication following **defense in depth** principles. Monitor DKIM status regularly and rotate DKIM keys as recommended by AWS.", + "Url": "https://hub.prowler.com/check/ses_identity_dkim_enabled" + } + }, + "Categories": [ + "identity-access", + "email-security" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/ses/ses_identity_dkim_enabled/ses_identity_dkim_enabled.py b/prowler/providers/aws/services/ses/ses_identity_dkim_enabled/ses_identity_dkim_enabled.py new file mode 100644 index 0000000000..2654010e28 --- /dev/null +++ b/prowler/providers/aws/services/ses/ses_identity_dkim_enabled/ses_identity_dkim_enabled.py @@ -0,0 +1,33 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.ses.ses_client import ses_client + + +class ses_identity_dkim_enabled(Check): + def execute(self): + findings = [] + for identity in ses_client.email_identities.values(): + report = Check_Report_AWS(metadata=self.metadata(), resource=identity) + if identity.dkim_status == "SUCCESS" and identity.dkim_signing_enabled: + report.status = "PASS" + report.status_extended = f"SES identity {identity.name} has DKIM signing enabled and verified." + elif identity.dkim_status in ( + "PENDING", + "NOT_STARTED", + "TEMPORARY_FAILURE", + ): + report.status = "FAIL" + report.status_extended = f"SES identity {identity.name} has DKIM signing not verified (status: {identity.dkim_status})." + elif identity.dkim_status == "FAILED": + report.status = "FAIL" + report.status_extended = f"SES identity {identity.name} has DKIM signing failed verification." + elif ( + identity.dkim_status == "SUCCESS" and not identity.dkim_signing_enabled + ): + report.status = "FAIL" + report.status_extended = f"SES identity {identity.name} has DKIM verified but signing is disabled." + else: + report.status = "FAIL" + report.status_extended = f"SES identity {identity.name} does not have DKIM signing configured." + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/ses/ses_service.py b/prowler/providers/aws/services/ses/ses_service.py index ff19f829e6..7d0feb8f54 100644 --- a/prowler/providers/aws/services/ses/ses_service.py +++ b/prowler/providers/aws/services/ses/ses_service.py @@ -46,9 +46,15 @@ class SES(AWSService): identity_attributes = regional_client.get_email_identity( EmailIdentity=identity.name ) - for _, content in identity_attributes["Policies"].items(): + for _, content in identity_attributes.get("Policies", {}).items(): identity.policy = loads(content) - identity.tags = identity_attributes["Tags"] + identity.tags = identity_attributes.get("Tags", []) + dkim_attrs = identity_attributes.get("DkimAttributes", {}) or {} + identity.dkim_status = dkim_attrs.get("Status") + identity.dkim_signing_enabled = dkim_attrs.get("SigningEnabled", False) + identity.dkim_signing_attributes_origin = dkim_attrs.get( + "SigningAttributesOrigin" + ) except Exception as error: logger.error( @@ -67,3 +73,6 @@ class Identity(BaseModel): type: Optional[str] policy: Optional[dict] = None tags: Optional[list] = [] + dkim_status: Optional[str] = None + dkim_signing_attributes_origin: Optional[str] = None + dkim_signing_enabled: Optional[bool] = False diff --git a/prowler/providers/azure/azure_provider.py b/prowler/providers/azure/azure_provider.py index 506b433690..be8b7e60a3 100644 --- a/prowler/providers/azure/azure_provider.py +++ b/prowler/providers/azure/azure_provider.py @@ -441,8 +441,8 @@ class AzureProvider(Provider): None """ printed_subscriptions = [] - for key, value in self._identity.subscriptions.items(): - intermediate = key + ": " + value + for subscription_id, display_name in self._identity.subscriptions.items(): + intermediate = display_name + ": " + subscription_id printed_subscriptions.append(intermediate) report_lines = [ f"Azure Tenant Domain: {Fore.YELLOW}{self._identity.tenant_domain}{Style.RESET_ALL} Azure Tenant ID: {Fore.YELLOW}{self._identity.tenant_ids[0]}{Style.RESET_ALL}", @@ -949,7 +949,7 @@ class AzureProvider(Provider): f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" ) - asyncio.get_event_loop().run_until_complete(get_azure_identity()) + asyncio.run(get_azure_identity()) # Managed identities only can be assigned resource, resource group and subscription scope permissions elif managed_identity_auth: @@ -969,19 +969,30 @@ class AzureProvider(Provider): ) if not subscription_ids: logger.info("Scanning all the Azure subscriptions...") - for subscription in subscriptions_client.subscriptions.list(): - # TODO: get tags or labels - # TODO: fill with AzureSubscription - identity.subscriptions.update( - {subscription.display_name: subscription.subscription_id} - ) + # TODO: get tags or labels + # TODO: fill with AzureSubscription + subscription_pairs = [ + (subscription.display_name, subscription.subscription_id) + for subscription in subscriptions_client.subscriptions.list() + ] else: logger.info("Scanning the subscriptions passed as argument ...") - for id in subscription_ids: - subscription = subscriptions_client.subscriptions.get( - subscription_id=id + subscription_pairs = [ + ( + subscriptions_client.subscriptions.get( + subscription_id=id + ).display_name, + id, ) - identity.subscriptions.update({subscription.display_name: id}) + for id in subscription_ids + ] + + # Key the subscriptions dict by subscription ID (which is + # guaranteed unique) and store the display name as the value. + # This avoids collisions when multiple subscriptions share + # the same display name. + for display_name, subscription_id in subscription_pairs: + identity.subscriptions[subscription_id] = display_name # If there are no subscriptions listed -> checks are not going to be run against any resource if not identity.subscriptions: @@ -1017,28 +1028,28 @@ class AzureProvider(Provider): Returns: A dictionary containing the locations available for each subscription. The dictionary - has subscription display names as keys and lists of location names as values. + has subscription IDs as keys and lists of location names as values. Examples: >>> provider = AzureProvider(...) >>> provider.get_locations() { - 'Subscription 1': ['eastus', 'eastus2', 'westus', 'westus2'], - 'Subscription 2': ['eastus', 'eastus2', 'westus', 'westus2'] + 'sub-id-1': ['eastus', 'eastus2', 'westus', 'westus2'], + 'sub-id-2': ['eastus', 'eastus2', 'westus', 'westus2'] } """ credentials = self.session subscription_client = SubscriptionClient(credentials) locations = {} - for display_name, subscription_id in self._identity.subscriptions.items(): - locations[display_name] = [] + for subscription_id, display_name in self._identity.subscriptions.items(): + locations[subscription_id] = [] # List locations for each subscription for location in subscription_client.subscriptions.list_locations( subscription_id ): - locations[display_name].append(location.name) + locations[subscription_id].append(location.name) return locations diff --git a/prowler/providers/azure/lib/mutelist/mutelist.py b/prowler/providers/azure/lib/mutelist/mutelist.py index 90ad609a1a..7d80d2e4cf 100644 --- a/prowler/providers/azure/lib/mutelist/mutelist.py +++ b/prowler/providers/azure/lib/mutelist/mutelist.py @@ -8,17 +8,23 @@ class AzureMutelist(Mutelist): self, finding: Check_Report_Azure, subscription_id: str, + subscription_name: str = "", ) -> bool: - return self.is_muted( - subscription_id, # support Azure Subscription ID in mutelist - finding.check_metadata.CheckID, - finding.location, - finding.resource_name, - unroll_dict(unroll_tags(finding.resource_tags)), - ) or self.is_muted( - finding.subscription, # support Azure Subscription Name in mutelist - finding.check_metadata.CheckID, - finding.location, - finding.resource_name, - unroll_dict(unroll_tags(finding.resource_tags)), - ) + account_names = [subscription_id] + for account_name in (subscription_name, finding.subscription): + if account_name and account_name not in account_names: + account_names.append(account_name) + + tags = unroll_dict(unroll_tags(finding.resource_tags)) + + for account_name in account_names: + if self.is_muted( + account_name, + finding.check_metadata.CheckID, + finding.location, + finding.resource_name, + tags, + ): + return True + + return False diff --git a/prowler/providers/azure/lib/service/service.py b/prowler/providers/azure/lib/service/service.py index a4fc4b9b9b..f8cfd417c9 100644 --- a/prowler/providers/azure/lib/service/service.py +++ b/prowler/providers/azure/lib/service/service.py @@ -49,15 +49,15 @@ class AzureService: if "GraphServiceClient" in str(service): clients.update({identity.tenant_domain: service(credentials=session)}) elif "LogsQueryClient" in str(service): - for display_name, id in identity.subscriptions.items(): - clients.update({display_name: service(credential=session)}) + for subscription_id, display_name in identity.subscriptions.items(): + clients.update({subscription_id: service(credential=session)}) else: - for display_name, id in identity.subscriptions.items(): + for subscription_id, display_name in identity.subscriptions.items(): clients.update( { - display_name: service( + subscription_id: service( credential=session, - subscription_id=id, + subscription_id=subscription_id, base_url=region_config.base_url, credential_scopes=region_config.credential_scopes, ) diff --git a/prowler/providers/azure/services/aisearch/aisearch_service.py b/prowler/providers/azure/services/aisearch/aisearch_service.py index 2324be227d..3f482a41a5 100644 --- a/prowler/providers/azure/services/aisearch/aisearch_service.py +++ b/prowler/providers/azure/services/aisearch/aisearch_service.py @@ -36,7 +36,7 @@ class AISearch(AzureService): ) except Exception as error: logger.error( - f"Subscription name: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return aisearch_services diff --git a/prowler/providers/azure/services/aisearch/aisearch_service_not_publicly_accessible/aisearch_service_not_publicly_accessible.py b/prowler/providers/azure/services/aisearch/aisearch_service_not_publicly_accessible/aisearch_service_not_publicly_accessible.py index 424b7a832e..9e048c7ba2 100644 --- a/prowler/providers/azure/services/aisearch/aisearch_service_not_publicly_accessible/aisearch_service_not_publicly_accessible.py +++ b/prowler/providers/azure/services/aisearch/aisearch_service_not_publicly_accessible/aisearch_service_not_publicly_accessible.py @@ -9,20 +9,23 @@ class aisearch_service_not_publicly_accessible(Check): findings = [] for ( - subscription_name, + subscription_id, aisearch_services, ) in aisearch_client.aisearch_services.items(): + subscription_name = aisearch_client.subscriptions.get( + subscription_id, subscription_id + ) for aisearch_service in aisearch_services.values(): report = Check_Report_Azure( metadata=self.metadata(), resource=aisearch_service ) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "FAIL" - report.status_extended = f"AISearch Service {aisearch_service.name} from subscription {subscription_name} allows public access." + report.status_extended = f"AISearch Service {aisearch_service.name} from subscription {subscription_name} ({subscription_id}) allows public access." if not aisearch_service.public_network_access: report.status = "PASS" - report.status_extended = f"AISearch Service {aisearch_service.name} from subscription {subscription_name} does not allows public access." + report.status_extended = f"AISearch Service {aisearch_service.name} from subscription {subscription_name} ({subscription_id}) does not allows public access." findings.append(report) diff --git a/prowler/providers/azure/services/aks/aks_cluster_rbac_enabled/aks_cluster_rbac_enabled.py b/prowler/providers/azure/services/aks/aks_cluster_rbac_enabled/aks_cluster_rbac_enabled.py index e5478b0d90..12ef99e524 100644 --- a/prowler/providers/azure/services/aks/aks_cluster_rbac_enabled/aks_cluster_rbac_enabled.py +++ b/prowler/providers/azure/services/aks/aks_cluster_rbac_enabled/aks_cluster_rbac_enabled.py @@ -6,16 +6,19 @@ class aks_cluster_rbac_enabled(Check): def execute(self) -> Check_Report_Azure: findings = [] - for subscription_name, clusters in aks_client.clusters.items(): + for subscription_id, clusters in aks_client.clusters.items(): + subscription_name = aks_client.subscriptions.get( + subscription_id, subscription_id + ) for cluster in clusters.values(): report = Check_Report_Azure(metadata=self.metadata(), resource=cluster) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "PASS" - report.status_extended = f"RBAC is enabled for cluster '{cluster.name}' in subscription '{subscription_name}'." + report.status_extended = f"RBAC is enabled for cluster '{cluster.name}' in subscription '{subscription_name} ({subscription_id})'." if not cluster.rbac_enabled: report.status = "FAIL" - report.status_extended = f"RBAC is not enabled for cluster '{cluster.name}' in subscription '{subscription_name}'." + report.status_extended = f"RBAC is not enabled for cluster '{cluster.name}' in subscription '{subscription_name} ({subscription_id})'." findings.append(report) diff --git a/prowler/providers/azure/services/aks/aks_clusters_created_with_private_nodes/aks_clusters_created_with_private_nodes.py b/prowler/providers/azure/services/aks/aks_clusters_created_with_private_nodes/aks_clusters_created_with_private_nodes.py index 6de9b3653f..7eab51e085 100644 --- a/prowler/providers/azure/services/aks/aks_clusters_created_with_private_nodes/aks_clusters_created_with_private_nodes.py +++ b/prowler/providers/azure/services/aks/aks_clusters_created_with_private_nodes/aks_clusters_created_with_private_nodes.py @@ -6,17 +6,20 @@ class aks_clusters_created_with_private_nodes(Check): def execute(self) -> Check_Report_Azure: findings = [] - for subscription_name, clusters in aks_client.clusters.items(): + for subscription_id, clusters in aks_client.clusters.items(): + subscription_name = aks_client.subscriptions.get( + subscription_id, subscription_id + ) for cluster in clusters.values(): report = Check_Report_Azure(metadata=self.metadata(), resource=cluster) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "PASS" - report.status_extended = f"Cluster '{cluster.name}' was created with private nodes in subscription '{subscription_name}'" + report.status_extended = f"Cluster '{cluster.name}' was created with private nodes in subscription '{subscription_name} ({subscription_id})'" for agent_pool in cluster.agent_pool_profiles: if getattr(agent_pool, "enable_node_public_ip", True): report.status = "FAIL" - report.status_extended = f"Cluster '{cluster.name}' was not created with private nodes in subscription '{subscription_name}'" + report.status_extended = f"Cluster '{cluster.name}' was not created with private nodes in subscription '{subscription_name} ({subscription_id})'" break findings.append(report) diff --git a/prowler/providers/azure/services/aks/aks_clusters_public_access_disabled/aks_clusters_public_access_disabled.py b/prowler/providers/azure/services/aks/aks_clusters_public_access_disabled/aks_clusters_public_access_disabled.py index b607abb6d9..5c73934e50 100644 --- a/prowler/providers/azure/services/aks/aks_clusters_public_access_disabled/aks_clusters_public_access_disabled.py +++ b/prowler/providers/azure/services/aks/aks_clusters_public_access_disabled/aks_clusters_public_access_disabled.py @@ -6,18 +6,21 @@ class aks_clusters_public_access_disabled(Check): def execute(self) -> Check_Report_Azure: findings = [] - for subscription_name, clusters in aks_client.clusters.items(): + for subscription_id, clusters in aks_client.clusters.items(): + subscription_name = aks_client.subscriptions.get( + subscription_id, subscription_id + ) for cluster in clusters.values(): report = Check_Report_Azure(metadata=self.metadata(), resource=cluster) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "FAIL" - report.status_extended = f"Public access to nodes is enabled for cluster '{cluster.name}' in subscription '{subscription_name}'" + report.status_extended = f"Public access to nodes is enabled for cluster '{cluster.name}' in subscription '{subscription_name} ({subscription_id})'" if cluster.private_fqdn: for agent_pool in cluster.agent_pool_profiles: if not getattr(agent_pool, "enable_node_public_ip", False): report.status = "PASS" - report.status_extended = f"Public access to nodes is disabled for cluster '{cluster.name}' in subscription '{subscription_name}'" + report.status_extended = f"Public access to nodes is disabled for cluster '{cluster.name}' in subscription '{subscription_name} ({subscription_id})'" findings.append(report) diff --git a/prowler/providers/azure/services/aks/aks_network_policy_enabled/aks_network_policy_enabled.py b/prowler/providers/azure/services/aks/aks_network_policy_enabled/aks_network_policy_enabled.py index 2af996ffa5..53a1562b47 100644 --- a/prowler/providers/azure/services/aks/aks_network_policy_enabled/aks_network_policy_enabled.py +++ b/prowler/providers/azure/services/aks/aks_network_policy_enabled/aks_network_policy_enabled.py @@ -6,16 +6,19 @@ class aks_network_policy_enabled(Check): def execute(self) -> Check_Report_Azure: findings = [] - for subscription_name, clusters in aks_client.clusters.items(): + for subscription_id, clusters in aks_client.clusters.items(): + subscription_name = aks_client.subscriptions.get( + subscription_id, subscription_id + ) for cluster_id, cluster in clusters.items(): report = Check_Report_Azure(metadata=self.metadata(), resource=cluster) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "PASS" - report.status_extended = f"Network policy is enabled for cluster '{cluster.name}' in subscription '{subscription_name}'." + report.status_extended = f"Network policy is enabled for cluster '{cluster.name}' in subscription '{subscription_name} ({subscription_id})'." if not getattr(cluster, "network_policy", False): report.status = "FAIL" - report.status_extended = f"Network policy is not enabled for cluster '{cluster.name}' in subscription '{subscription_name}'." + report.status_extended = f"Network policy is not enabled for cluster '{cluster.name}' in subscription '{subscription_name} ({subscription_id})'." findings.append(report) diff --git a/prowler/providers/azure/services/aks/aks_service.py b/prowler/providers/azure/services/aks/aks_service.py index 4c269fbf28..3d158a2f70 100644 --- a/prowler/providers/azure/services/aks/aks_service.py +++ b/prowler/providers/azure/services/aks/aks_service.py @@ -17,14 +17,14 @@ class AKS(AzureService): logger.info("AKS - Getting clusters...") clusters = {} - for subscription_name, client in self.clients.items(): + for subscription_id, client in self.clients.items(): try: clusters_list = client.managed_clusters.list() - clusters.update({subscription_name: {}}) + clusters.update({subscription_id: {}}) for cluster in clusters_list: if getattr(cluster, "kubernetes_version", None): - clusters[subscription_name].update( + clusters[subscription_id].update( { cluster.id: Cluster( id=cluster.id, @@ -60,7 +60,7 @@ class AKS(AzureService): ) except Exception as error: logger.error( - f"Subscription name: {subscription_name} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription_id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return clusters diff --git a/prowler/providers/azure/services/apim/apim_service.py b/prowler/providers/azure/services/apim/apim_service.py index 793eb727c3..98fb00f276 100644 --- a/prowler/providers/azure/services/apim/apim_service.py +++ b/prowler/providers/azure/services/apim/apim_service.py @@ -147,7 +147,7 @@ class APIM(AzureService): ) except Exception as error: logger.error( - f"Subscription name: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return instances diff --git a/prowler/providers/azure/services/apim/apim_threat_detection_llm_jacking/apim_threat_detection_llm_jacking.py b/prowler/providers/azure/services/apim/apim_threat_detection_llm_jacking/apim_threat_detection_llm_jacking.py index c08d4fe954..0fe68aa223 100644 --- a/prowler/providers/azure/services/apim/apim_threat_detection_llm_jacking/apim_threat_detection_llm_jacking.py +++ b/prowler/providers/azure/services/apim/apim_threat_detection_llm_jacking/apim_threat_detection_llm_jacking.py @@ -50,9 +50,11 @@ class apim_threat_detection_llm_jacking(Check): ], ) - # 1. Aggregate logs from all APIM instances first - all_llm_logs: List[LogsQueryLogEntry] = [] for subscription, instances in apim_client.instances.items(): + subscription_name = apim_client.subscriptions.get( + subscription, subscription + ) + all_llm_logs: List[LogsQueryLogEntry] = [] for instance in instances: if instance.log_analytics_workspace_id: logs = apim_client.get_llm_operations_logs( @@ -60,7 +62,8 @@ class apim_threat_detection_llm_jacking(Check): ) all_llm_logs.extend(logs) - # 2. Perform a single, global analysis on all collected logs + # Analyze logs only within the current subscription to avoid + # cross-subscription attribution when scanning multiple subscriptions. potential_llm_jacking_attackers = {} for log in all_llm_logs: operation_name = log.operation_id @@ -91,19 +94,17 @@ class apim_threat_detection_llm_jacking(Check): report = Check_Report_Azure(self.metadata(), resource=resource) report.subscription = subscription report.status = "FAIL" - report.status_extended = f"Potential LLM Jacking attack detected from IP address {principal_ip} with a threshold of {action_ratio}." + report.status_extended = f"Potential LLM Jacking attack detected from IP address {principal_ip} in subscription {subscription_name} ({subscription}) with an action ratio of {action_ratio}, above the configured threshold of {threshold}." findings.append(report) - # 4. If no threats were found after checking all principals, create a single PASS report + # If no threats were found after checking all principals, create a single PASS report. if not found_potential_llm_jacking_attackers: report = Check_Report_Azure(self.metadata(), resource={}) - report.resource_name = subscription - report.resource_id = ( - f"/subscriptions/{apim_client.subscriptions[subscription]}" - ) + report.resource_name = subscription_name + report.resource_id = f"/subscriptions/{subscription}" report.subscription = subscription report.status = "PASS" - report.status_extended = f"No potential LLM Jacking attacks detected across all monitored APIM instances in the last {threat_detection_minutes} minutes." + report.status_extended = f"No potential LLM Jacking attacks detected across monitored APIM instances in subscription {subscription_name} ({subscription}) in the last {threat_detection_minutes} minutes." findings.append(report) return findings diff --git a/prowler/providers/azure/services/app/app_client_certificates_on/app_client_certificates_on.py b/prowler/providers/azure/services/app/app_client_certificates_on/app_client_certificates_on.py index 44103a7625..d146a23334 100644 --- a/prowler/providers/azure/services/app/app_client_certificates_on/app_client_certificates_on.py +++ b/prowler/providers/azure/services/app/app_client_certificates_on/app_client_certificates_on.py @@ -7,18 +7,21 @@ class app_client_certificates_on(Check): findings = [] for ( - subscription_name, + subscription_id, apps, ) in app_client.apps.items(): + subscription_name = app_client.subscriptions.get( + subscription_id, subscription_id + ) for app in apps.values(): report = Check_Report_Azure(metadata=self.metadata(), resource=app) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "PASS" - report.status_extended = f"Clients are required to present a certificate for app '{app.name}' in subscription '{subscription_name}'." + report.status_extended = f"Clients are required to present a certificate for app '{app.name}' in subscription '{subscription_name} ({subscription_id})'." if app.client_cert_mode != "Required": report.status = "FAIL" - report.status_extended = f"Clients are not required to present a certificate for app '{app.name}' in subscription '{subscription_name}'." + report.status_extended = f"Clients are not required to present a certificate for app '{app.name}' in subscription '{subscription_name} ({subscription_id})'." findings.append(report) diff --git a/prowler/providers/azure/services/app/app_ensure_auth_is_set_up/app_ensure_auth_is_set_up.py b/prowler/providers/azure/services/app/app_ensure_auth_is_set_up/app_ensure_auth_is_set_up.py index 93d9d5b944..885fa64317 100644 --- a/prowler/providers/azure/services/app/app_ensure_auth_is_set_up/app_ensure_auth_is_set_up.py +++ b/prowler/providers/azure/services/app/app_ensure_auth_is_set_up/app_ensure_auth_is_set_up.py @@ -7,18 +7,21 @@ class app_ensure_auth_is_set_up(Check): findings = [] for ( - subscription_name, + subscription_id, apps, ) in app_client.apps.items(): + subscription_name = app_client.subscriptions.get( + subscription_id, subscription_id + ) for app in apps.values(): report = Check_Report_Azure(metadata=self.metadata(), resource=app) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "PASS" - report.status_extended = f"Authentication is set up for app '{app.name}' in subscription '{subscription_name}'." + report.status_extended = f"Authentication is set up for app '{app.name}' in subscription '{subscription_name} ({subscription_id})'." if not app.auth_enabled: report.status = "FAIL" - report.status_extended = f"Authentication is not set up for app '{app.name}' in subscription '{subscription_name}'." + report.status_extended = f"Authentication is not set up for app '{app.name}' in subscription '{subscription_name} ({subscription_id})'." findings.append(report) diff --git a/prowler/providers/azure/services/app/app_ensure_http_is_redirected_to_https/app_ensure_http_is_redirected_to_https.py b/prowler/providers/azure/services/app/app_ensure_http_is_redirected_to_https/app_ensure_http_is_redirected_to_https.py index 47a0b8851a..04c846c904 100644 --- a/prowler/providers/azure/services/app/app_ensure_http_is_redirected_to_https/app_ensure_http_is_redirected_to_https.py +++ b/prowler/providers/azure/services/app/app_ensure_http_is_redirected_to_https/app_ensure_http_is_redirected_to_https.py @@ -7,18 +7,21 @@ class app_ensure_http_is_redirected_to_https(Check): findings = [] for ( - subscription_name, + subscription_id, apps, ) in app_client.apps.items(): + subscription_name = app_client.subscriptions.get( + subscription_id, subscription_id + ) for app in apps.values(): report = Check_Report_Azure(metadata=self.metadata(), resource=app) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "PASS" - report.status_extended = f"HTTP is redirected to HTTPS for app '{app.name}' in subscription '{subscription_name}'." + report.status_extended = f"HTTP is redirected to HTTPS for app '{app.name}' in subscription '{subscription_name} ({subscription_id})'." if not app.https_only: report.status = "FAIL" - report.status_extended = f"HTTP is not redirected to HTTPS for app '{app.name}' in subscription '{subscription_name}'." + report.status_extended = f"HTTP is not redirected to HTTPS for app '{app.name}' in subscription '{subscription_name} ({subscription_id})'." findings.append(report) diff --git a/prowler/providers/azure/services/app/app_ensure_java_version_is_latest/app_ensure_java_version_is_latest.py b/prowler/providers/azure/services/app/app_ensure_java_version_is_latest/app_ensure_java_version_is_latest.py index bc4caf7cf3..46aaa5aa9a 100644 --- a/prowler/providers/azure/services/app/app_ensure_java_version_is_latest/app_ensure_java_version_is_latest.py +++ b/prowler/providers/azure/services/app/app_ensure_java_version_is_latest/app_ensure_java_version_is_latest.py @@ -7,9 +7,12 @@ class app_ensure_java_version_is_latest(Check): findings = [] for ( - subscription_name, + subscription_id, apps, ) in app_client.apps.items(): + subscription_name = app_client.subscriptions.get( + subscription_id, subscription_id + ) for app in apps.values(): linux_framework = getattr(app.configurations, "linux_fx_version", "") windows_framework_version = getattr( @@ -18,19 +21,19 @@ class app_ensure_java_version_is_latest(Check): if "java" in linux_framework.lower() or windows_framework_version: report = Check_Report_Azure(metadata=self.metadata(), resource=app) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "FAIL" java_latest_version = app_client.audit_config.get( "java_latest_version", "17" ) - report.status_extended = f"Java version is set to '{f'java{windows_framework_version}' if windows_framework_version else linux_framework}', but should be set to 'java {java_latest_version}' for app '{app.name}' in subscription '{subscription_name}'." + report.status_extended = f"Java version is set to '{f'java{windows_framework_version}' if windows_framework_version else linux_framework}', but should be set to 'java {java_latest_version}' for app '{app.name}' in subscription '{subscription_name} ({subscription_id})'." if ( f"java{java_latest_version}" in linux_framework or java_latest_version == windows_framework_version ): report.status = "PASS" - report.status_extended = f"Java version is set to 'java {java_latest_version}' for app '{app.name}' in subscription '{subscription_name}'." + report.status_extended = f"Java version is set to 'java {java_latest_version}' for app '{app.name}' in subscription '{subscription_name} ({subscription_id})'." findings.append(report) diff --git a/prowler/providers/azure/services/app/app_ensure_php_version_is_latest/app_ensure_php_version_is_latest.py b/prowler/providers/azure/services/app/app_ensure_php_version_is_latest/app_ensure_php_version_is_latest.py index 8c48c629e3..7ccd31fbb5 100644 --- a/prowler/providers/azure/services/app/app_ensure_php_version_is_latest/app_ensure_php_version_is_latest.py +++ b/prowler/providers/azure/services/app/app_ensure_php_version_is_latest/app_ensure_php_version_is_latest.py @@ -7,9 +7,12 @@ class app_ensure_php_version_is_latest(Check): findings = [] for ( - subscription_name, + subscription_id, apps, ) in app_client.apps.items(): + subscription_name = app_client.subscriptions.get( + subscription_id, subscription_id + ) for app in apps.values(): framework = getattr(app.configurations, "linux_fx_version", "") @@ -17,14 +20,14 @@ class app_ensure_php_version_is_latest(Check): app.configurations, "php_version", "" ): report = Check_Report_Azure(metadata=self.metadata(), resource=app) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "FAIL" php_latest_version = app_client.audit_config.get( "php_latest_version", "8.2" ) - report.status_extended = f"PHP version is set to '{framework}', the latest version that you could use is the '{php_latest_version}' version, for app '{app.name}' in subscription '{subscription_name}'." + report.status_extended = f"PHP version is set to '{framework}', the latest version that you could use is the '{php_latest_version}' version, for app '{app.name}' in subscription '{subscription_name} ({subscription_id})'." if ( php_latest_version in framework @@ -32,7 +35,7 @@ class app_ensure_php_version_is_latest(Check): == php_latest_version ): report.status = "PASS" - report.status_extended = f"PHP version is set to '{php_latest_version}' for app '{app.name}' in subscription '{subscription_name}'." + report.status_extended = f"PHP version is set to '{php_latest_version}' for app '{app.name}' in subscription '{subscription_name} ({subscription_id})'." findings.append(report) diff --git a/prowler/providers/azure/services/app/app_ensure_python_version_is_latest/app_ensure_python_version_is_latest.py b/prowler/providers/azure/services/app/app_ensure_python_version_is_latest/app_ensure_python_version_is_latest.py index 9be2d127e1..9ea6690843 100644 --- a/prowler/providers/azure/services/app/app_ensure_python_version_is_latest/app_ensure_python_version_is_latest.py +++ b/prowler/providers/azure/services/app/app_ensure_python_version_is_latest/app_ensure_python_version_is_latest.py @@ -7,9 +7,12 @@ class app_ensure_python_version_is_latest(Check): findings = [] for ( - subscription_name, + subscription_id, apps, ) in app_client.apps.items(): + subscription_name = app_client.subscriptions.get( + subscription_id, subscription_id + ) for app in apps.values(): framework = getattr(app.configurations, "linux_fx_version", "") @@ -17,12 +20,12 @@ class app_ensure_python_version_is_latest(Check): app.configurations, "python_version", "" ): report = Check_Report_Azure(metadata=self.metadata(), resource=app) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "FAIL" python_latest_version = app_client.audit_config.get( "python_latest_version", "3.12" ) - report.status_extended = f"Python version is '{framework}', the latest version that you could use is the '{python_latest_version}' version, for app '{app.name}' in subscription '{subscription_name}'." + report.status_extended = f"Python version is '{framework}', the latest version that you could use is the '{python_latest_version}' version, for app '{app.name}' in subscription '{subscription_name} ({subscription_id})'." if ( python_latest_version in framework @@ -30,7 +33,7 @@ class app_ensure_python_version_is_latest(Check): == python_latest_version ): report.status = "PASS" - report.status_extended = f"Python version is set to '{python_latest_version}' for app '{app.name}' in subscription '{subscription_name}'." + report.status_extended = f"Python version is set to '{python_latest_version}' for app '{app.name}' in subscription '{subscription_name} ({subscription_id})'." findings.append(report) diff --git a/prowler/providers/azure/services/app/app_ensure_using_http20/app_ensure_using_http20.py b/prowler/providers/azure/services/app/app_ensure_using_http20/app_ensure_using_http20.py index 52ea5c83cd..d08a9ef90d 100644 --- a/prowler/providers/azure/services/app/app_ensure_using_http20/app_ensure_using_http20.py +++ b/prowler/providers/azure/services/app/app_ensure_using_http20/app_ensure_using_http20.py @@ -7,20 +7,23 @@ class app_ensure_using_http20(Check): findings = [] for ( - subscription_name, + subscription_id, apps, ) in app_client.apps.items(): + subscription_name = app_client.subscriptions.get( + subscription_id, subscription_id + ) for app in apps.values(): report = Check_Report_Azure(metadata=self.metadata(), resource=app) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "FAIL" - report.status_extended = f"HTTP/2.0 is not enabled for app '{app.name}' in subscription '{subscription_name}'." + report.status_extended = f"HTTP/2.0 is not enabled for app '{app.name}' in subscription '{subscription_name} ({subscription_id})'." if app.configurations and getattr( app.configurations, "http20_enabled", False ): report.status = "PASS" - report.status_extended = f"HTTP/2.0 is enabled for app '{app.name}' in subscription '{subscription_name}'." + report.status_extended = f"HTTP/2.0 is enabled for app '{app.name}' in subscription '{subscription_name} ({subscription_id})'." findings.append(report) diff --git a/prowler/providers/azure/services/app/app_ftp_deployment_disabled/app_ftp_deployment_disabled.py b/prowler/providers/azure/services/app/app_ftp_deployment_disabled/app_ftp_deployment_disabled.py index 7177b05a69..94f41e8f55 100644 --- a/prowler/providers/azure/services/app/app_ftp_deployment_disabled/app_ftp_deployment_disabled.py +++ b/prowler/providers/azure/services/app/app_ftp_deployment_disabled/app_ftp_deployment_disabled.py @@ -7,21 +7,24 @@ class app_ftp_deployment_disabled(Check): findings = [] for ( - subscription_name, + subscription_id, apps, ) in app_client.apps.items(): + subscription_name = app_client.subscriptions.get( + subscription_id, subscription_id + ) for app in apps.values(): report = Check_Report_Azure(metadata=self.metadata(), resource=app) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "FAIL" - report.status_extended = f"FTP is enabled for app '{app.name}' in subscription '{subscription_name}'." + report.status_extended = f"FTP is enabled for app '{app.name}' in subscription '{subscription_name} ({subscription_id})'." if ( app.configurations and getattr(app.configurations, "ftps_state", "AllAllowed") != "AllAllowed" ): report.status = "PASS" - report.status_extended = f"FTP is disabled for app '{app.name}' in subscription '{subscription_name}'." + report.status_extended = f"FTP is disabled for app '{app.name}' in subscription '{subscription_name} ({subscription_id})'." findings.append(report) diff --git a/prowler/providers/azure/services/app/app_function_access_keys_configured/app_function_access_keys_configured.py b/prowler/providers/azure/services/app/app_function_access_keys_configured/app_function_access_keys_configured.py index 4c1fb89756..45f273784d 100644 --- a/prowler/providers/azure/services/app/app_function_access_keys_configured/app_function_access_keys_configured.py +++ b/prowler/providers/azure/services/app/app_function_access_keys_configured/app_function_access_keys_configured.py @@ -7,23 +7,24 @@ class app_function_access_keys_configured(Check): findings = [] for ( - subscription_name, + subscription_id, functions, ) in app_client.functions.items(): + subscription_name = app_client.subscriptions.get( + subscription_id, subscription_id + ) for function in functions.values(): if function.function_keys is not None: report = Check_Report_Azure( metadata=self.metadata(), resource=function ) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "FAIL" - report.status_extended = f"Function {function.name} does not have function keys configured." + report.status_extended = f"Function {function.name} from subscription {subscription_name} ({subscription_id}) does not have function keys configured." if len(function.function_keys) > 0: report.status = "PASS" - report.status_extended = ( - f"Function {function.name} has function keys configured." - ) + report.status_extended = f"Function {function.name} from subscription {subscription_name} ({subscription_id}) has function keys configured." findings.append(report) diff --git a/prowler/providers/azure/services/app/app_function_application_insights_enabled/app_function_application_insights_enabled.py b/prowler/providers/azure/services/app/app_function_application_insights_enabled/app_function_application_insights_enabled.py index 004af0da30..6fec5e7042 100644 --- a/prowler/providers/azure/services/app/app_function_application_insights_enabled/app_function_application_insights_enabled.py +++ b/prowler/providers/azure/services/app/app_function_application_insights_enabled/app_function_application_insights_enabled.py @@ -7,19 +7,20 @@ class app_function_application_insights_enabled(Check): findings = [] for ( - subscription_name, + subscription_id, functions, ) in app_client.functions.items(): + subscription_name = app_client.subscriptions.get( + subscription_id, subscription_id + ) for function in functions.values(): if function.enviroment_variables is not None: report = Check_Report_Azure( metadata=self.metadata(), resource=function ) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "FAIL" - report.status_extended = ( - f"Function {function.name} is not using Application Insights." - ) + report.status_extended = f"Function {function.name} from subscription {subscription_name} ({subscription_id}) is not using Application Insights." if function.enviroment_variables.get( "APPINSIGHTS_INSTRUMENTATIONKEY", None @@ -27,9 +28,7 @@ class app_function_application_insights_enabled(Check): "APPLICATIONINSIGHTS_CONNECTION_STRING", None ): report.status = "PASS" - report.status_extended = ( - f"Function {function.name} is using Application Insights." - ) + report.status_extended = f"Function {function.name} from subscription {subscription_name} ({subscription_id}) is using Application Insights." findings.append(report) diff --git a/prowler/providers/azure/services/app/app_function_ftps_deployment_disabled/app_function_ftps_deployment_disabled.py b/prowler/providers/azure/services/app/app_function_ftps_deployment_disabled/app_function_ftps_deployment_disabled.py index c899986036..9922e174cb 100644 --- a/prowler/providers/azure/services/app/app_function_ftps_deployment_disabled/app_function_ftps_deployment_disabled.py +++ b/prowler/providers/azure/services/app/app_function_ftps_deployment_disabled/app_function_ftps_deployment_disabled.py @@ -7,19 +7,20 @@ class app_function_ftps_deployment_disabled(Check): findings = [] for ( - subscription_name, + subscription_id, functions, ) in app_client.functions.items(): + subscription_name = app_client.subscriptions.get( + subscription_id, subscription_id + ) for function in functions.values(): report = Check_Report_Azure(metadata=self.metadata(), resource=function) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "FAIL" - report.status_extended = f"Function {function.name} has {'FTP' if function.ftps_state == 'AllAllowed' else 'FTPS' if function.ftps_state == 'FtpsOnly' else 'FTP or FTPS'} deployment enabled" + report.status_extended = f"Function {function.name} from subscription {subscription_name} ({subscription_id}) has {'FTP' if function.ftps_state == 'AllAllowed' else 'FTPS' if function.ftps_state == 'FtpsOnly' else 'FTP or FTPS'} deployment enabled." if function.ftps_state == "Disabled": report.status = "PASS" - report.status_extended = ( - f"Function {function.name} has FTP and FTPS deployment disabled" - ) + report.status_extended = f"Function {function.name} from subscription {subscription_name} ({subscription_id}) has FTP and FTPS deployment disabled." findings.append(report) diff --git a/prowler/providers/azure/services/app/app_function_identity_is_configured/app_function_identity_is_configured.py b/prowler/providers/azure/services/app/app_function_identity_is_configured/app_function_identity_is_configured.py index 0d68971f95..6ee83d6c5f 100644 --- a/prowler/providers/azure/services/app/app_function_identity_is_configured/app_function_identity_is_configured.py +++ b/prowler/providers/azure/services/app/app_function_identity_is_configured/app_function_identity_is_configured.py @@ -7,18 +7,26 @@ class app_function_identity_is_configured(Check): findings = [] for ( - subscription_name, + subscription_id, functions, ) in app_client.functions.items(): + subscription_name = app_client.subscriptions.get( + subscription_id, subscription_id + ) for function in functions.values(): report = Check_Report_Azure(metadata=self.metadata(), resource=function) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "FAIL" - report.status_extended = f"Function {function.name} does not have a managed identity enabled." + report.status_extended = f"Function {function.name} from subscription {subscription_name} ({subscription_id}) does not have a managed identity enabled." if function.identity: + identity_type = ( + function.identity.type + if getattr(function.identity, "type", "") + else "managed" + ) report.status = "PASS" - report.status_extended = f"Function {function.name} has a {function.identity.type if getattr(function.identity, 'type', '') else 'managed'} identity enabled." + report.status_extended = f"Function {function.name} from subscription {subscription_name} ({subscription_id}) has a {identity_type} identity enabled." findings.append(report) diff --git a/prowler/providers/azure/services/app/app_function_identity_without_admin_privileges/app_function_identity_without_admin_privileges.py b/prowler/providers/azure/services/app/app_function_identity_without_admin_privileges/app_function_identity_without_admin_privileges.py index 9804ce283c..5031ca7120 100644 --- a/prowler/providers/azure/services/app/app_function_identity_without_admin_privileges/app_function_identity_without_admin_privileges.py +++ b/prowler/providers/azure/services/app/app_function_identity_without_admin_privileges/app_function_identity_without_admin_privileges.py @@ -14,22 +14,25 @@ class app_function_identity_without_admin_privileges(Check): findings = [] for ( - subscription_name, + subscription_id, functions, ) in app_client.functions.items(): + subscription_name = app_client.subscriptions.get( + subscription_id, subscription_id + ) for function in functions.values(): if function.identity: report = Check_Report_Azure( metadata=self.metadata(), resource=function ) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "PASS" - report.status_extended = f"Function {function.name} has a managed identity enabled but without admin privileges." + report.status_extended = f"Function {function.name} from subscription {subscription_name} ({subscription_id}) has a managed identity enabled but without admin privileges." admin_roles_assigned = [] for role_assignment in iam_client.role_assignments[ - subscription_name + subscription_id ].values(): if ( role_assignment.agent_id == function.identity.principal_id @@ -43,8 +46,8 @@ class app_function_identity_without_admin_privileges(Check): ): admin_roles_assigned.append( getattr( - iam_client.roles[subscription_name].get( - f"/subscriptions/{iam_client.subscriptions[subscription_name]}/providers/Microsoft.Authorization/roleDefinitions/{role_assignment.role_id}" + iam_client.roles[subscription_id].get( + f"/subscriptions/{subscription_id}/providers/Microsoft.Authorization/roleDefinitions/{role_assignment.role_id}" ), "name", "", @@ -53,7 +56,7 @@ class app_function_identity_without_admin_privileges(Check): if admin_roles_assigned: report.status = "FAIL" - report.status_extended = f"Function {function.name} has a managed identity enabled and it is configure with admin privileges using {'roles: ' + ', '.join(admin_roles_assigned) if len(admin_roles_assigned) > 1 else 'role ' + admin_roles_assigned[0]}." + report.status_extended = f"Function {function.name} from subscription {subscription_name} ({subscription_id}) has a managed identity enabled and it is configure with admin privileges using {'roles: ' + ', '.join(admin_roles_assigned) if len(admin_roles_assigned) > 1 else 'role ' + admin_roles_assigned[0]}." findings.append(report) diff --git a/prowler/providers/azure/services/app/app_function_latest_runtime_version/app_function_latest_runtime_version.py b/prowler/providers/azure/services/app/app_function_latest_runtime_version/app_function_latest_runtime_version.py index 3cd8d349b4..828362a8fe 100644 --- a/prowler/providers/azure/services/app/app_function_latest_runtime_version/app_function_latest_runtime_version.py +++ b/prowler/providers/azure/services/app/app_function_latest_runtime_version/app_function_latest_runtime_version.py @@ -7,19 +7,20 @@ class app_function_latest_runtime_version(Check): findings = [] for ( - subscription_name, + subscription_id, functions, ) in app_client.functions.items(): + subscription_name = app_client.subscriptions.get( + subscription_id, subscription_id + ) for function in functions.values(): if function.enviroment_variables is not None: report = Check_Report_Azure( metadata=self.metadata(), resource=function ) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "PASS" - report.status_extended = ( - f"Function {function.name} is using the latest runtime." - ) + report.status_extended = f"Function {function.name} from subscription {subscription_name} ({subscription_id}) is using the latest runtime." if ( function.enviroment_variables.get( @@ -28,7 +29,7 @@ class app_function_latest_runtime_version(Check): != "~4" ): report.status = "FAIL" - report.status_extended = f"Function {function.name} is not using the latest runtime. The current runtime is '{function.enviroment_variables.get('FUNCTIONS_EXTENSION_VERSION', '')}' and should be '~4'." + report.status_extended = f"Function {function.name} from subscription {subscription_name} ({subscription_id}) is not using the latest runtime. The current runtime is '{function.enviroment_variables.get('FUNCTIONS_EXTENSION_VERSION', '')}' and should be '~4'." findings.append(report) diff --git a/prowler/providers/azure/services/app/app_function_not_publicly_accessible/app_function_not_publicly_accessible.py b/prowler/providers/azure/services/app/app_function_not_publicly_accessible/app_function_not_publicly_accessible.py index 3d506ae6e8..eede7d990f 100644 --- a/prowler/providers/azure/services/app/app_function_not_publicly_accessible/app_function_not_publicly_accessible.py +++ b/prowler/providers/azure/services/app/app_function_not_publicly_accessible/app_function_not_publicly_accessible.py @@ -7,22 +7,21 @@ class app_function_not_publicly_accessible(Check): findings = [] for ( - subscription_name, + subscription_id, functions, ) in app_client.functions.items(): + subscription_name = app_client.subscriptions.get( + subscription_id, subscription_id + ) for function in functions.values(): report = Check_Report_Azure(metadata=self.metadata(), resource=function) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "FAIL" - report.status_extended = ( - f"Function {function.name} is publicly accessible." - ) + report.status_extended = f"Function {function.name} from subscription {subscription_name} ({subscription_id}) is publicly accessible." if not function.public_access: report.status = "PASS" - report.status_extended = ( - f"Function {function.name} is not publicly accessible." - ) + report.status_extended = f"Function {function.name} from subscription {subscription_name} ({subscription_id}) is not publicly accessible." findings.append(report) diff --git a/prowler/providers/azure/services/app/app_function_vnet_integration_enabled/app_function_vnet_integration_enabled.py b/prowler/providers/azure/services/app/app_function_vnet_integration_enabled/app_function_vnet_integration_enabled.py index 027b98ac88..716c32955d 100644 --- a/prowler/providers/azure/services/app/app_function_vnet_integration_enabled/app_function_vnet_integration_enabled.py +++ b/prowler/providers/azure/services/app/app_function_vnet_integration_enabled/app_function_vnet_integration_enabled.py @@ -7,18 +7,21 @@ class app_function_vnet_integration_enabled(Check): findings = [] for ( - subscription_name, + subscription_id, functions, ) in app_client.functions.items(): + subscription_name = app_client.subscriptions.get( + subscription_id, subscription_id + ) for function in functions.values(): report = Check_Report_Azure(metadata=self.metadata(), resource=function) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "FAIL" - report.status_extended = f"Function {function.name} does not have virtual network integration enabled." + report.status_extended = f"Function {function.name} from subscription {subscription_name} ({subscription_id}) does not have virtual network integration enabled." if function.vnet_subnet_id: report.status = "PASS" - report.status_extended = f"Function {function.name} has Virtual Network integration enabled with subnet '{function.vnet_subnet_id}' enabled." + report.status_extended = f"Function {function.name} from subscription {subscription_name} ({subscription_id}) has Virtual Network integration enabled with subnet '{function.vnet_subnet_id}' enabled." findings.append(report) diff --git a/prowler/providers/azure/services/app/app_http_logs_enabled/app_http_logs_enabled.py b/prowler/providers/azure/services/app/app_http_logs_enabled/app_http_logs_enabled.py index 137ec3c494..ee3596e6bc 100644 --- a/prowler/providers/azure/services/app/app_http_logs_enabled/app_http_logs_enabled.py +++ b/prowler/providers/azure/services/app/app_http_logs_enabled/app_http_logs_enabled.py @@ -6,25 +6,28 @@ class app_http_logs_enabled(Check): def execute(self) -> Check_Report_Azure: findings = [] - for subscription_name, apps in app_client.apps.items(): + for subscription_id, apps in app_client.apps.items(): + subscription_name = app_client.subscriptions.get( + subscription_id, subscription_id + ) for app in apps.values(): if "functionapp" not in app.kind: report = Check_Report_Azure(metadata=self.metadata(), resource=app) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "FAIL" if not app.monitor_diagnostic_settings: - report.status_extended = f"App {app.name} does not have a diagnostic setting in subscription {subscription_name}." + report.status_extended = f"App {app.name} does not have a diagnostic setting in subscription {subscription_name} ({subscription_id})." else: for diagnostic_setting in app.monitor_diagnostic_settings: - report.status_extended = f"App {app.name} does not have HTTP Logs enabled in diagnostic setting {diagnostic_setting.name} in subscription {subscription_name}" + report.status_extended = f"App {app.name} does not have HTTP Logs enabled in diagnostic setting {diagnostic_setting.name} in subscription {subscription_name} ({subscription_id})" for log in diagnostic_setting.logs: if log.category == "AppServiceHTTPLogs" and log.enabled: report.status = "PASS" - report.status_extended = f"App {app.name} has HTTP Logs enabled in diagnostic setting {diagnostic_setting.name} in subscription {subscription_name}" + report.status_extended = f"App {app.name} has HTTP Logs enabled in diagnostic setting {diagnostic_setting.name} in subscription {subscription_name} ({subscription_id})" break elif log.category_group == "allLogs" and log.enabled: report.status = "PASS" - report.status_extended = f"App {app.name} has allLogs category group which includes HTTP Logs enabled in diagnostic setting {diagnostic_setting.name} in subscription {subscription_name}" + report.status_extended = f"App {app.name} has allLogs category group which includes HTTP Logs enabled in diagnostic setting {diagnostic_setting.name} in subscription {subscription_name} ({subscription_id})" break findings.append(report) diff --git a/prowler/providers/azure/services/app/app_minimum_tls_version_12/app_minimum_tls_version_12.py b/prowler/providers/azure/services/app/app_minimum_tls_version_12/app_minimum_tls_version_12.py index f6931ba7cf..75427c16c8 100644 --- a/prowler/providers/azure/services/app/app_minimum_tls_version_12/app_minimum_tls_version_12.py +++ b/prowler/providers/azure/services/app/app_minimum_tls_version_12/app_minimum_tls_version_12.py @@ -7,20 +7,23 @@ class app_minimum_tls_version_12(Check): findings = [] for ( - subscription_name, + subscription_id, apps, ) in app_client.apps.items(): + subscription_name = app_client.subscriptions.get( + subscription_id, subscription_id + ) for app in apps.values(): report = Check_Report_Azure(metadata=self.metadata(), resource=app) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "FAIL" - report.status_extended = f"Minimum TLS version is not set to 1.2 for app '{app.name}' in subscription '{subscription_name}'." + report.status_extended = f"Minimum TLS version is not set to 1.2 for app '{app.name}' in subscription '{subscription_name} ({subscription_id})'." if app.configurations and getattr( app.configurations, "min_tls_version", "" ) in ["1.2", "1.3"]: report.status = "PASS" - report.status_extended = f"Minimum TLS version is set to {app.configurations.min_tls_version} for app '{app.name}' in subscription '{subscription_name}'." + report.status_extended = f"Minimum TLS version is set to {app.configurations.min_tls_version} for app '{app.name}' in subscription '{subscription_name} ({subscription_id})'." findings.append(report) diff --git a/prowler/providers/azure/services/app/app_register_with_identity/app_register_with_identity.py b/prowler/providers/azure/services/app/app_register_with_identity/app_register_with_identity.py index 35961046f9..87bc58580f 100644 --- a/prowler/providers/azure/services/app/app_register_with_identity/app_register_with_identity.py +++ b/prowler/providers/azure/services/app/app_register_with_identity/app_register_with_identity.py @@ -7,18 +7,21 @@ class app_register_with_identity(Check): findings = [] for ( - subscription_name, + subscription_id, apps, ) in app_client.apps.items(): + subscription_name = app_client.subscriptions.get( + subscription_id, subscription_id + ) for app in apps.values(): report = Check_Report_Azure(metadata=self.metadata(), resource=app) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "PASS" - report.status_extended = f"App '{app.name}' in subscription '{subscription_name}' has an identity configured." + report.status_extended = f"App '{app.name}' in subscription '{subscription_name} ({subscription_id})' has an identity configured." if not app.identity: report.status = "FAIL" - report.status_extended = f"App '{app.name}' in subscription '{subscription_name}' does not have an identity configured." + report.status_extended = f"App '{app.name}' in subscription '{subscription_name} ({subscription_id})' does not have an identity configured." findings.append(report) diff --git a/prowler/providers/azure/services/app/app_service.py b/prowler/providers/azure/services/app/app_service.py index d70c51e778..201cd6a344 100644 --- a/prowler/providers/azure/services/app/app_service.py +++ b/prowler/providers/azure/services/app/app_service.py @@ -20,10 +20,10 @@ class App(AzureService): logger.info("App - Getting apps...") apps = {} - for subscription_name, client in self.clients.items(): + for subscription_id, client in self.clients.items(): try: apps_list = client.web_apps.list() - apps.update({subscription_name: {}}) + apps.update({subscription_id: {}}) for app in apps_list: # Filter function apps @@ -41,7 +41,7 @@ class App(AzureService): resource_group_name=app.resource_group, name=app.name ) - apps[subscription_name].update( + apps[subscription_id].update( { app.id: WebApp( resource_id=app.id, @@ -81,7 +81,7 @@ class App(AzureService): getattr(app, "client_cert_mode", "Ignore"), ), monitor_diagnostic_settings=self._get_app_monitor_settings( - app.name, app.resource_group, subscription_name + app.name, app.resource_group, subscription_id ), https_only=getattr(app, "https_only", False), identity=ManagedServiceIdentity( @@ -106,7 +106,7 @@ class App(AzureService): ) except Exception as error: logger.error( - f"Subscription name: {subscription_name} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription_id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return apps @@ -115,17 +115,17 @@ class App(AzureService): logger.info("Function - Getting functions...") functions = {} - for subscription_name, client in self.clients.items(): + for subscription_id, client in self.clients.items(): try: functions_list = client.web_apps.list() - functions.update({subscription_name: {}}) + functions.update({subscription_id: {}}) for function in functions_list: # Filter function apps if getattr(function, "kind", "").startswith("functionapp"): # List host keys host_keys = self._get_function_host_keys( - subscription_name, function.resource_group, function.name + subscription_id, function.resource_group, function.name ) if host_keys is not None: function_keys = getattr(host_keys, "function_keys", {}) @@ -133,16 +133,16 @@ class App(AzureService): function_keys = None application_settings = self._list_application_settings( - subscription_name, function.resource_group, function.name + subscription_id, function.resource_group, function.name ) function_config = self._get_function_config( - subscription_name, + subscription_id, function.resource_group, function.name, ) - functions[subscription_name].update( + functions[subscription_id].update( { function.id: FunctionApp( id=function.id, @@ -175,7 +175,7 @@ class App(AzureService): ) except Exception as error: logger.error( - f"Subscription name: {subscription_name} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription_id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return functions @@ -200,13 +200,13 @@ class App(AzureService): monitor_diagnostics_settings = [] try: monitor_diagnostics_settings = monitor_client.diagnostic_settings_with_uri( - self.subscriptions[subscription], - f"subscriptions/{self.subscriptions[subscription]}/resourceGroups/{resource_group}/providers/Microsoft.Web/sites/{app_name}", + subscription, + f"subscriptions/{subscription}/resourceGroups/{resource_group}/providers/Microsoft.Web/sites/{app_name}", monitor_client.clients[subscription], ) except Exception as error: logger.error( - f"Subscription name: {self.subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {self.subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return monitor_diagnostics_settings diff --git a/prowler/providers/azure/services/appinsights/appinsights_ensure_is_configured/appinsights_ensure_is_configured.py b/prowler/providers/azure/services/appinsights/appinsights_ensure_is_configured/appinsights_ensure_is_configured.py index c7761c115e..d803c2ea18 100644 --- a/prowler/providers/azure/services/appinsights/appinsights_ensure_is_configured/appinsights_ensure_is_configured.py +++ b/prowler/providers/azure/services/appinsights/appinsights_ensure_is_configured/appinsights_ensure_is_configured.py @@ -8,19 +8,20 @@ class appinsights_ensure_is_configured(Check): def execute(self) -> Check_Report_Azure: findings = [] - for subscription_name, components in appinsights_client.components.items(): + for subscription_id, components in appinsights_client.components.items(): + subscription_name = appinsights_client.subscriptions.get( + subscription_id, subscription_id + ) report = Check_Report_Azure(metadata=self.metadata(), resource={}) report.status = "PASS" - report.subscription = subscription_name - report.resource_name = subscription_name - report.resource_id = ( - f"/subscriptions/{appinsights_client.subscriptions[subscription_name]}" - ) - report.status_extended = f"There is at least one AppInsight configured in subscription {subscription_name}." + report.subscription = subscription_id + report.resource_name = subscription_id + report.resource_id = f"/subscriptions/{subscription_id}" + report.status_extended = f"There is at least one AppInsight configured in subscription {subscription_name} ({subscription_id})." if len(components) < 1: report.status = "FAIL" - report.status_extended = f"There are no AppInsight configured in subscription {subscription_name}." + report.status_extended = f"There are no AppInsight configured in subscription {subscription_name} ({subscription_id})." findings.append(report) diff --git a/prowler/providers/azure/services/appinsights/appinsights_service.py b/prowler/providers/azure/services/appinsights/appinsights_service.py index aae9dbf9b0..918a0f1b0f 100644 --- a/prowler/providers/azure/services/appinsights/appinsights_service.py +++ b/prowler/providers/azure/services/appinsights/appinsights_service.py @@ -15,13 +15,13 @@ class AppInsights(AzureService): logger.info("AppInsights - Getting components...") components = {} - for subscription_name, client in self.clients.items(): + for subscription_id, client in self.clients.items(): try: components_list = client.components.list() - components.update({subscription_name: {}}) + components.update({subscription_id: {}}) for component in components_list: - components[subscription_name].update( + components[subscription_id].update( { component.app_id: Component( resource_id=component.id, @@ -35,7 +35,7 @@ class AppInsights(AzureService): ) except Exception as error: logger.error( - f"Subscription name: {subscription_name} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription_id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return components diff --git a/prowler/providers/azure/services/containerregistry/containerregistry_admin_user_disabled/containerregistry_admin_user_disabled.py b/prowler/providers/azure/services/containerregistry/containerregistry_admin_user_disabled/containerregistry_admin_user_disabled.py index 05cd0b1d6d..368dc3535e 100644 --- a/prowler/providers/azure/services/containerregistry/containerregistry_admin_user_disabled/containerregistry_admin_user_disabled.py +++ b/prowler/providers/azure/services/containerregistry/containerregistry_admin_user_disabled/containerregistry_admin_user_disabled.py @@ -9,17 +9,20 @@ class containerregistry_admin_user_disabled(Check): findings = [] for subscription, registries in containerregistry_client.registries.items(): + subscription_name = containerregistry_client.subscriptions.get( + subscription, subscription + ) for container_registry_info in registries.values(): report = Check_Report_Azure( metadata=self.metadata(), resource=container_registry_info ) report.subscription = subscription report.status = "FAIL" - report.status_extended = f"Container Registry {container_registry_info.name} from subscription {subscription} has its admin user enabled." + report.status_extended = f"Container Registry {container_registry_info.name} from subscription {subscription_name} ({subscription}) has its admin user enabled." if not container_registry_info.admin_user_enabled: report.status = "PASS" - report.status_extended = f"Container Registry {container_registry_info.name} from subscription {subscription} has its admin user disabled." + report.status_extended = f"Container Registry {container_registry_info.name} from subscription {subscription_name} ({subscription}) has its admin user disabled." findings.append(report) diff --git a/prowler/providers/azure/services/containerregistry/containerregistry_not_publicly_accessible/containerregistry_not_publicly_accessible.py b/prowler/providers/azure/services/containerregistry/containerregistry_not_publicly_accessible/containerregistry_not_publicly_accessible.py index e6401af404..707be1aafb 100644 --- a/prowler/providers/azure/services/containerregistry/containerregistry_not_publicly_accessible/containerregistry_not_publicly_accessible.py +++ b/prowler/providers/azure/services/containerregistry/containerregistry_not_publicly_accessible/containerregistry_not_publicly_accessible.py @@ -9,17 +9,20 @@ class containerregistry_not_publicly_accessible(Check): findings = [] for subscription, registries in containerregistry_client.registries.items(): + subscription_name = containerregistry_client.subscriptions.get( + subscription, subscription + ) for container_registry_info in registries.values(): report = Check_Report_Azure( metadata=self.metadata(), resource=container_registry_info ) report.subscription = subscription report.status = "FAIL" - report.status_extended = f"Container Registry {container_registry_info.name} from subscription {subscription} allows unrestricted network access." + report.status_extended = f"Container Registry {container_registry_info.name} from subscription {subscription_name} ({subscription}) allows unrestricted network access." if not container_registry_info.public_network_access: report.status = "PASS" - report.status_extended = f"Container Registry {container_registry_info.name} from subscription {subscription} does not allow unrestricted network access." + report.status_extended = f"Container Registry {container_registry_info.name} from subscription {subscription_name} ({subscription}) does not allow unrestricted network access." findings.append(report) diff --git a/prowler/providers/azure/services/containerregistry/containerregistry_service.py b/prowler/providers/azure/services/containerregistry/containerregistry_service.py index e0004429f0..ee6cce39f2 100644 --- a/prowler/providers/azure/services/containerregistry/containerregistry_service.py +++ b/prowler/providers/azure/services/containerregistry/containerregistry_service.py @@ -64,7 +64,7 @@ class ContainerRegistry(AzureService): ) except Exception as error: logger.error( - f"Subscription name: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return registries @@ -81,13 +81,13 @@ class ContainerRegistry(AzureService): monitor_diagnostics_settings = [] try: monitor_diagnostics_settings = monitor_client.diagnostic_settings_with_uri( - self.subscriptions[subscription], - f"subscriptions/{self.subscriptions[subscription]}/resourceGroups/{resource_group}/providers/Microsoft.ContainerRegistry/registries/{registry_name}", + subscription, + f"subscriptions/{subscription}/resourceGroups/{resource_group}/providers/Microsoft.ContainerRegistry/registries/{registry_name}", monitor_client.clients[subscription], ) except Exception as error: logger.error( - f"Subscription name: {self.subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {self.subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return monitor_diagnostics_settings diff --git a/prowler/providers/azure/services/containerregistry/containerregistry_uses_private_link/containerregistry_uses_private_link.py b/prowler/providers/azure/services/containerregistry/containerregistry_uses_private_link/containerregistry_uses_private_link.py index 5962a34c77..e9e6c324ad 100644 --- a/prowler/providers/azure/services/containerregistry/containerregistry_uses_private_link/containerregistry_uses_private_link.py +++ b/prowler/providers/azure/services/containerregistry/containerregistry_uses_private_link/containerregistry_uses_private_link.py @@ -9,17 +9,20 @@ class containerregistry_uses_private_link(Check): findings = [] for subscription, registries in containerregistry_client.registries.items(): + subscription_name = containerregistry_client.subscriptions.get( + subscription, subscription + ) for container_registry_info in registries.values(): report = Check_Report_Azure( metadata=self.metadata(), resource=container_registry_info ) report.subscription = subscription report.status = "FAIL" - report.status_extended = f"Container Registry {container_registry_info.name} from subscription {subscription} does not use a private link." + report.status_extended = f"Container Registry {container_registry_info.name} from subscription {subscription_name} ({subscription}) does not use a private link." if container_registry_info.private_endpoint_connections: report.status = "PASS" - report.status_extended = f"Container Registry {container_registry_info.name} from subscription {subscription} uses a private link." + report.status_extended = f"Container Registry {container_registry_info.name} from subscription {subscription_name} ({subscription}) uses a private link." findings.append(report) diff --git a/prowler/providers/azure/services/cosmosdb/cosmosdb_account_firewall_use_selected_networks/cosmosdb_account_firewall_use_selected_networks.py b/prowler/providers/azure/services/cosmosdb/cosmosdb_account_firewall_use_selected_networks/cosmosdb_account_firewall_use_selected_networks.py index d664fbfa3a..69d8bff663 100644 --- a/prowler/providers/azure/services/cosmosdb/cosmosdb_account_firewall_use_selected_networks/cosmosdb_account_firewall_use_selected_networks.py +++ b/prowler/providers/azure/services/cosmosdb/cosmosdb_account_firewall_use_selected_networks/cosmosdb_account_firewall_use_selected_networks.py @@ -6,14 +6,17 @@ class cosmosdb_account_firewall_use_selected_networks(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, accounts in cosmosdb_client.accounts.items(): + subscription_name = cosmosdb_client.subscriptions.get( + subscription, subscription + ) for account in accounts: report = Check_Report_Azure(metadata=self.metadata(), resource=account) report.subscription = subscription report.status = "FAIL" - report.status_extended = f"CosmosDB account {account.name} from subscription {subscription} has firewall rules that allow access from all networks." + report.status_extended = f"CosmosDB account {account.name} from subscription {subscription_name} ({subscription}) has firewall rules that allow access from all networks." if account.is_virtual_network_filter_enabled: report.status = "PASS" - report.status_extended = f"CosmosDB account {account.name} from subscription {subscription} has firewall rules that allow access only from selected networks." + report.status_extended = f"CosmosDB account {account.name} from subscription {subscription_name} ({subscription}) has firewall rules that allow access only from selected networks." findings.append(report) return findings diff --git a/prowler/providers/azure/services/cosmosdb/cosmosdb_account_use_aad_and_rbac/cosmosdb_account_use_aad_and_rbac.py b/prowler/providers/azure/services/cosmosdb/cosmosdb_account_use_aad_and_rbac/cosmosdb_account_use_aad_and_rbac.py index b521792256..acf77e240c 100644 --- a/prowler/providers/azure/services/cosmosdb/cosmosdb_account_use_aad_and_rbac/cosmosdb_account_use_aad_and_rbac.py +++ b/prowler/providers/azure/services/cosmosdb/cosmosdb_account_use_aad_and_rbac/cosmosdb_account_use_aad_and_rbac.py @@ -6,14 +6,17 @@ class cosmosdb_account_use_aad_and_rbac(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, accounts in cosmosdb_client.accounts.items(): + subscription_name = cosmosdb_client.subscriptions.get( + subscription, subscription + ) for account in accounts: report = Check_Report_Azure(metadata=self.metadata(), resource=account) report.subscription = subscription report.status = "FAIL" - report.status_extended = f"CosmosDB account {account.name} from subscription {subscription} is not using AAD and RBAC" + report.status_extended = f"CosmosDB account {account.name} from subscription {subscription_name} ({subscription}) is not using AAD and RBAC" if account.disable_local_auth: report.status = "PASS" - report.status_extended = f"CosmosDB account {account.name} from subscription {subscription} is using AAD and RBAC" + report.status_extended = f"CosmosDB account {account.name} from subscription {subscription_name} ({subscription}) is using AAD and RBAC" findings.append(report) return findings diff --git a/prowler/providers/azure/services/cosmosdb/cosmosdb_account_use_private_endpoints/cosmosdb_account_use_private_endpoints.py b/prowler/providers/azure/services/cosmosdb/cosmosdb_account_use_private_endpoints/cosmosdb_account_use_private_endpoints.py index 8229801134..d54abca891 100644 --- a/prowler/providers/azure/services/cosmosdb/cosmosdb_account_use_private_endpoints/cosmosdb_account_use_private_endpoints.py +++ b/prowler/providers/azure/services/cosmosdb/cosmosdb_account_use_private_endpoints/cosmosdb_account_use_private_endpoints.py @@ -6,14 +6,17 @@ class cosmosdb_account_use_private_endpoints(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, accounts in cosmosdb_client.accounts.items(): + subscription_name = cosmosdb_client.subscriptions.get( + subscription, subscription + ) for account in accounts: report = Check_Report_Azure(metadata=self.metadata(), resource=account) report.subscription = subscription report.status = "FAIL" - report.status_extended = f"CosmosDB account {account.name} from subscription {subscription} is not using private endpoints connections" + report.status_extended = f"CosmosDB account {account.name} from subscription {subscription_name} ({subscription}) is not using private endpoints connections" if account.private_endpoint_connections: report.status = "PASS" - report.status_extended = f"CosmosDB account {account.name} from subscription {subscription} is using private endpoints connections" + report.status_extended = f"CosmosDB account {account.name} from subscription {subscription_name} ({subscription}) is using private endpoints connections" findings.append(report) return findings diff --git a/prowler/providers/azure/services/cosmosdb/cosmosdb_service.py b/prowler/providers/azure/services/cosmosdb/cosmosdb_service.py index 2d229bc060..c36af1d2e9 100644 --- a/prowler/providers/azure/services/cosmosdb/cosmosdb_service.py +++ b/prowler/providers/azure/services/cosmosdb/cosmosdb_service.py @@ -48,7 +48,7 @@ class CosmosDB(AzureService): ) except Exception as error: logger.error( - f"Subscription name: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return accounts diff --git a/prowler/providers/azure/services/databricks/databricks_workspace_cmk_encryption_enabled/databricks_workspace_cmk_encryption_enabled.py b/prowler/providers/azure/services/databricks/databricks_workspace_cmk_encryption_enabled/databricks_workspace_cmk_encryption_enabled.py index a8e366f15a..ec40b94c10 100644 --- a/prowler/providers/azure/services/databricks/databricks_workspace_cmk_encryption_enabled/databricks_workspace_cmk_encryption_enabled.py +++ b/prowler/providers/azure/services/databricks/databricks_workspace_cmk_encryption_enabled/databricks_workspace_cmk_encryption_enabled.py @@ -17,6 +17,9 @@ class databricks_workspace_cmk_encryption_enabled(Check): def execute(self): findings = [] for subscription, workspaces in databricks_client.workspaces.items(): + subscription_name = databricks_client.subscriptions.get( + subscription, subscription + ) for workspace in workspaces.values(): report = Check_Report_Azure( metadata=self.metadata(), resource=workspace @@ -25,9 +28,9 @@ class databricks_workspace_cmk_encryption_enabled(Check): enc = workspace.managed_disk_encryption if enc: report.status = "PASS" - report.status_extended = f"Databricks workspace {workspace.name} in subscription {subscription} has customer-managed key (CMK) encryption enabled with key {enc.key_vault_uri}/{enc.key_name}/{enc.key_version}." + report.status_extended = f"Databricks workspace {workspace.name} in subscription {subscription_name} ({subscription}) has customer-managed key (CMK) encryption enabled with key {enc.key_vault_uri}/{enc.key_name}/{enc.key_version}." else: report.status = "FAIL" - report.status_extended = f"Databricks workspace {workspace.name} in subscription {subscription} does not have customer-managed key (CMK) encryption enabled." + report.status_extended = f"Databricks workspace {workspace.name} in subscription {subscription_name} ({subscription}) does not have customer-managed key (CMK) encryption enabled." findings.append(report) return findings diff --git a/prowler/providers/azure/services/databricks/databricks_workspace_vnet_injection_enabled/databricks_workspace_vnet_injection_enabled.py b/prowler/providers/azure/services/databricks/databricks_workspace_vnet_injection_enabled/databricks_workspace_vnet_injection_enabled.py index f667342dab..7092536b7d 100644 --- a/prowler/providers/azure/services/databricks/databricks_workspace_vnet_injection_enabled/databricks_workspace_vnet_injection_enabled.py +++ b/prowler/providers/azure/services/databricks/databricks_workspace_vnet_injection_enabled/databricks_workspace_vnet_injection_enabled.py @@ -17,6 +17,9 @@ class databricks_workspace_vnet_injection_enabled(Check): def execute(self): findings = [] for subscription, workspaces in databricks_client.workspaces.items(): + subscription_name = databricks_client.subscriptions.get( + subscription, subscription + ) for workspace in workspaces.values(): report = Check_Report_Azure( metadata=self.metadata(), resource=workspace @@ -24,9 +27,9 @@ class databricks_workspace_vnet_injection_enabled(Check): report.subscription = subscription if workspace.custom_managed_vnet_id: report.status = "PASS" - report.status_extended = f"Databricks workspace {workspace.name} in subscription {subscription} is deployed in a customer-managed VNet ({workspace.custom_managed_vnet_id})." + report.status_extended = f"Databricks workspace {workspace.name} in subscription {subscription_name} ({subscription}) is deployed in a customer-managed VNet ({workspace.custom_managed_vnet_id})." else: report.status = "FAIL" - report.status_extended = f"Databricks workspace {workspace.name} in subscription {subscription} is not deployed in a customer-managed VNet (VNet Injection is not enabled)." + report.status_extended = f"Databricks workspace {workspace.name} in subscription {subscription_name} ({subscription}) is not deployed in a customer-managed VNet (VNet Injection is not enabled)." findings.append(report) return findings diff --git a/prowler/providers/azure/services/defender/defender_additional_email_configured_with_a_security_contact/defender_additional_email_configured_with_a_security_contact.py b/prowler/providers/azure/services/defender/defender_additional_email_configured_with_a_security_contact/defender_additional_email_configured_with_a_security_contact.py index 06cb1f06d2..8a8013a261 100644 --- a/prowler/providers/azure/services/defender/defender_additional_email_configured_with_a_security_contact/defender_additional_email_configured_with_a_security_contact.py +++ b/prowler/providers/azure/services/defender/defender_additional_email_configured_with_a_security_contact/defender_additional_email_configured_with_a_security_contact.py @@ -7,9 +7,12 @@ class defender_additional_email_configured_with_a_security_contact(Check): findings = [] for ( - subscription_name, + subscription_id, security_contact_configurations, ) in defender_client.security_contact_configurations.items(): + subscription_name = defender_client.subscriptions.get( + subscription_id, subscription_id + ) for contact_configuration in security_contact_configurations.values(): report = Check_Report_Azure( metadata=self.metadata(), resource=contact_configuration @@ -19,14 +22,14 @@ class defender_additional_email_configured_with_a_security_contact(Check): if contact_configuration.name else "Security Contact" ) - report.subscription = subscription_name + report.subscription = subscription_id if len(contact_configuration.emails) > 0: report.status = "PASS" - report.status_extended = f"There is another correct email configured for subscription {subscription_name}." + report.status_extended = f"There is another correct email configured for subscription {subscription_name} ({subscription_id})." else: report.status = "FAIL" - report.status_extended = f"There is not another correct email configured for subscription {subscription_name}." + report.status_extended = f"There is not another correct email configured for subscription {subscription_name} ({subscription_id})." findings.append(report) diff --git a/prowler/providers/azure/services/defender/defender_assessments_vm_endpoint_protection_installed/defender_assessments_vm_endpoint_protection_installed.py b/prowler/providers/azure/services/defender/defender_assessments_vm_endpoint_protection_installed/defender_assessments_vm_endpoint_protection_installed.py index 1e24f4918a..d06447bdf8 100644 --- a/prowler/providers/azure/services/defender/defender_assessments_vm_endpoint_protection_installed/defender_assessments_vm_endpoint_protection_installed.py +++ b/prowler/providers/azure/services/defender/defender_assessments_vm_endpoint_protection_installed/defender_assessments_vm_endpoint_protection_installed.py @@ -7,9 +7,12 @@ class defender_assessments_vm_endpoint_protection_installed(Check): findings = [] for ( - subscription_name, + subscription_id, assessments, ) in defender_client.assessments.items(): + subscription_name = defender_client.subscriptions.get( + subscription_id, subscription_id + ) if ( "Install endpoint protection solution on virtual machines" in assessments @@ -20,9 +23,9 @@ class defender_assessments_vm_endpoint_protection_installed(Check): "Install endpoint protection solution on virtual machines" ], ) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "PASS" - report.status_extended = f"Endpoint protection is set up in all VMs in subscription {subscription_name}." + report.status_extended = f"Endpoint protection is set up in all VMs in subscription {subscription_name} ({subscription_id})." if ( assessments[ @@ -31,7 +34,7 @@ class defender_assessments_vm_endpoint_protection_installed(Check): == "Unhealthy" ): report.status = "FAIL" - report.status_extended = f"Endpoint protection is not set up in all VMs in subscription {subscription_name}." + report.status_extended = f"Endpoint protection is not set up in all VMs in subscription {subscription_name} ({subscription_id})." findings.append(report) diff --git a/prowler/providers/azure/services/defender/defender_attack_path_notifications_properly_configured/defender_attack_path_notifications_properly_configured.py b/prowler/providers/azure/services/defender/defender_attack_path_notifications_properly_configured/defender_attack_path_notifications_properly_configured.py index 8a9935457d..47d2a76cef 100644 --- a/prowler/providers/azure/services/defender/defender_attack_path_notifications_properly_configured/defender_attack_path_notifications_properly_configured.py +++ b/prowler/providers/azure/services/defender/defender_attack_path_notifications_properly_configured/defender_attack_path_notifications_properly_configured.py @@ -24,9 +24,12 @@ class defender_attack_path_notifications_properly_configured(Check): min_risk_index = risk_levels.index(min_risk_level) for ( - subscription_name, + subscription_id, security_contact_configurations, ) in defender_client.security_contact_configurations.items(): + subscription_name = defender_client.subscriptions.get( + subscription_id, subscription_id + ) for contact_configuration in security_contact_configurations.values(): report = Check_Report_Azure( metadata=self.metadata(), resource=contact_configuration @@ -36,21 +39,21 @@ class defender_attack_path_notifications_properly_configured(Check): if contact_configuration.name else "Security Contact" ) - report.subscription = subscription_name + report.subscription = subscription_id actual_risk_level = getattr( contact_configuration, "attack_path_minimal_risk_level", None ) if not actual_risk_level or actual_risk_level not in risk_levels: report.status = "FAIL" - report.status_extended = f"Attack path notifications are not enabled in subscription {subscription_name} for security contact {contact_configuration.name}." + report.status_extended = f"Attack path notifications are not enabled in subscription {subscription_name} ({subscription_id}) for security contact {contact_configuration.name}." else: actual_risk_index = risk_levels.index(actual_risk_level) if actual_risk_index <= min_risk_index: report.status = "PASS" - report.status_extended = f"Attack path notifications are enabled with minimal risk level {actual_risk_level} in subscription {subscription_name} for security contact {contact_configuration.name}." + report.status_extended = f"Attack path notifications are enabled with minimal risk level {actual_risk_level} in subscription {subscription_name} ({subscription_id}) for security contact {contact_configuration.name}." else: report.status = "FAIL" - report.status_extended = f"Attack path notifications are enabled with minimal risk level {actual_risk_level} in subscription {subscription_name} for security contact {contact_configuration.name}." + report.status_extended = f"Attack path notifications are enabled with minimal risk level {actual_risk_level} in subscription {subscription_name} ({subscription_id}) for security contact {contact_configuration.name}." findings.append(report) return findings diff --git a/prowler/providers/azure/services/defender/defender_auto_provisioning_log_analytics_agent_vms_on/defender_auto_provisioning_log_analytics_agent_vms_on.py b/prowler/providers/azure/services/defender/defender_auto_provisioning_log_analytics_agent_vms_on/defender_auto_provisioning_log_analytics_agent_vms_on.py index 5a4121a511..0c1162ace5 100644 --- a/prowler/providers/azure/services/defender/defender_auto_provisioning_log_analytics_agent_vms_on/defender_auto_provisioning_log_analytics_agent_vms_on.py +++ b/prowler/providers/azure/services/defender/defender_auto_provisioning_log_analytics_agent_vms_on/defender_auto_provisioning_log_analytics_agent_vms_on.py @@ -7,21 +7,24 @@ class defender_auto_provisioning_log_analytics_agent_vms_on(Check): findings = [] for ( - subscription_name, + subscription_id, auto_provisioning_settings, ) in defender_client.auto_provisioning_settings.items(): + subscription_name = defender_client.subscriptions.get( + subscription_id, subscription_id + ) for auto_provisioning_setting in auto_provisioning_settings.values(): report = Check_Report_Azure( metadata=self.metadata(), resource=auto_provisioning_setting, ) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "PASS" - report.status_extended = f"Defender Auto Provisioning Log Analytics Agents from subscription {subscription_name} is set to ON." + report.status_extended = f"Defender Auto Provisioning Log Analytics Agents from subscription {subscription_name} ({subscription_id}) is set to ON." if auto_provisioning_setting.auto_provision != "On": report.status = "FAIL" - report.status_extended = f"Defender Auto Provisioning Log Analytics Agents from subscription {subscription_name} is set to OFF." + report.status_extended = f"Defender Auto Provisioning Log Analytics Agents from subscription {subscription_name} ({subscription_id}) is set to OFF." findings.append(report) diff --git a/prowler/providers/azure/services/defender/defender_auto_provisioning_vulnerabilty_assessments_machines_on/defender_auto_provisioning_vulnerabilty_assessments_machines_on.py b/prowler/providers/azure/services/defender/defender_auto_provisioning_vulnerabilty_assessments_machines_on/defender_auto_provisioning_vulnerabilty_assessments_machines_on.py index f3bb4dbc25..90404e39e4 100644 --- a/prowler/providers/azure/services/defender/defender_auto_provisioning_vulnerabilty_assessments_machines_on/defender_auto_provisioning_vulnerabilty_assessments_machines_on.py +++ b/prowler/providers/azure/services/defender/defender_auto_provisioning_vulnerabilty_assessments_machines_on/defender_auto_provisioning_vulnerabilty_assessments_machines_on.py @@ -7,9 +7,12 @@ class defender_auto_provisioning_vulnerabilty_assessments_machines_on(Check): findings = [] for ( - subscription_name, + subscription_id, assessments, ) in defender_client.assessments.items(): + subscription_name = defender_client.subscriptions.get( + subscription_id, subscription_id + ) if ( "Machines should have a vulnerability assessment solution" in assessments @@ -20,9 +23,9 @@ class defender_auto_provisioning_vulnerabilty_assessments_machines_on(Check): "Machines should have a vulnerability assessment solution" ], ) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "PASS" - report.status_extended = f"Vulnerability assessment is set up in all VMs in subscription {subscription_name}." + report.status_extended = f"Vulnerability assessment is set up in all VMs in subscription {subscription_name} ({subscription_id})." if ( assessments[ @@ -31,7 +34,7 @@ class defender_auto_provisioning_vulnerabilty_assessments_machines_on(Check): == "Unhealthy" ): report.status = "FAIL" - report.status_extended = f"Vulnerability assessment is not set up in all VMs in subscription {subscription_name}." + report.status_extended = f"Vulnerability assessment is not set up in all VMs in subscription {subscription_name} ({subscription_id})." findings.append(report) diff --git a/prowler/providers/azure/services/defender/defender_container_images_resolved_vulnerabilities/defender_container_images_resolved_vulnerabilities.py b/prowler/providers/azure/services/defender/defender_container_images_resolved_vulnerabilities/defender_container_images_resolved_vulnerabilities.py index 51dd1e3648..56a5e10752 100644 --- a/prowler/providers/azure/services/defender/defender_container_images_resolved_vulnerabilities/defender_container_images_resolved_vulnerabilities.py +++ b/prowler/providers/azure/services/defender/defender_container_images_resolved_vulnerabilities/defender_container_images_resolved_vulnerabilities.py @@ -7,9 +7,12 @@ class defender_container_images_resolved_vulnerabilities(Check): findings = [] for ( - subscription_name, + subscription_id, assessments, ) in defender_client.assessments.items(): + subscription_name = defender_client.subscriptions.get( + subscription_id, subscription_id + ) if ( "Azure running container images should have vulnerabilities resolved (powered by Microsoft Defender Vulnerability Management)" in assessments @@ -28,9 +31,9 @@ class defender_container_images_resolved_vulnerabilities(Check): "Azure running container images should have vulnerabilities resolved (powered by Microsoft Defender Vulnerability Management)" ], ) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "PASS" - report.status_extended = f"Azure running container images do not have unresolved vulnerabilities in subscription '{subscription_name}'." + report.status_extended = f"Azure running container images do not have unresolved vulnerabilities in subscription '{subscription_name} ({subscription_id})'." if ( assessments[ "Azure running container images should have vulnerabilities resolved (powered by Microsoft Defender Vulnerability Management)" @@ -38,7 +41,7 @@ class defender_container_images_resolved_vulnerabilities(Check): == "Unhealthy" ): report.status = "FAIL" - report.status_extended = f"Azure running container images have unresolved vulnerabilities in subscription '{subscription_name}'." + report.status_extended = f"Azure running container images have unresolved vulnerabilities in subscription '{subscription_name} ({subscription_id})'." findings.append(report) diff --git a/prowler/providers/azure/services/defender/defender_container_images_scan_enabled/defender_container_images_scan_enabled.py b/prowler/providers/azure/services/defender/defender_container_images_scan_enabled/defender_container_images_scan_enabled.py index 06b067d1a4..2cb5d1974e 100644 --- a/prowler/providers/azure/services/defender/defender_container_images_scan_enabled/defender_container_images_scan_enabled.py +++ b/prowler/providers/azure/services/defender/defender_container_images_scan_enabled/defender_container_images_scan_enabled.py @@ -6,20 +6,21 @@ class defender_container_images_scan_enabled(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, pricings in defender_client.pricings.items(): + subscription_name = defender_client.subscriptions.get( + subscription, subscription + ) if "Containers" in pricings: report = Check_Report_Azure( metadata=self.metadata(), resource=pricings["Containers"] ) report.subscription = subscription report.status = "PASS" - report.status_extended = ( - f"Container image scan is enabled in subscription {subscription}." - ) + report.status_extended = f"Container image scan is enabled in subscription {subscription_name} ({subscription})." if not pricings["Containers"].extensions.get( "ContainerRegistriesVulnerabilityAssessments" ): report.status = "FAIL" - report.status_extended = f"Container image scan is disabled in subscription {subscription}." + report.status_extended = f"Container image scan is disabled in subscription {subscription_name} ({subscription})." findings.append(report) return findings diff --git a/prowler/providers/azure/services/defender/defender_ensure_defender_for_app_services_is_on/defender_ensure_defender_for_app_services_is_on.py b/prowler/providers/azure/services/defender/defender_ensure_defender_for_app_services_is_on/defender_ensure_defender_for_app_services_is_on.py index cd30f2e5dd..e7362491bf 100644 --- a/prowler/providers/azure/services/defender/defender_ensure_defender_for_app_services_is_on/defender_ensure_defender_for_app_services_is_on.py +++ b/prowler/providers/azure/services/defender/defender_ensure_defender_for_app_services_is_on/defender_ensure_defender_for_app_services_is_on.py @@ -6,6 +6,9 @@ class defender_ensure_defender_for_app_services_is_on(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, pricings in defender_client.pricings.items(): + subscription_name = defender_client.subscriptions.get( + subscription, subscription + ) if "AppServices" in pricings: report = Check_Report_Azure( metadata=self.metadata(), resource=pricings["AppServices"] @@ -13,10 +16,10 @@ class defender_ensure_defender_for_app_services_is_on(Check): report.subscription = subscription report.resource_name = "Defender plan App Services" report.status = "PASS" - report.status_extended = f"Defender plan Defender for App Services from subscription {subscription} is set to ON (pricing tier standard)." + report.status_extended = f"Defender plan Defender for App Services from subscription {subscription_name} ({subscription}) is set to ON (pricing tier standard)." if pricings["AppServices"].pricing_tier != "Standard": report.status = "FAIL" - report.status_extended = f"Defender plan Defender for App Services from subscription {subscription} is set to OFF (pricing tier not standard)." + report.status_extended = f"Defender plan Defender for App Services from subscription {subscription_name} ({subscription}) is set to OFF (pricing tier not standard)." findings.append(report) return findings diff --git a/prowler/providers/azure/services/defender/defender_ensure_defender_for_arm_is_on/defender_ensure_defender_for_arm_is_on.py b/prowler/providers/azure/services/defender/defender_ensure_defender_for_arm_is_on/defender_ensure_defender_for_arm_is_on.py index c05102b351..f759967661 100644 --- a/prowler/providers/azure/services/defender/defender_ensure_defender_for_arm_is_on/defender_ensure_defender_for_arm_is_on.py +++ b/prowler/providers/azure/services/defender/defender_ensure_defender_for_arm_is_on/defender_ensure_defender_for_arm_is_on.py @@ -6,6 +6,9 @@ class defender_ensure_defender_for_arm_is_on(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, pricings in defender_client.pricings.items(): + subscription_name = defender_client.subscriptions.get( + subscription, subscription + ) if "Arm" in pricings: report = Check_Report_Azure( metadata=self.metadata(), resource=pricings["Arm"] @@ -13,10 +16,10 @@ class defender_ensure_defender_for_arm_is_on(Check): report.subscription = subscription report.resource_name = "Defender plan ARM" report.status = "PASS" - report.status_extended = f"Defender plan Defender for ARM from subscription {subscription} is set to ON (pricing tier standard)." + report.status_extended = f"Defender plan Defender for ARM from subscription {subscription_name} ({subscription}) is set to ON (pricing tier standard)." if pricings["Arm"].pricing_tier != "Standard": report.status = "FAIL" - report.status_extended = f"Defender plan Defender for ARM from subscription {subscription} is set to OFF (pricing tier not standard)." + report.status_extended = f"Defender plan Defender for ARM from subscription {subscription_name} ({subscription}) is set to OFF (pricing tier not standard)." findings.append(report) return findings diff --git a/prowler/providers/azure/services/defender/defender_ensure_defender_for_azure_sql_databases_is_on/defender_ensure_defender_for_azure_sql_databases_is_on.py b/prowler/providers/azure/services/defender/defender_ensure_defender_for_azure_sql_databases_is_on/defender_ensure_defender_for_azure_sql_databases_is_on.py index 3b05ddd415..6aeb8d0ddd 100644 --- a/prowler/providers/azure/services/defender/defender_ensure_defender_for_azure_sql_databases_is_on/defender_ensure_defender_for_azure_sql_databases_is_on.py +++ b/prowler/providers/azure/services/defender/defender_ensure_defender_for_azure_sql_databases_is_on/defender_ensure_defender_for_azure_sql_databases_is_on.py @@ -6,16 +6,19 @@ class defender_ensure_defender_for_azure_sql_databases_is_on(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, pricings in defender_client.pricings.items(): + subscription_name = defender_client.subscriptions.get( + subscription, subscription + ) if "SqlServers" in pricings: report = Check_Report_Azure( metadata=self.metadata(), resource=pricings["SqlServers"] ) report.subscription = subscription report.status = "PASS" - report.status_extended = f"Defender plan Defender for Azure SQL DB Servers from subscription {subscription} is set to ON (pricing tier standard)." + report.status_extended = f"Defender plan Defender for Azure SQL DB Servers from subscription {subscription_name} ({subscription}) is set to ON (pricing tier standard)." if pricings["SqlServers"].pricing_tier != "Standard": report.status = "FAIL" - report.status_extended = f"Defender plan Defender for Azure SQL DB Servers from subscription {subscription} is set to OFF (pricing tier not standard)." + report.status_extended = f"Defender plan Defender for Azure SQL DB Servers from subscription {subscription_name} ({subscription}) is set to OFF (pricing tier not standard)." findings.append(report) return findings diff --git a/prowler/providers/azure/services/defender/defender_ensure_defender_for_containers_is_on/defender_ensure_defender_for_containers_is_on.py b/prowler/providers/azure/services/defender/defender_ensure_defender_for_containers_is_on/defender_ensure_defender_for_containers_is_on.py index 56a5ff32e3..00741c18f8 100644 --- a/prowler/providers/azure/services/defender/defender_ensure_defender_for_containers_is_on/defender_ensure_defender_for_containers_is_on.py +++ b/prowler/providers/azure/services/defender/defender_ensure_defender_for_containers_is_on/defender_ensure_defender_for_containers_is_on.py @@ -6,16 +6,19 @@ class defender_ensure_defender_for_containers_is_on(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, pricings in defender_client.pricings.items(): + subscription_name = defender_client.subscriptions.get( + subscription, subscription + ) if "Containers" in pricings: report = Check_Report_Azure( metadata=self.metadata(), resource=pricings["Containers"] ) report.subscription = subscription report.status = "PASS" - report.status_extended = f"Defender plan Defender for Containers from subscription {subscription} is set to ON (pricing tier standard)." + report.status_extended = f"Defender plan Defender for Containers from subscription {subscription_name} ({subscription}) is set to ON (pricing tier standard)." if pricings["Containers"].pricing_tier != "Standard": report.status = "FAIL" - report.status_extended = f"Defender plan Defender for Containers from subscription {subscription} is set to OFF (pricing tier not standard)." + report.status_extended = f"Defender plan Defender for Containers from subscription {subscription_name} ({subscription}) is set to OFF (pricing tier not standard)." findings.append(report) return findings diff --git a/prowler/providers/azure/services/defender/defender_ensure_defender_for_cosmosdb_is_on/defender_ensure_defender_for_cosmosdb_is_on.py b/prowler/providers/azure/services/defender/defender_ensure_defender_for_cosmosdb_is_on/defender_ensure_defender_for_cosmosdb_is_on.py index eba77eb94f..b709f00831 100644 --- a/prowler/providers/azure/services/defender/defender_ensure_defender_for_cosmosdb_is_on/defender_ensure_defender_for_cosmosdb_is_on.py +++ b/prowler/providers/azure/services/defender/defender_ensure_defender_for_cosmosdb_is_on/defender_ensure_defender_for_cosmosdb_is_on.py @@ -6,6 +6,9 @@ class defender_ensure_defender_for_cosmosdb_is_on(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, pricings in defender_client.pricings.items(): + subscription_name = defender_client.subscriptions.get( + subscription, subscription + ) if "CosmosDbs" in pricings: report = Check_Report_Azure( metadata=self.metadata(), resource=pricings["CosmosDbs"] @@ -13,10 +16,10 @@ class defender_ensure_defender_for_cosmosdb_is_on(Check): report.subscription = subscription report.resource_name = "Defender plan Cosmos DB" report.status = "PASS" - report.status_extended = f"Defender plan Defender for Cosmos DB from subscription {subscription} is set to ON (pricing tier standard)." + report.status_extended = f"Defender plan Defender for Cosmos DB from subscription {subscription_name} ({subscription}) is set to ON (pricing tier standard)." if pricings["CosmosDbs"].pricing_tier != "Standard": report.status = "FAIL" - report.status_extended = f"Defender plan Defender for Cosmos DB from subscription {subscription} is set to OFF (pricing tier not standard)." + report.status_extended = f"Defender plan Defender for Cosmos DB from subscription {subscription_name} ({subscription}) is set to OFF (pricing tier not standard)." findings.append(report) return findings diff --git a/prowler/providers/azure/services/defender/defender_ensure_defender_for_databases_is_on/defender_ensure_defender_for_databases_is_on.py b/prowler/providers/azure/services/defender/defender_ensure_defender_for_databases_is_on/defender_ensure_defender_for_databases_is_on.py index 21f43a7e13..45b991cbdb 100644 --- a/prowler/providers/azure/services/defender/defender_ensure_defender_for_databases_is_on/defender_ensure_defender_for_databases_is_on.py +++ b/prowler/providers/azure/services/defender/defender_ensure_defender_for_databases_is_on/defender_ensure_defender_for_databases_is_on.py @@ -6,6 +6,9 @@ class defender_ensure_defender_for_databases_is_on(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, pricings in defender_client.pricings.items(): + subscription_name = defender_client.subscriptions.get( + subscription, subscription + ) if ( "SqlServers" in pricings and "SqlServerVirtualMachines" in pricings @@ -17,7 +20,7 @@ class defender_ensure_defender_for_databases_is_on(Check): ) report.subscription = subscription report.status = "PASS" - report.status_extended = f"Defender plan Defender for Databases from subscription {subscription} is set to ON (pricing tier standard)." + report.status_extended = f"Defender plan Defender for Databases from subscription {subscription_name} ({subscription}) is set to ON (pricing tier standard)." if ( pricings["SqlServers"].pricing_tier != "Standard" or pricings["SqlServerVirtualMachines"].pricing_tier != "Standard" @@ -26,7 +29,7 @@ class defender_ensure_defender_for_databases_is_on(Check): or pricings["CosmosDbs"].pricing_tier != "Standard" ): report.status = "FAIL" - report.status_extended = f"Defender plan Defender for Databases from subscription {subscription} is set to OFF (pricing tier not standard)." + report.status_extended = f"Defender plan Defender for Databases from subscription {subscription_name} ({subscription}) is set to OFF (pricing tier not standard)." findings.append(report) return findings diff --git a/prowler/providers/azure/services/defender/defender_ensure_defender_for_dns_is_on/defender_ensure_defender_for_dns_is_on.py b/prowler/providers/azure/services/defender/defender_ensure_defender_for_dns_is_on/defender_ensure_defender_for_dns_is_on.py index e096e93bab..86fd78f554 100644 --- a/prowler/providers/azure/services/defender/defender_ensure_defender_for_dns_is_on/defender_ensure_defender_for_dns_is_on.py +++ b/prowler/providers/azure/services/defender/defender_ensure_defender_for_dns_is_on/defender_ensure_defender_for_dns_is_on.py @@ -6,6 +6,9 @@ class defender_ensure_defender_for_dns_is_on(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, pricings in defender_client.pricings.items(): + subscription_name = defender_client.subscriptions.get( + subscription, subscription + ) if "Dns" in pricings: report = Check_Report_Azure( metadata=self.metadata(), resource=pricings["Dns"] @@ -13,10 +16,10 @@ class defender_ensure_defender_for_dns_is_on(Check): report.subscription = subscription report.resource_name = "Defender plan DNS" report.status = "PASS" - report.status_extended = f"Defender plan Defender for DNS from subscription {subscription} is set to ON (pricing tier standard)." + report.status_extended = f"Defender plan Defender for DNS from subscription {subscription_name} ({subscription}) is set to ON (pricing tier standard)." if pricings["Dns"].pricing_tier != "Standard": report.status = "FAIL" - report.status_extended = f"Defender plan Defender for DNS from subscription {subscription} is set to OFF (pricing tier not standard)." + report.status_extended = f"Defender plan Defender for DNS from subscription {subscription_name} ({subscription}) is set to OFF (pricing tier not standard)." findings.append(report) return findings diff --git a/prowler/providers/azure/services/defender/defender_ensure_defender_for_keyvault_is_on/defender_ensure_defender_for_keyvault_is_on.py b/prowler/providers/azure/services/defender/defender_ensure_defender_for_keyvault_is_on/defender_ensure_defender_for_keyvault_is_on.py index 202e76b4b4..42bcb62ed4 100644 --- a/prowler/providers/azure/services/defender/defender_ensure_defender_for_keyvault_is_on/defender_ensure_defender_for_keyvault_is_on.py +++ b/prowler/providers/azure/services/defender/defender_ensure_defender_for_keyvault_is_on/defender_ensure_defender_for_keyvault_is_on.py @@ -6,6 +6,9 @@ class defender_ensure_defender_for_keyvault_is_on(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, pricings in defender_client.pricings.items(): + subscription_name = defender_client.subscriptions.get( + subscription, subscription + ) if "KeyVaults" in pricings: report = Check_Report_Azure( metadata=self.metadata(), resource=pricings["KeyVaults"] @@ -13,10 +16,10 @@ class defender_ensure_defender_for_keyvault_is_on(Check): report.subscription = subscription report.resource_name = "Defender plan KeyVaults" report.status = "PASS" - report.status_extended = f"Defender plan Defender for KeyVaults from subscription {subscription} is set to ON (pricing tier standard)." + report.status_extended = f"Defender plan Defender for KeyVaults from subscription {subscription_name} ({subscription}) is set to ON (pricing tier standard)." if pricings["KeyVaults"].pricing_tier != "Standard": report.status = "FAIL" - report.status_extended = f"Defender plan Defender for KeyVaults from subscription {subscription} is set to OFF (pricing tier not standard)." + report.status_extended = f"Defender plan Defender for KeyVaults from subscription {subscription_name} ({subscription}) is set to OFF (pricing tier not standard)." findings.append(report) return findings diff --git a/prowler/providers/azure/services/defender/defender_ensure_defender_for_os_relational_databases_is_on/defender_ensure_defender_for_os_relational_databases_is_on.py b/prowler/providers/azure/services/defender/defender_ensure_defender_for_os_relational_databases_is_on/defender_ensure_defender_for_os_relational_databases_is_on.py index 7497e9fc2a..187b1950f1 100644 --- a/prowler/providers/azure/services/defender/defender_ensure_defender_for_os_relational_databases_is_on/defender_ensure_defender_for_os_relational_databases_is_on.py +++ b/prowler/providers/azure/services/defender/defender_ensure_defender_for_os_relational_databases_is_on/defender_ensure_defender_for_os_relational_databases_is_on.py @@ -6,6 +6,9 @@ class defender_ensure_defender_for_os_relational_databases_is_on(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, pricings in defender_client.pricings.items(): + subscription_name = defender_client.subscriptions.get( + subscription, subscription + ) if "OpenSourceRelationalDatabases" in pricings: report = Check_Report_Azure( metadata=self.metadata(), @@ -14,10 +17,10 @@ class defender_ensure_defender_for_os_relational_databases_is_on(Check): report.subscription = subscription report.resource_name = "Defender plan Open-Source Relational Databases" report.status = "PASS" - report.status_extended = f"Defender plan Defender for Open-Source Relational Databases from subscription {subscription} is set to ON (pricing tier standard)." + report.status_extended = f"Defender plan Defender for Open-Source Relational Databases from subscription {subscription_name} ({subscription}) is set to ON (pricing tier standard)." if pricings["OpenSourceRelationalDatabases"].pricing_tier != "Standard": report.status = "FAIL" - report.status_extended = f"Defender plan Defender for Open-Source Relational Databases from subscription {subscription} is set to OFF (pricing tier not standard)." + report.status_extended = f"Defender plan Defender for Open-Source Relational Databases from subscription {subscription_name} ({subscription}) is set to OFF (pricing tier not standard)." findings.append(report) return findings diff --git a/prowler/providers/azure/services/defender/defender_ensure_defender_for_server_is_on/defender_ensure_defender_for_server_is_on.py b/prowler/providers/azure/services/defender/defender_ensure_defender_for_server_is_on/defender_ensure_defender_for_server_is_on.py index 54cf846b78..3c5afd49b9 100644 --- a/prowler/providers/azure/services/defender/defender_ensure_defender_for_server_is_on/defender_ensure_defender_for_server_is_on.py +++ b/prowler/providers/azure/services/defender/defender_ensure_defender_for_server_is_on/defender_ensure_defender_for_server_is_on.py @@ -6,6 +6,9 @@ class defender_ensure_defender_for_server_is_on(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, pricings in defender_client.pricings.items(): + subscription_name = defender_client.subscriptions.get( + subscription, subscription + ) if "VirtualMachines" in pricings: report = Check_Report_Azure( metadata=self.metadata(), @@ -14,10 +17,10 @@ class defender_ensure_defender_for_server_is_on(Check): report.subscription = subscription report.resource_name = "Defender plan Servers" report.status = "PASS" - report.status_extended = f"Defender plan Defender for Servers from subscription {subscription} is set to ON (pricing tier standard)." + report.status_extended = f"Defender plan Defender for Servers from subscription {subscription_name} ({subscription}) is set to ON (pricing tier standard)." if pricings["VirtualMachines"].pricing_tier != "Standard": report.status = "FAIL" - report.status_extended = f"Defender plan Defender for Servers from subscription {subscription} is set to OFF (pricing tier not standard)." + report.status_extended = f"Defender plan Defender for Servers from subscription {subscription_name} ({subscription}) is set to OFF (pricing tier not standard)." findings.append(report) return findings diff --git a/prowler/providers/azure/services/defender/defender_ensure_defender_for_sql_servers_is_on/defender_ensure_defender_for_sql_servers_is_on.py b/prowler/providers/azure/services/defender/defender_ensure_defender_for_sql_servers_is_on/defender_ensure_defender_for_sql_servers_is_on.py index 741b5906a9..5f40f32b28 100644 --- a/prowler/providers/azure/services/defender/defender_ensure_defender_for_sql_servers_is_on/defender_ensure_defender_for_sql_servers_is_on.py +++ b/prowler/providers/azure/services/defender/defender_ensure_defender_for_sql_servers_is_on/defender_ensure_defender_for_sql_servers_is_on.py @@ -6,6 +6,9 @@ class defender_ensure_defender_for_sql_servers_is_on(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, pricings in defender_client.pricings.items(): + subscription_name = defender_client.subscriptions.get( + subscription, subscription + ) if "SqlServerVirtualMachines" in pricings: report = Check_Report_Azure( metadata=self.metadata(), @@ -14,10 +17,10 @@ class defender_ensure_defender_for_sql_servers_is_on(Check): report.subscription = subscription report.resource_name = "Defender plan SQL Server VMs" report.status = "PASS" - report.status_extended = f"Defender plan Defender for SQL Server VMs from subscription {subscription} is set to ON (pricing tier standard)." + report.status_extended = f"Defender plan Defender for SQL Server VMs from subscription {subscription_name} ({subscription}) is set to ON (pricing tier standard)." if pricings["SqlServerVirtualMachines"].pricing_tier != "Standard": report.status = "FAIL" - report.status_extended = f"Defender plan Defender for SQL Server VMs from subscription {subscription} is set to OFF (pricing tier not standard)." + report.status_extended = f"Defender plan Defender for SQL Server VMs from subscription {subscription_name} ({subscription}) is set to OFF (pricing tier not standard)." findings.append(report) return findings diff --git a/prowler/providers/azure/services/defender/defender_ensure_defender_for_storage_is_on/defender_ensure_defender_for_storage_is_on.py b/prowler/providers/azure/services/defender/defender_ensure_defender_for_storage_is_on/defender_ensure_defender_for_storage_is_on.py index 390d6e8cde..e4844d343d 100644 --- a/prowler/providers/azure/services/defender/defender_ensure_defender_for_storage_is_on/defender_ensure_defender_for_storage_is_on.py +++ b/prowler/providers/azure/services/defender/defender_ensure_defender_for_storage_is_on/defender_ensure_defender_for_storage_is_on.py @@ -6,6 +6,9 @@ class defender_ensure_defender_for_storage_is_on(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, pricings in defender_client.pricings.items(): + subscription_name = defender_client.subscriptions.get( + subscription, subscription + ) if "StorageAccounts" in pricings: report = Check_Report_Azure( metadata=self.metadata(), @@ -14,10 +17,10 @@ class defender_ensure_defender_for_storage_is_on(Check): report.subscription = subscription report.resource_name = "Defender plan Storage Accounts" report.status = "PASS" - report.status_extended = f"Defender plan Defender for Storage Accounts from subscription {subscription} is set to ON (pricing tier standard)." + report.status_extended = f"Defender plan Defender for Storage Accounts from subscription {subscription_name} ({subscription}) is set to ON (pricing tier standard)." if pricings["StorageAccounts"].pricing_tier != "Standard": report.status = "FAIL" - report.status_extended = f"Defender plan Defender for Storage Accounts from subscription {subscription} is set to OFF (pricing tier not standard)." + report.status_extended = f"Defender plan Defender for Storage Accounts from subscription {subscription_name} ({subscription}) is set to OFF (pricing tier not standard)." findings.append(report) return findings diff --git a/prowler/providers/azure/services/defender/defender_ensure_iot_hub_defender_is_on/defender_ensure_iot_hub_defender_is_on.py b/prowler/providers/azure/services/defender/defender_ensure_iot_hub_defender_is_on/defender_ensure_iot_hub_defender_is_on.py index e608e09fbd..07cbc79102 100644 --- a/prowler/providers/azure/services/defender/defender_ensure_iot_hub_defender_is_on/defender_ensure_iot_hub_defender_is_on.py +++ b/prowler/providers/azure/services/defender/defender_ensure_iot_hub_defender_is_on/defender_ensure_iot_hub_defender_is_on.py @@ -7,18 +7,19 @@ class defender_ensure_iot_hub_defender_is_on(Check): findings = [] for ( - subscription_name, + subscription_id, iot_security_solutions, ) in defender_client.iot_security_solutions.items(): + subscription_name = defender_client.subscriptions.get( + subscription_id, subscription_id + ) if not iot_security_solutions: report = Check_Report_Azure(metadata=self.metadata(), resource={}) report.status = "FAIL" - report.subscription = subscription_name - report.resource_name = subscription_name - report.resource_id = ( - f"/subscriptions/{defender_client.subscriptions[subscription_name]}" - ) - report.status_extended = f"No IoT Security Solutions found in the subscription {subscription_name}." + report.subscription = subscription_id + report.resource_name = subscription_id + report.resource_id = f"/subscriptions/{subscription_id}" + report.status_extended = f"No IoT Security Solutions found in the subscription {subscription_name} ({subscription_id})." findings.append(report) else: for iot_security_solution in iot_security_solutions.values(): @@ -26,13 +27,13 @@ class defender_ensure_iot_hub_defender_is_on(Check): metadata=self.metadata(), resource=iot_security_solution, ) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "PASS" - report.status_extended = f"The security solution {iot_security_solution.name} is enabled in subscription {subscription_name}." + report.status_extended = f"The security solution {iot_security_solution.name} is enabled in subscription {subscription_name} ({subscription_id})." if iot_security_solution.status != "Enabled": report.status = "FAIL" - report.status_extended = f"The security solution {iot_security_solution.name} is disabled in subscription {subscription_name}" + report.status_extended = f"The security solution {iot_security_solution.name} is disabled in subscription {subscription_name} ({subscription_id})" findings.append(report) diff --git a/prowler/providers/azure/services/defender/defender_ensure_mcas_is_enabled/defender_ensure_mcas_is_enabled.py b/prowler/providers/azure/services/defender/defender_ensure_mcas_is_enabled/defender_ensure_mcas_is_enabled.py index 4899f2dc06..c836fa1c67 100644 --- a/prowler/providers/azure/services/defender/defender_ensure_mcas_is_enabled/defender_ensure_mcas_is_enabled.py +++ b/prowler/providers/azure/services/defender/defender_ensure_mcas_is_enabled/defender_ensure_mcas_is_enabled.py @@ -7,29 +7,30 @@ class defender_ensure_mcas_is_enabled(Check): findings = [] for ( - subscription_name, + subscription_id, settings, ) in defender_client.settings.items(): + subscription_name = defender_client.subscriptions.get( + subscription_id, subscription_id + ) if "MCAS" not in settings: report = Check_Report_Azure(metadata=self.metadata(), resource={}) - report.subscription = subscription_name - report.resource_name = subscription_name - report.resource_id = ( - f"/subscriptions/{defender_client.subscriptions[subscription_name]}" - ) + report.subscription = subscription_id + report.resource_name = subscription_id + report.resource_id = f"/subscriptions/{subscription_id}" report.status = "FAIL" - report.status_extended = f"Microsoft Defender for Cloud Apps not exists for subscription {subscription_name}." + report.status_extended = f"Microsoft Defender for Cloud Apps not exists for subscription {subscription_name} ({subscription_id})." else: report = Check_Report_Azure( metadata=self.metadata(), resource=settings["MCAS"] ) - report.subscription = subscription_name + report.subscription = subscription_id if settings["MCAS"].enabled: report.status = "PASS" - report.status_extended = f"Microsoft Defender for Cloud Apps is enabled for subscription {subscription_name}." + report.status_extended = f"Microsoft Defender for Cloud Apps is enabled for subscription {subscription_name} ({subscription_id})." else: report.status = "FAIL" - report.status_extended = f"Microsoft Defender for Cloud Apps is disabled for subscription {subscription_name}." + report.status_extended = f"Microsoft Defender for Cloud Apps is disabled for subscription {subscription_name} ({subscription_id})." findings.append(report) diff --git a/prowler/providers/azure/services/defender/defender_ensure_notify_alerts_severity_is_high/defender_ensure_notify_alerts_severity_is_high.py b/prowler/providers/azure/services/defender/defender_ensure_notify_alerts_severity_is_high/defender_ensure_notify_alerts_severity_is_high.py index d01fec8966..fb99f0277b 100644 --- a/prowler/providers/azure/services/defender/defender_ensure_notify_alerts_severity_is_high/defender_ensure_notify_alerts_severity_is_high.py +++ b/prowler/providers/azure/services/defender/defender_ensure_notify_alerts_severity_is_high/defender_ensure_notify_alerts_severity_is_high.py @@ -7,9 +7,12 @@ class defender_ensure_notify_alerts_severity_is_high(Check): findings = [] for ( - subscription_name, + subscription_id, security_contact_configurations, ) in defender_client.security_contact_configurations.items(): + subscription_name = defender_client.subscriptions.get( + subscription_id, subscription_id + ) for contact_configuration in security_contact_configurations.values(): report = Check_Report_Azure( metadata=self.metadata(), resource=contact_configuration @@ -19,16 +22,16 @@ class defender_ensure_notify_alerts_severity_is_high(Check): if contact_configuration.name else "Security Contact" ) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "FAIL" - report.status_extended = f"Notifications are not enabled for alerts with a minimum severity of high or lower in subscription {subscription_name}." + report.status_extended = f"Notifications are not enabled for alerts with a minimum severity of high or lower in subscription {subscription_name} ({subscription_id})." if ( contact_configuration.alert_minimal_severity and contact_configuration.alert_minimal_severity != "Critical" ): report.status = "PASS" - report.status_extended = f"Notifications are enabled for alerts with a minimum severity of high or lower ({contact_configuration.alert_minimal_severity}) in subscription {subscription_name}." + report.status_extended = f"Notifications are enabled for alerts with a minimum severity of high or lower ({contact_configuration.alert_minimal_severity}) in subscription {subscription_name} ({subscription_id})." findings.append(report) diff --git a/prowler/providers/azure/services/defender/defender_ensure_notify_emails_to_owners/defender_ensure_notify_emails_to_owners.py b/prowler/providers/azure/services/defender/defender_ensure_notify_emails_to_owners/defender_ensure_notify_emails_to_owners.py index ed16c609c3..7c751bb816 100644 --- a/prowler/providers/azure/services/defender/defender_ensure_notify_emails_to_owners/defender_ensure_notify_emails_to_owners.py +++ b/prowler/providers/azure/services/defender/defender_ensure_notify_emails_to_owners/defender_ensure_notify_emails_to_owners.py @@ -7,9 +7,12 @@ class defender_ensure_notify_emails_to_owners(Check): findings = [] for ( - subscription_name, + subscription_id, security_contact_configurations, ) in defender_client.security_contact_configurations.items(): + subscription_name = defender_client.subscriptions.get( + subscription_id, subscription_id + ) for contact_configuration in security_contact_configurations.values(): report = Check_Report_Azure( metadata=self.metadata(), @@ -20,16 +23,16 @@ class defender_ensure_notify_emails_to_owners(Check): if contact_configuration.name else "Security Contact" ) - report.subscription = subscription_name + report.subscription = subscription_id if ( contact_configuration.notifications_by_role.state and "Owner" in contact_configuration.notifications_by_role.roles ): report.status = "PASS" - report.status_extended = f"The Owner role is notified for subscription {subscription_name}." + report.status_extended = f"The Owner role is notified for subscription {subscription_name} ({subscription_id})." else: report.status = "FAIL" - report.status_extended = f"The Owner role is not notified for subscription {subscription_name}." + report.status_extended = f"The Owner role is not notified for subscription {subscription_name} ({subscription_id})." findings.append(report) diff --git a/prowler/providers/azure/services/defender/defender_ensure_system_updates_are_applied/defender_ensure_system_updates_are_applied.py b/prowler/providers/azure/services/defender/defender_ensure_system_updates_are_applied/defender_ensure_system_updates_are_applied.py index 1984888f02..01da3dec82 100644 --- a/prowler/providers/azure/services/defender/defender_ensure_system_updates_are_applied/defender_ensure_system_updates_are_applied.py +++ b/prowler/providers/azure/services/defender/defender_ensure_system_updates_are_applied/defender_ensure_system_updates_are_applied.py @@ -7,9 +7,12 @@ class defender_ensure_system_updates_are_applied(Check): findings = [] for ( - subscription_name, + subscription_id, assessments, ) in defender_client.assessments.items(): + subscription_name = defender_client.subscriptions.get( + subscription_id, subscription_id + ) if ( "Log Analytics agent should be installed on virtual machines" in assessments @@ -23,9 +26,9 @@ class defender_ensure_system_updates_are_applied(Check): "System updates should be installed on your machines" ], ) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "PASS" - report.status_extended = f"System updates are applied for all the VMs in the subscription {subscription_name}." + report.status_extended = f"System updates are applied for all the VMs in the subscription {subscription_name} ({subscription_id})." if ( assessments[ @@ -42,7 +45,7 @@ class defender_ensure_system_updates_are_applied(Check): == "Unhealthy" ): report.status = "FAIL" - report.status_extended = f"System updates are not applied for all the VMs in the subscription {subscription_name}." + report.status_extended = f"System updates are not applied for all the VMs in the subscription {subscription_name} ({subscription_id})." findings.append(report) diff --git a/prowler/providers/azure/services/defender/defender_ensure_wdatp_is_enabled/defender_ensure_wdatp_is_enabled.py b/prowler/providers/azure/services/defender/defender_ensure_wdatp_is_enabled/defender_ensure_wdatp_is_enabled.py index 47aa40a904..9b2049c9fb 100644 --- a/prowler/providers/azure/services/defender/defender_ensure_wdatp_is_enabled/defender_ensure_wdatp_is_enabled.py +++ b/prowler/providers/azure/services/defender/defender_ensure_wdatp_is_enabled/defender_ensure_wdatp_is_enabled.py @@ -7,29 +7,30 @@ class defender_ensure_wdatp_is_enabled(Check): findings = [] for ( - subscription_name, + subscription_id, settings, ) in defender_client.settings.items(): + subscription_name = defender_client.subscriptions.get( + subscription_id, subscription_id + ) if "WDATP" not in settings: report = Check_Report_Azure(metadata=self.metadata(), resource={}) - report.subscription = subscription_name - report.resource_name = subscription_name - report.resource_id = ( - f"/subscriptions/{defender_client.subscriptions[subscription_name]}" - ) + report.subscription = subscription_id + report.resource_name = subscription_id + report.resource_id = f"/subscriptions/{subscription_id}" report.status = "FAIL" - report.status_extended = f"Microsoft Defender for Endpoint integration not exists for subscription {subscription_name}." + report.status_extended = f"Microsoft Defender for Endpoint integration not exists for subscription {subscription_name} ({subscription_id})." else: report = Check_Report_Azure( metadata=self.metadata(), resource=settings["WDATP"] ) - report.subscription = subscription_name + report.subscription = subscription_id if settings["WDATP"].enabled: report.status = "PASS" - report.status_extended = f"Microsoft Defender for Endpoint integration is enabled for subscription {subscription_name}." + report.status_extended = f"Microsoft Defender for Endpoint integration is enabled for subscription {subscription_name} ({subscription_id})." else: report.status = "FAIL" - report.status_extended = f"Microsoft Defender for Endpoint integration is disabled for subscription {subscription_name}." + report.status_extended = f"Microsoft Defender for Endpoint integration is disabled for subscription {subscription_name} ({subscription_id})." findings.append(report) diff --git a/prowler/providers/azure/services/defender/defender_service.py b/prowler/providers/azure/services/defender/defender_service.py index 089a8846d7..7da96cd8ec 100644 --- a/prowler/providers/azure/services/defender/defender_service.py +++ b/prowler/providers/azure/services/defender/defender_service.py @@ -30,14 +30,14 @@ class Defender(AzureService): def _get_pricings(self): logger.info("Defender - Getting pricings...") pricings = {} - for subscription_name, client in self.clients.items(): + for subscription_id, client in self.clients.items(): try: pricings_list = client.pricings.list( - scope_id=f"subscriptions/{self.subscriptions[subscription_name]}" + scope_id=f"subscriptions/{subscription_id}" ) - pricings.update({subscription_name: {}}) + pricings.update({subscription_id: {}}) for pricing in pricings_list.value: - pricings[subscription_name].update( + pricings[subscription_id].update( { pricing.name: Pricing( resource_id=pricing.id, @@ -60,23 +60,23 @@ class Defender(AzureService): except ResourceNotFoundError as error: if "Subscription Not Registered" in error.message: logger.error( - f"Subscription name: {subscription_name} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: Subscription Not Registered - Please register to Microsoft.Security in order to view your security status" + f"Subscription ID: {subscription_id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: Subscription Not Registered - Please register to Microsoft.Security in order to view your security status" ) except Exception as error: logger.error( - f"Subscription name: {subscription_name} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription_id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return pricings def _get_auto_provisioning_settings(self): logger.info("Defender - Getting auto provisioning settings...") auto_provisioning = {} - for subscription_name, client in self.clients.items(): + for subscription_id, client in self.clients.items(): try: auto_provisioning_settings = client.auto_provisioning_settings.list() - auto_provisioning.update({subscription_name: {}}) + auto_provisioning.update({subscription_id: {}}) for ap in auto_provisioning_settings: - auto_provisioning[subscription_name].update( + auto_provisioning[subscription_id].update( { ap.name: AutoProvisioningSetting( resource_id=ap.id, @@ -89,25 +89,25 @@ class Defender(AzureService): except ClientAuthenticationError as error: if "Subscription Not Registered" in error.message: logger.error( - f"Subscription name: {subscription_name} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: Subscription Not Registered - Please register to Microsoft.Security in order to view your security status" + f"Subscription ID: {subscription_id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: Subscription Not Registered - Please register to Microsoft.Security in order to view your security status" ) except Exception as error: logger.error( - f"Subscription name: {subscription_name} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription_id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return auto_provisioning def _get_assessments(self): logger.info("Defender - Getting assessments...") assessments = {} - for subscription_name, client in self.clients.items(): + for subscription_id, client in self.clients.items(): try: assessments_list = client.assessments.list( - f"subscriptions/{self.subscriptions[subscription_name]}" + f"subscriptions/{subscription_id}" ) - assessments.update({subscription_name: {}}) + assessments.update({subscription_id: {}}) for assessment in assessments_list: - assessments[subscription_name].update( + assessments[subscription_id].update( { assessment.display_name: Assesment( resource_id=assessment.id, @@ -120,19 +120,19 @@ class Defender(AzureService): ) except Exception as error: logger.error( - f"Subscription name: {subscription_name} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription_id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return assessments def _get_settings(self): logger.info("Defender - Getting settings...") settings = {} - for subscription_name, client in self.clients.items(): + for subscription_id, client in self.clients.items(): try: settings_list = client.settings.list() - settings.update({subscription_name: {}}) + settings.update({subscription_id: {}}) for setting in settings_list: - settings[subscription_name].update( + settings[subscription_id].update( { setting.name: Setting( resource_id=setting.id, @@ -146,11 +146,11 @@ class Defender(AzureService): except ClientAuthenticationError as error: if "Subscription Not Registered" in error.message: logger.error( - f"Subscription name: {subscription_name} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: Subscription Not Registered - Please register to Microsoft.Security in order to view your security status" + f"Subscription ID: {subscription_id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: Subscription Not Registered - Please register to Microsoft.Security in order to view your security status" ) except Exception as error: logger.error( - f"Subscription name: {subscription_name} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription_id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return settings @@ -166,7 +166,7 @@ class Defender(AzureService): """ logger.info("Defender - Getting security contacts...") security_contacts = {} - for subscription_name, subscription_id in self.subscriptions.items(): + for subscription_id, display_name in self.subscriptions.items(): try: url = f"https://management.azure.com/subscriptions/{subscription_id}/providers/Microsoft.Security/securityContacts?api-version=2023-12-01-preview" headers = { @@ -176,7 +176,7 @@ class Defender(AzureService): response = requests.get(url, headers=headers) response.raise_for_status() contact_configurations = response.json().get("value", []) - security_contacts[subscription_name] = {} + security_contacts[subscription_id] = {} for contact_configuration in contact_configurations: props = contact_configuration.get("properties", {}) @@ -204,7 +204,7 @@ class Defender(AzureService): if value is not None: alert_minimal_severity = value - security_contacts[subscription_name][ + security_contacts[subscription_id][ contact_configuration.get("name", "default") ] = SecurityContactConfiguration( id=contact_configuration.get("id", ""), @@ -221,21 +221,21 @@ class Defender(AzureService): ) except Exception as error: logger.error( - f"Subscription name: {subscription_name} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription_id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return security_contacts def _get_iot_security_solutions(self): logger.info("Defender - Getting IoT Security Solutions...") iot_security_solutions = {} - for subscription_name, client in self.clients.items(): + for subscription_id, client in self.clients.items(): try: iot_security_solutions_list = ( client.iot_security_solution.list_by_subscription() ) - iot_security_solutions.update({subscription_name: {}}) + iot_security_solutions.update({subscription_id: {}}) for iot_security_solution in iot_security_solutions_list: - iot_security_solutions[subscription_name].update( + iot_security_solutions[subscription_id].update( { iot_security_solution.id: IoTSecuritySolution( resource_id=iot_security_solution.id, @@ -246,7 +246,7 @@ class Defender(AzureService): ) except Exception as error: logger.error( - f"Subscription name: {subscription_name} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription_id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return iot_security_solutions @@ -257,22 +257,22 @@ class Defender(AzureService): Returns: A dictionary of JIT policies for each subscription. The format will be: { - "subscription_name": { + "subscription_id": { "jit_policy_id": JITPolicy } } """ logger.info("Defender - Getting JIT policies...") jit_policies = {} - for subscription_name, client in self.clients.items(): + for subscription_id, client in self.clients.items(): try: - jit_policies[subscription_name] = {} + jit_policies[subscription_id] = {} policies = client.jit_network_access_policies.list() for policy in policies: vm_ids = set() for vm in getattr(policy, "virtual_machines", []): vm_ids.add(vm.id) - jit_policies[subscription_name].update( + jit_policies[subscription_id].update( { policy.id: JITPolicy( id=policy.id, @@ -284,7 +284,7 @@ class Defender(AzureService): ) except Exception as error: logger.error( - f"Subscription name: {subscription_name} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription_id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return jit_policies diff --git a/prowler/providers/azure/services/entra/entra_user_with_vm_access_has_mfa/entra_user_with_vm_access_has_mfa.py b/prowler/providers/azure/services/entra/entra_user_with_vm_access_has_mfa/entra_user_with_vm_access_has_mfa.py index eec21c474a..917200864e 100644 --- a/prowler/providers/azure/services/entra/entra_user_with_vm_access_has_mfa/entra_user_with_vm_access_has_mfa.py +++ b/prowler/providers/azure/services/entra/entra_user_with_vm_access_has_mfa/entra_user_with_vm_access_has_mfa.py @@ -20,10 +20,13 @@ class entra_user_with_vm_access_has_mfa(Check): for users in entra_client.users.values(): for user in users.values(): for ( - subscription_name, + subscription_id, role_assigns, ) in iam_client.role_assignments.items(): - if (user.id, subscription_name) in already_reported: + subscription_name = entra_client.subscriptions.get( + subscription_id, subscription_id + ) + if (user.id, subscription_id) in already_reported: continue for assignment in role_assigns.values(): @@ -44,15 +47,15 @@ class entra_user_with_vm_access_has_mfa(Check): report = Check_Report_Azure( metadata=self.metadata(), resource=user ) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "FAIL" - report.status_extended = f"User {user.name} without MFA can access VMs in subscription {subscription_name}" + report.status_extended = f"User {user.name} without MFA can access VMs in subscription {subscription_name} ({subscription_id})" if user.is_mfa_capable: report.status = "PASS" - report.status_extended = f"User {user.name} can access VMs in subscription {subscription_name} but it has MFA." + report.status_extended = f"User {user.name} can access VMs in subscription {subscription_name} ({subscription_id}) but it has MFA." findings.append(report) - already_reported.add((user.id, subscription_name)) + already_reported.add((user.id, subscription_id)) break return findings diff --git a/prowler/providers/azure/services/iam/iam_custom_role_has_permissions_to_administer_resource_locks/iam_custom_role_has_permissions_to_administer_resource_locks.py b/prowler/providers/azure/services/iam/iam_custom_role_has_permissions_to_administer_resource_locks/iam_custom_role_has_permissions_to_administer_resource_locks.py index c6c16326a3..ee0604dd34 100644 --- a/prowler/providers/azure/services/iam/iam_custom_role_has_permissions_to_administer_resource_locks/iam_custom_role_has_permissions_to_administer_resource_locks.py +++ b/prowler/providers/azure/services/iam/iam_custom_role_has_permissions_to_administer_resource_locks/iam_custom_role_has_permissions_to_administer_resource_locks.py @@ -8,6 +8,7 @@ class iam_custom_role_has_permissions_to_administer_resource_locks(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, roles in iam_client.custom_roles.items(): + subscription_name = iam_client.subscriptions.get(subscription, subscription) exits_role_with_permission_over_locks = False for custom_role in roles.values(): @@ -18,7 +19,7 @@ class iam_custom_role_has_permissions_to_administer_resource_locks(Check): ) report.subscription = subscription report.status = "FAIL" - report.status_extended = f"Role {custom_role.name} from subscription {subscription} has no permission to administer resource locks." + report.status_extended = f"Role {custom_role.name} from subscription {subscription_name} ({subscription}) has no permission to administer resource locks." for permission_item in custom_role.permissions: if exits_role_with_permission_over_locks: @@ -26,7 +27,7 @@ class iam_custom_role_has_permissions_to_administer_resource_locks(Check): for action in permission_item.actions: if search("^Microsoft.Authorization/locks/.*", action): report.status = "PASS" - report.status_extended = f"Role {custom_role.name} from subscription {subscription} has permission to administer resource locks." + report.status_extended = f"Role {custom_role.name} from subscription {subscription_name} ({subscription}) has permission to administer resource locks." exits_role_with_permission_over_locks = True break findings.append(report) diff --git a/prowler/providers/azure/services/iam/iam_role_user_access_admin_restricted/iam_role_user_access_admin_restricted.py b/prowler/providers/azure/services/iam/iam_role_user_access_admin_restricted/iam_role_user_access_admin_restricted.py index 4880880cb0..409d7292ad 100644 --- a/prowler/providers/azure/services/iam/iam_role_user_access_admin_restricted/iam_role_user_access_admin_restricted.py +++ b/prowler/providers/azure/services/iam/iam_role_user_access_admin_restricted/iam_role_user_access_admin_restricted.py @@ -6,11 +6,14 @@ class iam_role_user_access_admin_restricted(Check): def execute(self): findings = [] - for subscription_name, assignments in iam_client.role_assignments.items(): + for subscription_id, assignments in iam_client.role_assignments.items(): + subscription_name = iam_client.subscriptions.get( + subscription_id, subscription_id + ) for assignment in assignments.values(): role_assignment_name = getattr( - iam_client.roles[subscription_name].get( - f"/subscriptions/{iam_client.subscriptions[subscription_name]}/providers/Microsoft.Authorization/roleDefinitions/{assignment.role_id}" + iam_client.roles[subscription_id].get( + f"/subscriptions/{subscription_id}/providers/Microsoft.Authorization/roleDefinitions/{assignment.role_id}" ), "name", "", @@ -18,12 +21,12 @@ class iam_role_user_access_admin_restricted(Check): report = Check_Report_Azure( metadata=self.metadata(), resource=assignment ) - report.subscription = subscription_name + report.subscription = subscription_id if role_assignment_name == "User Access Administrator": report.status = "FAIL" - report.status_extended = f"Role assignment {assignment.name} in subscription {subscription_name} grants User Access Administrator role to {getattr(assignment, 'agent_type', '')} {getattr(assignment, 'agent_id', '')}." + report.status_extended = f"Role assignment {assignment.name} in subscription {subscription_name} ({subscription_id}) grants User Access Administrator role to {getattr(assignment, 'agent_type', '')} {getattr(assignment, 'agent_id', '')}." else: report.status = "PASS" - report.status_extended = f"Role assignment {assignment.name} in subscription {subscription_name} does not grant User Access Administrator role." + report.status_extended = f"Role assignment {assignment.name} in subscription {subscription_name} ({subscription_id}) does not grant User Access Administrator role." findings.append(report) return findings diff --git a/prowler/providers/azure/services/iam/iam_service.py b/prowler/providers/azure/services/iam/iam_service.py index 55f1eb7e71..6a9ff814e9 100644 --- a/prowler/providers/azure/services/iam/iam_service.py +++ b/prowler/providers/azure/services/iam/iam_service.py @@ -23,7 +23,7 @@ class IAM(AzureService): builtin_roles.update({subscription: {}}) custom_roles.update({subscription: {}}) all_roles = client.role_definitions.list( - scope=f"/subscriptions/{self.subscriptions[subscription]}", + scope=f"/subscriptions/{subscription}", ) for role in all_roles: if role.role_type == "CustomRole": @@ -53,7 +53,7 @@ class IAM(AzureService): ) except Exception as error: logger.error( - f"Subscription name: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return builtin_roles, custom_roles @@ -83,7 +83,7 @@ class IAM(AzureService): ) except Exception as error: logger.error( - f"Subscription name: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return role_assignments diff --git a/prowler/providers/azure/services/iam/iam_subscription_roles_owner_custom_not_created/iam_subscription_roles_owner_custom_not_created.py b/prowler/providers/azure/services/iam/iam_subscription_roles_owner_custom_not_created/iam_subscription_roles_owner_custom_not_created.py index 8580a3aab7..abee3905b8 100644 --- a/prowler/providers/azure/services/iam/iam_subscription_roles_owner_custom_not_created/iam_subscription_roles_owner_custom_not_created.py +++ b/prowler/providers/azure/services/iam/iam_subscription_roles_owner_custom_not_created/iam_subscription_roles_owner_custom_not_created.py @@ -8,20 +8,21 @@ class iam_subscription_roles_owner_custom_not_created(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, roles in iam_client.custom_roles.items(): + subscription_name = iam_client.subscriptions.get(subscription, subscription) for custom_role in roles.values(): report = Check_Report_Azure( metadata=self.metadata(), resource=custom_role ) report.subscription = subscription report.status = "PASS" - report.status_extended = f"Role {custom_role.name} from subscription {subscription} is not a custom owner role." + report.status_extended = f"Role {custom_role.name} from subscription {subscription_name} ({subscription}) is not a custom owner role." for scope in custom_role.assignable_scopes: if search("^/.*", scope): for permission_item in custom_role.permissions: for action in permission_item.actions: if action == "*": report.status = "FAIL" - report.status_extended = f"Role {custom_role.name} from subscription {subscription} is a custom owner role." + report.status_extended = f"Role {custom_role.name} from subscription {subscription_name} ({subscription}) is a custom owner role." break findings.append(report) diff --git a/prowler/providers/azure/services/keyvault/keyvault_access_only_through_private_endpoints/keyvault_access_only_through_private_endpoints.py b/prowler/providers/azure/services/keyvault/keyvault_access_only_through_private_endpoints/keyvault_access_only_through_private_endpoints.py index 1a363f2d61..0eafd35de5 100644 --- a/prowler/providers/azure/services/keyvault/keyvault_access_only_through_private_endpoints/keyvault_access_only_through_private_endpoints.py +++ b/prowler/providers/azure/services/keyvault/keyvault_access_only_through_private_endpoints/keyvault_access_only_through_private_endpoints.py @@ -17,6 +17,9 @@ class keyvault_access_only_through_private_endpoints(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, key_vaults in keyvault_client.key_vaults.items(): + subscription_name = keyvault_client.subscriptions.get( + subscription, subscription + ) for keyvault in key_vaults: if ( keyvault.properties @@ -29,9 +32,9 @@ class keyvault_access_only_through_private_endpoints(Check): if keyvault.properties.public_network_access_disabled: report.status = "PASS" - report.status_extended = f"Keyvault {keyvault.name} from subscription {subscription} has public network access disabled and is using private endpoints." + report.status_extended = f"Keyvault {keyvault.name} from subscription {subscription_name} ({subscription}) has public network access disabled and is using private endpoints." else: report.status = "FAIL" - report.status_extended = f"Keyvault {keyvault.name} from subscription {subscription} has public network access enabled while using private endpoints." + report.status_extended = f"Keyvault {keyvault.name} from subscription {subscription_name} ({subscription}) has public network access enabled while using private endpoints." findings.append(report) return findings diff --git a/prowler/providers/azure/services/keyvault/keyvault_key_expiration_set_in_non_rbac/keyvault_key_expiration_set_in_non_rbac.py b/prowler/providers/azure/services/keyvault/keyvault_key_expiration_set_in_non_rbac/keyvault_key_expiration_set_in_non_rbac.py index 6b3e8803c4..fd4b4074ff 100644 --- a/prowler/providers/azure/services/keyvault/keyvault_key_expiration_set_in_non_rbac/keyvault_key_expiration_set_in_non_rbac.py +++ b/prowler/providers/azure/services/keyvault/keyvault_key_expiration_set_in_non_rbac/keyvault_key_expiration_set_in_non_rbac.py @@ -6,6 +6,9 @@ class keyvault_key_expiration_set_in_non_rbac(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, key_vaults in keyvault_client.key_vaults.items(): + subscription_name = keyvault_client.subscriptions.get( + subscription, subscription + ) for keyvault in key_vaults: if not keyvault.properties.enable_rbac_authorization: for key in keyvault.keys or []: @@ -17,9 +20,9 @@ class keyvault_key_expiration_set_in_non_rbac(Check): report.subscription = subscription if not key.attributes.expires: report.status = "FAIL" - report.status_extended = f"Key {key.name} in Key Vault {keyvault.name} from subscription {subscription} does not have an expiration date set." + report.status_extended = f"Key {key.name} in Key Vault {keyvault.name} from subscription {subscription_name} ({subscription}) does not have an expiration date set." else: report.status = "PASS" - report.status_extended = f"Key {key.name} in Key Vault {keyvault.name} from subscription {subscription} has an expiration date set." + report.status_extended = f"Key {key.name} in Key Vault {keyvault.name} from subscription {subscription_name} ({subscription}) has an expiration date set." findings.append(report) return findings diff --git a/prowler/providers/azure/services/keyvault/keyvault_key_rotation_enabled/keyvault_key_rotation_enabled.py b/prowler/providers/azure/services/keyvault/keyvault_key_rotation_enabled/keyvault_key_rotation_enabled.py index 988bd5c47c..8a587a41ed 100644 --- a/prowler/providers/azure/services/keyvault/keyvault_key_rotation_enabled/keyvault_key_rotation_enabled.py +++ b/prowler/providers/azure/services/keyvault/keyvault_key_rotation_enabled/keyvault_key_rotation_enabled.py @@ -6,6 +6,9 @@ class keyvault_key_rotation_enabled(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, key_vaults in keyvault_client.key_vaults.items(): + subscription_name = keyvault_client.subscriptions.get( + subscription, subscription + ) for keyvault in key_vaults: for key in keyvault.keys or []: report = Check_Report_Azure(metadata=self.metadata(), resource=key) @@ -19,9 +22,9 @@ class keyvault_key_rotation_enabled(Check): ) ): report.status = "PASS" - report.status_extended = f"Key {key.name} in Key Vault {keyvault.name} from subscription {subscription} has a rotation policy set." + report.status_extended = f"Key {key.name} in Key Vault {keyvault.name} from subscription {subscription_name} ({subscription}) has a rotation policy set." else: report.status = "FAIL" - report.status_extended = f"Key {key.name} in Key Vault {keyvault.name} from subscription {subscription} does not have a rotation policy set." + report.status_extended = f"Key {key.name} in Key Vault {keyvault.name} from subscription {subscription_name} ({subscription}) does not have a rotation policy set." findings.append(report) return findings diff --git a/prowler/providers/azure/services/keyvault/keyvault_logging_enabled/keyvault_logging_enabled.py b/prowler/providers/azure/services/keyvault/keyvault_logging_enabled/keyvault_logging_enabled.py index dee956d63d..68ef149cd7 100644 --- a/prowler/providers/azure/services/keyvault/keyvault_logging_enabled/keyvault_logging_enabled.py +++ b/prowler/providers/azure/services/keyvault/keyvault_logging_enabled/keyvault_logging_enabled.py @@ -6,12 +6,15 @@ class keyvault_logging_enabled(Check): def execute(self) -> Check_Report_Azure: findings = [] - for subscription_name, key_vaults in keyvault_client.key_vaults.items(): + for subscription_id, key_vaults in keyvault_client.key_vaults.items(): + subscription_name = keyvault_client.subscriptions.get( + subscription_id, subscription_id + ) for keyvault in key_vaults: report = Check_Report_Azure(metadata=self.metadata(), resource=keyvault) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "FAIL" - report.status_extended = f"Key Vault {keyvault.name} in subscription {subscription_name} does not have a diagnostic setting with audit logging." + report.status_extended = f"Key Vault {keyvault.name} in subscription {subscription_name} ({subscription_id}) does not have a diagnostic setting with audit logging." for diagnostic_setting in keyvault.monitor_diagnostic_settings or []: has_audit = False has_all_logs = False @@ -22,7 +25,7 @@ class keyvault_logging_enabled(Check): has_all_logs = True if has_audit and has_all_logs: report.status = "PASS" - report.status_extended = f"Key Vault {keyvault.name} in subscription {subscription_name} has a diagnostic setting with audit logging." + report.status_extended = f"Key Vault {keyvault.name} in subscription {subscription_name} ({subscription_id}) has a diagnostic setting with audit logging." break findings.append(report) diff --git a/prowler/providers/azure/services/keyvault/keyvault_non_rbac_secret_expiration_set/keyvault_non_rbac_secret_expiration_set.py b/prowler/providers/azure/services/keyvault/keyvault_non_rbac_secret_expiration_set/keyvault_non_rbac_secret_expiration_set.py index 2123ad0d32..a0d83eeae6 100644 --- a/prowler/providers/azure/services/keyvault/keyvault_non_rbac_secret_expiration_set/keyvault_non_rbac_secret_expiration_set.py +++ b/prowler/providers/azure/services/keyvault/keyvault_non_rbac_secret_expiration_set/keyvault_non_rbac_secret_expiration_set.py @@ -6,6 +6,9 @@ class keyvault_non_rbac_secret_expiration_set(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, key_vaults in keyvault_client.key_vaults.items(): + subscription_name = keyvault_client.subscriptions.get( + subscription, subscription + ) for keyvault in key_vaults: if not keyvault.properties.enable_rbac_authorization: for secret in keyvault.secrets or []: @@ -17,9 +20,9 @@ class keyvault_non_rbac_secret_expiration_set(Check): report.subscription = subscription if not secret.attributes.expires: report.status = "FAIL" - report.status_extended = f"Secret {secret.name} in Key Vault {keyvault.name} from subscription {subscription} does not have an expiration date set." + report.status_extended = f"Secret {secret.name} in Key Vault {keyvault.name} from subscription {subscription_name} ({subscription}) does not have an expiration date set." else: report.status = "PASS" - report.status_extended = f"Secret {secret.name} in Key Vault {keyvault.name} from subscription {subscription} has an expiration date set." + report.status_extended = f"Secret {secret.name} in Key Vault {keyvault.name} from subscription {subscription_name} ({subscription}) has an expiration date set." findings.append(report) return findings diff --git a/prowler/providers/azure/services/keyvault/keyvault_private_endpoints/keyvault_private_endpoints.py b/prowler/providers/azure/services/keyvault/keyvault_private_endpoints/keyvault_private_endpoints.py index 84c6b17e57..9af1b8ba2d 100644 --- a/prowler/providers/azure/services/keyvault/keyvault_private_endpoints/keyvault_private_endpoints.py +++ b/prowler/providers/azure/services/keyvault/keyvault_private_endpoints/keyvault_private_endpoints.py @@ -6,16 +6,19 @@ class keyvault_private_endpoints(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, key_vaults in keyvault_client.key_vaults.items(): + subscription_name = keyvault_client.subscriptions.get( + subscription, subscription + ) for keyvault in key_vaults: report = Check_Report_Azure(metadata=self.metadata(), resource=keyvault) report.subscription = subscription report.status = "FAIL" - report.status_extended = f"Keyvault {keyvault.name} from subscription {subscription} is not using private endpoints." + report.status_extended = f"Keyvault {keyvault.name} from subscription {subscription_name} ({subscription}) is not using private endpoints." if ( keyvault.properties and keyvault.properties.private_endpoint_connections ): report.status = "PASS" - report.status_extended = f"Keyvault {keyvault.name} from subscription {subscription} is using private endpoints." + report.status_extended = f"Keyvault {keyvault.name} from subscription {subscription_name} ({subscription}) is using private endpoints." findings.append(report) return findings diff --git a/prowler/providers/azure/services/keyvault/keyvault_rbac_enabled/keyvault_rbac_enabled.py b/prowler/providers/azure/services/keyvault/keyvault_rbac_enabled/keyvault_rbac_enabled.py index e26c5b84f7..1025cf5118 100644 --- a/prowler/providers/azure/services/keyvault/keyvault_rbac_enabled/keyvault_rbac_enabled.py +++ b/prowler/providers/azure/services/keyvault/keyvault_rbac_enabled/keyvault_rbac_enabled.py @@ -6,16 +6,19 @@ class keyvault_rbac_enabled(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, key_vaults in keyvault_client.key_vaults.items(): + subscription_name = keyvault_client.subscriptions.get( + subscription, subscription + ) for keyvault in key_vaults: report = Check_Report_Azure(metadata=self.metadata(), resource=keyvault) report.subscription = subscription report.status = "FAIL" - report.status_extended = f"Keyvault {keyvault.name} from subscription {subscription} is not using RBAC for access control." + report.status_extended = f"Keyvault {keyvault.name} from subscription {subscription_name} ({subscription}) is not using RBAC for access control." if ( keyvault.properties and keyvault.properties.enable_rbac_authorization ): report.status = "PASS" - report.status_extended = f"Keyvault {keyvault.name} from subscription {subscription} is using RBAC for access control." + report.status_extended = f"Keyvault {keyvault.name} from subscription {subscription_name} ({subscription}) is using RBAC for access control." findings.append(report) return findings diff --git a/prowler/providers/azure/services/keyvault/keyvault_rbac_key_expiration_set/keyvault_rbac_key_expiration_set.py b/prowler/providers/azure/services/keyvault/keyvault_rbac_key_expiration_set/keyvault_rbac_key_expiration_set.py index 1a5ca4f268..edc3416343 100644 --- a/prowler/providers/azure/services/keyvault/keyvault_rbac_key_expiration_set/keyvault_rbac_key_expiration_set.py +++ b/prowler/providers/azure/services/keyvault/keyvault_rbac_key_expiration_set/keyvault_rbac_key_expiration_set.py @@ -6,6 +6,9 @@ class keyvault_rbac_key_expiration_set(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, key_vaults in keyvault_client.key_vaults.items(): + subscription_name = keyvault_client.subscriptions.get( + subscription, subscription + ) for keyvault in key_vaults: if keyvault.properties.enable_rbac_authorization: for key in keyvault.keys or []: @@ -17,9 +20,9 @@ class keyvault_rbac_key_expiration_set(Check): report.subscription = subscription if not key.attributes.expires: report.status = "FAIL" - report.status_extended = f"Key {key.name} in Key Vault {keyvault.name} from subscription {subscription} does not have an expiration date set." + report.status_extended = f"Key {key.name} in Key Vault {keyvault.name} from subscription {subscription_name} ({subscription}) does not have an expiration date set." else: report.status = "PASS" - report.status_extended = f"Key {key.name} in Key Vault {keyvault.name} from subscription {subscription} has an expiration date set." + report.status_extended = f"Key {key.name} in Key Vault {keyvault.name} from subscription {subscription_name} ({subscription}) has an expiration date set." findings.append(report) return findings diff --git a/prowler/providers/azure/services/keyvault/keyvault_rbac_secret_expiration_set/keyvault_rbac_secret_expiration_set.py b/prowler/providers/azure/services/keyvault/keyvault_rbac_secret_expiration_set/keyvault_rbac_secret_expiration_set.py index 3515f40f08..ed9ee564b4 100644 --- a/prowler/providers/azure/services/keyvault/keyvault_rbac_secret_expiration_set/keyvault_rbac_secret_expiration_set.py +++ b/prowler/providers/azure/services/keyvault/keyvault_rbac_secret_expiration_set/keyvault_rbac_secret_expiration_set.py @@ -6,6 +6,9 @@ class keyvault_rbac_secret_expiration_set(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, key_vaults in keyvault_client.key_vaults.items(): + subscription_name = keyvault_client.subscriptions.get( + subscription, subscription + ) for keyvault in key_vaults: if keyvault.properties.enable_rbac_authorization: for secret in keyvault.secrets or []: @@ -17,9 +20,9 @@ class keyvault_rbac_secret_expiration_set(Check): report.subscription = subscription if not secret.attributes.expires: report.status = "FAIL" - report.status_extended = f"Secret {secret.name} in Key Vault {keyvault.name} from subscription {subscription} does not have an expiration date set." + report.status_extended = f"Secret {secret.name} in Key Vault {keyvault.name} from subscription {subscription_name} ({subscription}) does not have an expiration date set." else: report.status = "PASS" - report.status_extended = f"Secret {secret.name} in Key Vault {keyvault.name} from subscription {subscription} has an expiration date set." + report.status_extended = f"Secret {secret.name} in Key Vault {keyvault.name} from subscription {subscription_name} ({subscription}) has an expiration date set." findings.append(report) return findings diff --git a/prowler/providers/azure/services/keyvault/keyvault_recoverable/keyvault_recoverable.py b/prowler/providers/azure/services/keyvault/keyvault_recoverable/keyvault_recoverable.py index 3ffe1f5b93..ee3da35496 100644 --- a/prowler/providers/azure/services/keyvault/keyvault_recoverable/keyvault_recoverable.py +++ b/prowler/providers/azure/services/keyvault/keyvault_recoverable/keyvault_recoverable.py @@ -6,16 +6,19 @@ class keyvault_recoverable(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, key_vaults in keyvault_client.key_vaults.items(): + subscription_name = keyvault_client.subscriptions.get( + subscription, subscription + ) for keyvault in key_vaults: report = Check_Report_Azure(metadata=self.metadata(), resource=keyvault) report.subscription = subscription report.status = "FAIL" - report.status_extended = f"Keyvault {keyvault.name} from subscription {subscription} is not recoverable." + report.status_extended = f"Keyvault {keyvault.name} from subscription {subscription_name} ({subscription}) is not recoverable." if ( keyvault.properties.enable_soft_delete and keyvault.properties.enable_purge_protection ): report.status = "PASS" - report.status_extended = f"Keyvault {keyvault.name} from subscription {subscription} is recoverable." + report.status_extended = f"Keyvault {keyvault.name} from subscription {subscription_name} ({subscription}) is recoverable." findings.append(report) return findings diff --git a/prowler/providers/azure/services/keyvault/keyvault_service.py b/prowler/providers/azure/services/keyvault/keyvault_service.py index 8f8a0cc452..9fb3fd98af 100644 --- a/prowler/providers/azure/services/keyvault/keyvault_service.py +++ b/prowler/providers/azure/services/keyvault/keyvault_service.py @@ -56,7 +56,7 @@ class KeyVault(AzureService): except Exception as error: logger.error( - f"Subscription name: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return key_vaults @@ -172,7 +172,7 @@ class KeyVault(AzureService): except Exception as error: logger.error( - f"Subscription name: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) try: @@ -204,7 +204,7 @@ class KeyVault(AzureService): # TODO: handle different errors here since we are catching all HTTP Errors here except HttpResponseError: logger.warning( - f"Subscription name: {subscription} -- has no access policy configured for keyvault {keyvault_name}" + f"Subscription ID: {subscription} -- has no access policy configured for keyvault {keyvault_name}" ) return keys @@ -256,7 +256,7 @@ class KeyVault(AzureService): ) except Exception as error: logger.error( - f"Subscription name: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return secrets @@ -268,13 +268,13 @@ class KeyVault(AzureService): monitor_diagnostics_settings = [] try: monitor_diagnostics_settings = monitor_client.diagnostic_settings_with_uri( - self.subscriptions[subscription], - f"subscriptions/{self.subscriptions[subscription]}/resourceGroups/{resource_group}/providers/Microsoft.KeyVault/vaults/{keyvault_name}", + subscription, + f"subscriptions/{subscription}/resourceGroups/{resource_group}/providers/Microsoft.KeyVault/vaults/{keyvault_name}", monitor_client.clients[subscription], ) except Exception as error: logger.error( - f"Subscription name: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return monitor_diagnostics_settings diff --git a/prowler/providers/azure/services/monitor/monitor_alert_create_policy_assignment/monitor_alert_create_policy_assignment.py b/prowler/providers/azure/services/monitor/monitor_alert_create_policy_assignment/monitor_alert_create_policy_assignment.py index 2a7d99b622..695e0b1033 100644 --- a/prowler/providers/azure/services/monitor/monitor_alert_create_policy_assignment/monitor_alert_create_policy_assignment.py +++ b/prowler/providers/azure/services/monitor/monitor_alert_create_policy_assignment/monitor_alert_create_policy_assignment.py @@ -8,9 +8,12 @@ class monitor_alert_create_policy_assignment(Check): findings = [] for ( - subscription_name, + subscription_id, activity_log_alerts, ) in monitor_client.alert_rules.items(): + subscription_name = monitor_client.subscriptions.get( + subscription_id, subscription_id + ) for alert_rule in activity_log_alerts: if check_alert_rule( alert_rule, "Microsoft.Authorization/policyAssignments/write" @@ -18,19 +21,17 @@ class monitor_alert_create_policy_assignment(Check): report = Check_Report_Azure( metadata=self.metadata(), resource=alert_rule ) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "PASS" - report.status_extended = f"There is an alert configured for creating Policy Assignments in subscription {subscription_name}." + report.status_extended = f"There is an alert configured for creating Policy Assignments in subscription {subscription_name} ({subscription_id})." break else: report = Check_Report_Azure(metadata=self.metadata(), resource={}) - report.subscription = subscription_name - report.resource_name = subscription_name - report.resource_id = ( - f"/subscriptions/{monitor_client.subscriptions[subscription_name]}" - ) + report.subscription = subscription_id + report.resource_name = subscription_id + report.resource_id = f"/subscriptions/{subscription_id}" report.status = "FAIL" - report.status_extended = f"There is not an alert for creating Policy Assignments in subscription {subscription_name}." + report.status_extended = f"There is not an alert for creating Policy Assignments in subscription {subscription_name} ({subscription_id})." findings.append(report) diff --git a/prowler/providers/azure/services/monitor/monitor_alert_create_update_nsg/monitor_alert_create_update_nsg.py b/prowler/providers/azure/services/monitor/monitor_alert_create_update_nsg/monitor_alert_create_update_nsg.py index 2decfe40be..bb141d17fb 100644 --- a/prowler/providers/azure/services/monitor/monitor_alert_create_update_nsg/monitor_alert_create_update_nsg.py +++ b/prowler/providers/azure/services/monitor/monitor_alert_create_update_nsg/monitor_alert_create_update_nsg.py @@ -8,9 +8,12 @@ class monitor_alert_create_update_nsg(Check): findings = [] for ( - subscription_name, + subscription_id, activity_log_alerts, ) in monitor_client.alert_rules.items(): + subscription_name = monitor_client.subscriptions.get( + subscription_id, subscription_id + ) for alert_rule in activity_log_alerts: if check_alert_rule( alert_rule, "Microsoft.Network/networkSecurityGroups/write" @@ -18,19 +21,17 @@ class monitor_alert_create_update_nsg(Check): report = Check_Report_Azure( metadata=self.metadata(), resource=alert_rule ) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "PASS" - report.status_extended = f"There is an alert configured for creating/updating Network Security Groups in subscription {subscription_name}." + report.status_extended = f"There is an alert configured for creating/updating Network Security Groups in subscription {subscription_name} ({subscription_id})." break else: report = Check_Report_Azure(metadata=self.metadata(), resource={}) - report.subscription = subscription_name - report.resource_name = subscription_name - report.resource_id = ( - f"/subscriptions/{monitor_client.subscriptions[subscription_name]}" - ) + report.subscription = subscription_id + report.resource_name = subscription_id + report.resource_id = f"/subscriptions/{subscription_id}" report.status = "FAIL" - report.status_extended = f"There is not an alert for creating/updating Network Security Groups in subscription {subscription_name}." + report.status_extended = f"There is not an alert for creating/updating Network Security Groups in subscription {subscription_name} ({subscription_id})." findings.append(report) diff --git a/prowler/providers/azure/services/monitor/monitor_alert_create_update_public_ip_address_rule/monitor_alert_create_update_public_ip_address_rule.py b/prowler/providers/azure/services/monitor/monitor_alert_create_update_public_ip_address_rule/monitor_alert_create_update_public_ip_address_rule.py index bc8f0bc694..3ace548574 100644 --- a/prowler/providers/azure/services/monitor/monitor_alert_create_update_public_ip_address_rule/monitor_alert_create_update_public_ip_address_rule.py +++ b/prowler/providers/azure/services/monitor/monitor_alert_create_update_public_ip_address_rule/monitor_alert_create_update_public_ip_address_rule.py @@ -8,9 +8,12 @@ class monitor_alert_create_update_public_ip_address_rule(Check): findings = [] for ( - subscription_name, + subscription_id, activity_log_alerts, ) in monitor_client.alert_rules.items(): + subscription_name = monitor_client.subscriptions.get( + subscription_id, subscription_id + ) for alert_rule in activity_log_alerts: if check_alert_rule( alert_rule, "Microsoft.Network/publicIPAddresses/write" @@ -18,19 +21,17 @@ class monitor_alert_create_update_public_ip_address_rule(Check): report = Check_Report_Azure( metadata=self.metadata(), resource=alert_rule ) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "PASS" - report.status_extended = f"There is an alert configured for creating/updating Public IP address rule in subscription {subscription_name}." + report.status_extended = f"There is an alert configured for creating/updating Public IP address rule in subscription {subscription_name} ({subscription_id})." break else: report = Check_Report_Azure(metadata=self.metadata(), resource={}) - report.subscription = subscription_name - report.resource_name = subscription_name - report.resource_id = ( - f"/subscriptions/{monitor_client.subscriptions[subscription_name]}" - ) + report.subscription = subscription_id + report.resource_name = subscription_id + report.resource_id = f"/subscriptions/{subscription_id}" report.status = "FAIL" - report.status_extended = f"There is not an alert for creating/updating Public IP address rule in subscription {subscription_name}." + report.status_extended = f"There is not an alert for creating/updating Public IP address rule in subscription {subscription_name} ({subscription_id})." findings.append(report) diff --git a/prowler/providers/azure/services/monitor/monitor_alert_create_update_security_solution/monitor_alert_create_update_security_solution.py b/prowler/providers/azure/services/monitor/monitor_alert_create_update_security_solution/monitor_alert_create_update_security_solution.py index 71334a364b..afd11b8550 100644 --- a/prowler/providers/azure/services/monitor/monitor_alert_create_update_security_solution/monitor_alert_create_update_security_solution.py +++ b/prowler/providers/azure/services/monitor/monitor_alert_create_update_security_solution/monitor_alert_create_update_security_solution.py @@ -8,9 +8,12 @@ class monitor_alert_create_update_security_solution(Check): findings = [] for ( - subscription_name, + subscription_id, activity_log_alerts, ) in monitor_client.alert_rules.items(): + subscription_name = monitor_client.subscriptions.get( + subscription_id, subscription_id + ) for alert_rule in activity_log_alerts: if check_alert_rule( alert_rule, "Microsoft.Security/securitySolutions/write" @@ -18,19 +21,17 @@ class monitor_alert_create_update_security_solution(Check): report = Check_Report_Azure( metadata=self.metadata(), resource=alert_rule ) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "PASS" - report.status_extended = f"There is an alert configured for creating/updating Security Solution in subscription {subscription_name}." + report.status_extended = f"There is an alert configured for creating/updating Security Solution in subscription {subscription_name} ({subscription_id})." break else: report = Check_Report_Azure(metadata=self.metadata(), resource={}) - report.subscription = subscription_name - report.resource_name = subscription_name - report.resource_id = ( - f"/subscriptions/{monitor_client.subscriptions[subscription_name]}" - ) + report.subscription = subscription_id + report.resource_name = subscription_id + report.resource_id = f"/subscriptions/{subscription_id}" report.status = "FAIL" - report.status_extended = f"There is not an alert for creating/updating Security Solution in subscription {subscription_name}." + report.status_extended = f"There is not an alert for creating/updating Security Solution in subscription {subscription_name} ({subscription_id})." findings.append(report) diff --git a/prowler/providers/azure/services/monitor/monitor_alert_create_update_sqlserver_fr/monitor_alert_create_update_sqlserver_fr.py b/prowler/providers/azure/services/monitor/monitor_alert_create_update_sqlserver_fr/monitor_alert_create_update_sqlserver_fr.py index feae49c01d..cb11e7b714 100644 --- a/prowler/providers/azure/services/monitor/monitor_alert_create_update_sqlserver_fr/monitor_alert_create_update_sqlserver_fr.py +++ b/prowler/providers/azure/services/monitor/monitor_alert_create_update_sqlserver_fr/monitor_alert_create_update_sqlserver_fr.py @@ -8,9 +8,12 @@ class monitor_alert_create_update_sqlserver_fr(Check): findings = [] for ( - subscription_name, + subscription_id, activity_log_alerts, ) in monitor_client.alert_rules.items(): + subscription_name = monitor_client.subscriptions.get( + subscription_id, subscription_id + ) for alert_rule in activity_log_alerts: if check_alert_rule( alert_rule, "Microsoft.Sql/servers/firewallRules/write" @@ -18,19 +21,17 @@ class monitor_alert_create_update_sqlserver_fr(Check): report = Check_Report_Azure( metadata=self.metadata(), resource=alert_rule ) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "PASS" - report.status_extended = f"There is an alert configured for creating/updating SQL Server firewall rule in subscription {subscription_name}." + report.status_extended = f"There is an alert configured for creating/updating SQL Server firewall rule in subscription {subscription_name} ({subscription_id})." break else: report = Check_Report_Azure(metadata=self.metadata(), resource={}) - report.subscription = subscription_name - report.resource_name = subscription_name - report.resource_id = ( - f"/subscriptions/{monitor_client.subscriptions[subscription_name]}" - ) + report.subscription = subscription_id + report.resource_name = subscription_id + report.resource_id = f"/subscriptions/{subscription_id}" report.status = "FAIL" - report.status_extended = f"There is not an alert for creating/updating SQL Server firewall rule in subscription {subscription_name}." + report.status_extended = f"There is not an alert for creating/updating SQL Server firewall rule in subscription {subscription_name} ({subscription_id})." findings.append(report) diff --git a/prowler/providers/azure/services/monitor/monitor_alert_delete_nsg/monitor_alert_delete_nsg.py b/prowler/providers/azure/services/monitor/monitor_alert_delete_nsg/monitor_alert_delete_nsg.py index bf4d2eb170..aa4bf8438e 100644 --- a/prowler/providers/azure/services/monitor/monitor_alert_delete_nsg/monitor_alert_delete_nsg.py +++ b/prowler/providers/azure/services/monitor/monitor_alert_delete_nsg/monitor_alert_delete_nsg.py @@ -8,9 +8,12 @@ class monitor_alert_delete_nsg(Check): findings = [] for ( - subscription_name, + subscription_id, activity_log_alerts, ) in monitor_client.alert_rules.items(): + subscription_name = monitor_client.subscriptions.get( + subscription_id, subscription_id + ) for alert_rule in activity_log_alerts: if check_alert_rule( alert_rule, "Microsoft.Network/networkSecurityGroups/delete" @@ -20,19 +23,17 @@ class monitor_alert_delete_nsg(Check): report = Check_Report_Azure( metadata=self.metadata(), resource=alert_rule ) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "PASS" - report.status_extended = f"There is an alert configured for deleting Network Security Groups in subscription {subscription_name}." + report.status_extended = f"There is an alert configured for deleting Network Security Groups in subscription {subscription_name} ({subscription_id})." break else: report = Check_Report_Azure(metadata=self.metadata(), resource={}) - report.subscription = subscription_name - report.resource_name = subscription_name - report.resource_id = ( - f"/subscriptions/{monitor_client.subscriptions[subscription_name]}" - ) + report.subscription = subscription_id + report.resource_name = subscription_id + report.resource_id = f"/subscriptions/{subscription_id}" report.status = "FAIL" - report.status_extended = f"There is not an alert for deleting Network Security Groups in subscription {subscription_name}." + report.status_extended = f"There is not an alert for deleting Network Security Groups in subscription {subscription_name} ({subscription_id})." findings.append(report) diff --git a/prowler/providers/azure/services/monitor/monitor_alert_delete_policy_assignment/monitor_alert_delete_policy_assignment.py b/prowler/providers/azure/services/monitor/monitor_alert_delete_policy_assignment/monitor_alert_delete_policy_assignment.py index cd236de59d..abed374b1e 100644 --- a/prowler/providers/azure/services/monitor/monitor_alert_delete_policy_assignment/monitor_alert_delete_policy_assignment.py +++ b/prowler/providers/azure/services/monitor/monitor_alert_delete_policy_assignment/monitor_alert_delete_policy_assignment.py @@ -8,9 +8,12 @@ class monitor_alert_delete_policy_assignment(Check): findings = [] for ( - subscription_name, + subscription_id, activity_log_alerts, ) in monitor_client.alert_rules.items(): + subscription_name = monitor_client.subscriptions.get( + subscription_id, subscription_id + ) for alert_rule in activity_log_alerts: if check_alert_rule( alert_rule, "Microsoft.Authorization/policyAssignments/delete" @@ -18,19 +21,17 @@ class monitor_alert_delete_policy_assignment(Check): report = Check_Report_Azure( metadata=self.metadata(), resource=alert_rule ) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "PASS" - report.status_extended = f"There is an alert configured for deleting policy assignment in subscription {subscription_name}." + report.status_extended = f"There is an alert configured for deleting policy assignment in subscription {subscription_name} ({subscription_id})." break else: report = Check_Report_Azure(metadata=self.metadata(), resource={}) - report.subscription = subscription_name - report.resource_name = subscription_name - report.resource_id = ( - f"/subscriptions/{monitor_client.subscriptions[subscription_name]}" - ) + report.subscription = subscription_id + report.resource_name = subscription_id + report.resource_id = f"/subscriptions/{subscription_id}" report.status = "FAIL" - report.status_extended = f"There is not an alert for deleting policy assignment in subscription {subscription_name}." + report.status_extended = f"There is not an alert for deleting policy assignment in subscription {subscription_name} ({subscription_id})." findings.append(report) diff --git a/prowler/providers/azure/services/monitor/monitor_alert_delete_public_ip_address_rule/monitor_alert_delete_public_ip_address_rule.py b/prowler/providers/azure/services/monitor/monitor_alert_delete_public_ip_address_rule/monitor_alert_delete_public_ip_address_rule.py index a60a972d65..7ea8420bc0 100644 --- a/prowler/providers/azure/services/monitor/monitor_alert_delete_public_ip_address_rule/monitor_alert_delete_public_ip_address_rule.py +++ b/prowler/providers/azure/services/monitor/monitor_alert_delete_public_ip_address_rule/monitor_alert_delete_public_ip_address_rule.py @@ -8,9 +8,12 @@ class monitor_alert_delete_public_ip_address_rule(Check): findings = [] for ( - subscription_name, + subscription_id, activity_log_alerts, ) in monitor_client.alert_rules.items(): + subscription_name = monitor_client.subscriptions.get( + subscription_id, subscription_id + ) for alert_rule in activity_log_alerts: if check_alert_rule( alert_rule, "Microsoft.Network/publicIPAddresses/delete" @@ -18,19 +21,17 @@ class monitor_alert_delete_public_ip_address_rule(Check): report = Check_Report_Azure( metadata=self.metadata(), resource=alert_rule ) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "PASS" - report.status_extended = f"There is an alert configured for deleting public IP address rule in subscription {subscription_name}." + report.status_extended = f"There is an alert configured for deleting public IP address rule in subscription {subscription_name} ({subscription_id})." break else: report = Check_Report_Azure(metadata=self.metadata(), resource={}) - report.subscription = subscription_name - report.resource_name = subscription_name - report.resource_id = ( - f"/subscriptions/{monitor_client.subscriptions[subscription_name]}" - ) + report.subscription = subscription_id + report.resource_name = subscription_id + report.resource_id = f"/subscriptions/{subscription_id}" report.status = "FAIL" - report.status_extended = f"There is not an alert for deleting public IP address rule in subscription {subscription_name}." + report.status_extended = f"There is not an alert for deleting public IP address rule in subscription {subscription_name} ({subscription_id})." findings.append(report) diff --git a/prowler/providers/azure/services/monitor/monitor_alert_delete_security_solution/monitor_alert_delete_security_solution.py b/prowler/providers/azure/services/monitor/monitor_alert_delete_security_solution/monitor_alert_delete_security_solution.py index 94b0f510e2..975e5ff2df 100644 --- a/prowler/providers/azure/services/monitor/monitor_alert_delete_security_solution/monitor_alert_delete_security_solution.py +++ b/prowler/providers/azure/services/monitor/monitor_alert_delete_security_solution/monitor_alert_delete_security_solution.py @@ -8,9 +8,12 @@ class monitor_alert_delete_security_solution(Check): findings = [] for ( - subscription_name, + subscription_id, activity_log_alerts, ) in monitor_client.alert_rules.items(): + subscription_name = monitor_client.subscriptions.get( + subscription_id, subscription_id + ) for alert_rule in activity_log_alerts: if check_alert_rule( alert_rule, "Microsoft.Security/securitySolutions/delete" @@ -18,19 +21,17 @@ class monitor_alert_delete_security_solution(Check): report = Check_Report_Azure( metadata=self.metadata(), resource=alert_rule ) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "PASS" - report.status_extended = f"There is an alert configured for deleting Security Solution in subscription {subscription_name}." + report.status_extended = f"There is an alert configured for deleting Security Solution in subscription {subscription_name} ({subscription_id})." break else: report = Check_Report_Azure(metadata=self.metadata(), resource={}) - report.subscription = subscription_name - report.resource_name = subscription_name - report.resource_id = ( - f"/subscriptions/{monitor_client.subscriptions[subscription_name]}" - ) + report.subscription = subscription_id + report.resource_name = subscription_id + report.resource_id = f"/subscriptions/{subscription_id}" report.status = "FAIL" - report.status_extended = f"There is not an alert for deleting Security Solution in subscription {subscription_name}." + report.status_extended = f"There is not an alert for deleting Security Solution in subscription {subscription_name} ({subscription_id})." findings.append(report) diff --git a/prowler/providers/azure/services/monitor/monitor_alert_delete_sqlserver_fr/monitor_alert_delete_sqlserver_fr.py b/prowler/providers/azure/services/monitor/monitor_alert_delete_sqlserver_fr/monitor_alert_delete_sqlserver_fr.py index 7b09098aaf..700a0caba1 100644 --- a/prowler/providers/azure/services/monitor/monitor_alert_delete_sqlserver_fr/monitor_alert_delete_sqlserver_fr.py +++ b/prowler/providers/azure/services/monitor/monitor_alert_delete_sqlserver_fr/monitor_alert_delete_sqlserver_fr.py @@ -8,9 +8,12 @@ class monitor_alert_delete_sqlserver_fr(Check): findings = [] for ( - subscription_name, + subscription_id, activity_log_alerts, ) in monitor_client.alert_rules.items(): + subscription_name = monitor_client.subscriptions.get( + subscription_id, subscription_id + ) for alert_rule in activity_log_alerts: if check_alert_rule( alert_rule, "Microsoft.Sql/servers/firewallRules/delete" @@ -18,19 +21,17 @@ class monitor_alert_delete_sqlserver_fr(Check): report = Check_Report_Azure( metadata=self.metadata(), resource=alert_rule ) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "PASS" - report.status_extended = f"There is an alert configured for deleting SQL Server firewall rule in subscription {subscription_name}." + report.status_extended = f"There is an alert configured for deleting SQL Server firewall rule in subscription {subscription_name} ({subscription_id})." break else: report = Check_Report_Azure(metadata=self.metadata(), resource={}) - report.subscription = subscription_name - report.resource_name = subscription_name - report.resource_id = ( - f"/subscriptions/{monitor_client.subscriptions[subscription_name]}" - ) + report.subscription = subscription_id + report.resource_name = subscription_id + report.resource_id = f"/subscriptions/{subscription_id}" report.status = "FAIL" - report.status_extended = f"There is not an alert for deleting SQL Server firewall rule in subscription {subscription_name}." + report.status_extended = f"There is not an alert for deleting SQL Server firewall rule in subscription {subscription_name} ({subscription_id})." findings.append(report) diff --git a/prowler/providers/azure/services/monitor/monitor_alert_service_health_exists/monitor_alert_service_health_exists.py b/prowler/providers/azure/services/monitor/monitor_alert_service_health_exists/monitor_alert_service_health_exists.py index 1a20efcdd3..8eea7dacb2 100644 --- a/prowler/providers/azure/services/monitor/monitor_alert_service_health_exists/monitor_alert_service_health_exists.py +++ b/prowler/providers/azure/services/monitor/monitor_alert_service_health_exists/monitor_alert_service_health_exists.py @@ -7,9 +7,12 @@ class monitor_alert_service_health_exists(Check): findings = [] for ( - subscription_name, + subscription_id, activity_log_alerts, ) in monitor_client.alert_rules.items(): + subscription_name = monitor_client.subscriptions.get( + subscription_id, subscription_id + ) for alert_rule in activity_log_alerts: # Check if alert rule is enabled and has required Service Health conditions if alert_rule.enabled: @@ -31,19 +34,17 @@ class monitor_alert_service_health_exists(Check): report = Check_Report_Azure( metadata=self.metadata(), resource=alert_rule ) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "PASS" - report.status_extended = f"There is an activity log alert for Service Health in subscription {subscription_name}." + report.status_extended = f"There is an activity log alert for Service Health in subscription {subscription_name} ({subscription_id})." break else: report = Check_Report_Azure(metadata=self.metadata(), resource={}) - report.subscription = subscription_name - report.resource_name = subscription_name - report.resource_id = ( - f"/subscriptions/{monitor_client.subscriptions[subscription_name]}" - ) + report.subscription = subscription_id + report.resource_name = subscription_id + report.resource_id = f"/subscriptions/{subscription_id}" report.status = "FAIL" - report.status_extended = f"There is no activity log alert for Service Health in subscription {subscription_name}." + report.status_extended = f"There is no activity log alert for Service Health in subscription {subscription_name} ({subscription_id})." findings.append(report) diff --git a/prowler/providers/azure/services/monitor/monitor_diagnostic_setting_with_appropriate_categories/monitor_diagnostic_setting_with_appropriate_categories.py b/prowler/providers/azure/services/monitor/monitor_diagnostic_setting_with_appropriate_categories/monitor_diagnostic_setting_with_appropriate_categories.py index 0e5ee3f3ec..f0bc2d2f38 100644 --- a/prowler/providers/azure/services/monitor/monitor_diagnostic_setting_with_appropriate_categories/monitor_diagnostic_setting_with_appropriate_categories.py +++ b/prowler/providers/azure/services/monitor/monitor_diagnostic_setting_with_appropriate_categories/monitor_diagnostic_setting_with_appropriate_categories.py @@ -7,9 +7,12 @@ class monitor_diagnostic_setting_with_appropriate_categories(Check): findings = [] for ( - subscription_name, + subscription_id, diagnostic_settings, ) in monitor_client.diagnostics_settings.items(): + subscription_name = monitor_client.subscriptions.get( + subscription_id, subscription_id + ) compliant_setting = None for diagnostic_setting in diagnostic_settings: @@ -41,18 +44,16 @@ class monitor_diagnostic_setting_with_appropriate_categories(Check): report = Check_Report_Azure( metadata=self.metadata(), resource=compliant_setting ) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "PASS" - report.status_extended = f"Diagnostic setting {compliant_setting.name} captures appropriate categories in subscription {subscription_name}." + report.status_extended = f"Diagnostic setting {compliant_setting.name} captures appropriate categories in subscription {subscription_name} ({subscription_id})." else: report = Check_Report_Azure(metadata=self.metadata(), resource={}) - report.subscription = subscription_name - report.resource_name = subscription_name - report.resource_id = ( - f"/subscriptions/{monitor_client.subscriptions[subscription_name]}" - ) + report.subscription = subscription_id + report.resource_name = subscription_id + report.resource_id = f"/subscriptions/{subscription_id}" report.status = "FAIL" - report.status_extended = f"No diagnostic setting captures all appropriate categories (Administrative, Security, Alert, Policy) in subscription {subscription_name}." + report.status_extended = f"No diagnostic setting captures all appropriate categories (Administrative, Security, Alert, Policy) in subscription {subscription_name} ({subscription_id})." findings.append(report) diff --git a/prowler/providers/azure/services/monitor/monitor_diagnostic_settings_exists/monitor_diagnostic_settings_exists.py b/prowler/providers/azure/services/monitor/monitor_diagnostic_settings_exists/monitor_diagnostic_settings_exists.py index c23e7a2af1..1dbe142a28 100644 --- a/prowler/providers/azure/services/monitor/monitor_diagnostic_settings_exists/monitor_diagnostic_settings_exists.py +++ b/prowler/providers/azure/services/monitor/monitor_diagnostic_settings_exists/monitor_diagnostic_settings_exists.py @@ -7,30 +7,29 @@ class monitor_diagnostic_settings_exists(Check): findings = [] for ( - subscription_name, + subscription_id, diagnostic_settings, ) in monitor_client.diagnostics_settings.items(): + subscription_name = monitor_client.subscriptions.get( + subscription_id, subscription_id + ) if diagnostic_settings: # At least one diagnostic setting exists - report on the first one diagnostic_setting = diagnostic_settings[0] report = Check_Report_Azure( metadata=self.metadata(), resource=diagnostic_setting ) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "PASS" - report.status_extended = f"Diagnostic setting {diagnostic_setting.name} found in subscription {subscription_name}." + report.status_extended = f"Diagnostic setting {diagnostic_setting.name} found in subscription {subscription_name} ({subscription_id})." else: # No diagnostic settings - report on subscription report = Check_Report_Azure(metadata=self.metadata(), resource={}) - report.subscription = subscription_name - report.resource_name = subscription_name - report.resource_id = ( - f"/subscriptions/{monitor_client.subscriptions[subscription_name]}" - ) + report.subscription = subscription_id + report.resource_name = subscription_id + report.resource_id = f"/subscriptions/{subscription_id}" report.status = "FAIL" - report.status_extended = ( - f"No diagnostic settings found in subscription {subscription_name}." - ) + report.status_extended = f"No diagnostic settings found in subscription {subscription_name} ({subscription_id})." findings.append(report) diff --git a/prowler/providers/azure/services/monitor/monitor_service.py b/prowler/providers/azure/services/monitor/monitor_service.py index 948b0cceec..07d41d58f2 100644 --- a/prowler/providers/azure/services/monitor/monitor_service.py +++ b/prowler/providers/azure/services/monitor/monitor_service.py @@ -23,13 +23,13 @@ class Monitor(AzureService): try: diagnostics_settings_list = self.diagnostic_settings_with_uri( subscription, - f"subscriptions/{self.subscriptions[subscription]}/", + f"subscriptions/{subscription}/", client, ) diagnostics_settings.update({subscription: diagnostics_settings_list}) except Exception as error: logger.error( - f"Subscription name: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return diagnostics_settings @@ -61,7 +61,7 @@ class Monitor(AzureService): ) except Exception as error: logger.error( - f"Subscription id: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return diagnostics_settings @@ -94,7 +94,7 @@ class Monitor(AzureService): ) except Exception as error: logger.error( - f"Subscription name: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return alert_rules diff --git a/prowler/providers/azure/services/monitor/monitor_storage_account_with_activity_logs_cmk_encrypted/monitor_storage_account_with_activity_logs_cmk_encrypted.py b/prowler/providers/azure/services/monitor/monitor_storage_account_with_activity_logs_cmk_encrypted/monitor_storage_account_with_activity_logs_cmk_encrypted.py index 2400fe2206..135fde0c64 100644 --- a/prowler/providers/azure/services/monitor/monitor_storage_account_with_activity_logs_cmk_encrypted/monitor_storage_account_with_activity_logs_cmk_encrypted.py +++ b/prowler/providers/azure/services/monitor/monitor_storage_account_with_activity_logs_cmk_encrypted/monitor_storage_account_with_activity_logs_cmk_encrypted.py @@ -8,24 +8,25 @@ class monitor_storage_account_with_activity_logs_cmk_encrypted(Check): findings = [] for ( - subscription_name, + subscription_id, diagnostic_settings, ) in monitor_client.diagnostics_settings.items(): + subscription_name = monitor_client.subscriptions.get( + subscription_id, subscription_id + ) for diagnostic_setting in diagnostic_settings: - for storage_account in storage_client.storage_accounts[ - subscription_name - ]: + for storage_account in storage_client.storage_accounts[subscription_id]: if storage_account.name == diagnostic_setting.storage_account_name: report = Check_Report_Azure( metadata=self.metadata(), resource=storage_account ) - report.subscription = subscription_name + report.subscription = subscription_id if storage_account.encryption_type == "Microsoft.Storage": report.status = "FAIL" - report.status_extended = f"Storage account {storage_account.name} storing activity log in subscription {subscription_name} is not encrypted with Customer Managed Key." + report.status_extended = f"Storage account {storage_account.name} storing activity log in subscription {subscription_name} ({subscription_id}) is not encrypted with Customer Managed Key." else: report.status = "PASS" - report.status_extended = f"Storage account {storage_account.name} storing activity log in subscription {subscription_name} is encrypted with Customer Managed Key or not necessary." + report.status_extended = f"Storage account {storage_account.name} storing activity log in subscription {subscription_name} ({subscription_id}) is encrypted with Customer Managed Key or not necessary." findings.append(report) diff --git a/prowler/providers/azure/services/monitor/monitor_storage_account_with_activity_logs_is_private/monitor_storage_account_with_activity_logs_is_private.py b/prowler/providers/azure/services/monitor/monitor_storage_account_with_activity_logs_is_private/monitor_storage_account_with_activity_logs_is_private.py index 0fc6b71768..6008f7d909 100644 --- a/prowler/providers/azure/services/monitor/monitor_storage_account_with_activity_logs_is_private/monitor_storage_account_with_activity_logs_is_private.py +++ b/prowler/providers/azure/services/monitor/monitor_storage_account_with_activity_logs_is_private/monitor_storage_account_with_activity_logs_is_private.py @@ -8,24 +8,25 @@ class monitor_storage_account_with_activity_logs_is_private(Check): findings = [] for ( - subscription_name, + subscription_id, diagnostic_settings, ) in monitor_client.diagnostics_settings.items(): + subscription_name = monitor_client.subscriptions.get( + subscription_id, subscription_id + ) for diagnostic_setting in diagnostic_settings: - for storage_account in storage_client.storage_accounts[ - subscription_name - ]: + for storage_account in storage_client.storage_accounts[subscription_id]: if storage_account.name == diagnostic_setting.storage_account_name: report = Check_Report_Azure( metadata=self.metadata(), resource=storage_account ) - report.subscription = subscription_name + report.subscription = subscription_id if storage_account.allow_blob_public_access: report.status = "FAIL" - report.status_extended = f"Blob public access enabled in storage account {storage_account.name} storing activity logs in subscription {subscription_name}." + report.status_extended = f"Blob public access enabled in storage account {storage_account.name} storing activity logs in subscription {subscription_name} ({subscription_id})." else: report.status = "PASS" - report.status_extended = f"Blob public access disabled in storage account {storage_account.name} storing activity logs in subscription {subscription_name}." + report.status_extended = f"Blob public access disabled in storage account {storage_account.name} storing activity logs in subscription {subscription_name} ({subscription_id})." findings.append(report) diff --git a/prowler/providers/azure/services/mysql/mysql_flexible_server_audit_log_connection_activated/mysql_flexible_server_audit_log_connection_activated.py b/prowler/providers/azure/services/mysql/mysql_flexible_server_audit_log_connection_activated/mysql_flexible_server_audit_log_connection_activated.py index 5071da4b20..d29a2e0879 100644 --- a/prowler/providers/azure/services/mysql/mysql_flexible_server_audit_log_connection_activated/mysql_flexible_server_audit_log_connection_activated.py +++ b/prowler/providers/azure/services/mysql/mysql_flexible_server_audit_log_connection_activated/mysql_flexible_server_audit_log_connection_activated.py @@ -7,14 +7,17 @@ class mysql_flexible_server_audit_log_connection_activated(Check): findings = [] for ( - subscription_name, + subscription_id, servers, ) in mysql_client.flexible_servers.items(): + subscription_name = mysql_client.subscriptions.get( + subscription_id, subscription_id + ) for server in servers.values(): report = Check_Report_Azure(metadata=self.metadata(), resource=server) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "FAIL" - report.status_extended = f"Audit log is disabled for server {server.name} in subscription {subscription_name}." + report.status_extended = f"Audit log is disabled for server {server.name} in subscription {subscription_name} ({subscription_id})." if "audit_log_events" in server.configurations: report.resource_id = server.configurations[ @@ -25,7 +28,7 @@ class mysql_flexible_server_audit_log_connection_activated(Check): "audit_log_events" ].value.lower().split(","): report.status = "PASS" - report.status_extended = f"Audit log is enabled for server {server.name} in subscription {subscription_name}." + report.status_extended = f"Audit log is enabled for server {server.name} in subscription {subscription_name} ({subscription_id})." findings.append(report) diff --git a/prowler/providers/azure/services/mysql/mysql_flexible_server_audit_log_enabled/mysql_flexible_server_audit_log_enabled.py b/prowler/providers/azure/services/mysql/mysql_flexible_server_audit_log_enabled/mysql_flexible_server_audit_log_enabled.py index 81918f7756..c7a33bae44 100644 --- a/prowler/providers/azure/services/mysql/mysql_flexible_server_audit_log_enabled/mysql_flexible_server_audit_log_enabled.py +++ b/prowler/providers/azure/services/mysql/mysql_flexible_server_audit_log_enabled/mysql_flexible_server_audit_log_enabled.py @@ -7,14 +7,17 @@ class mysql_flexible_server_audit_log_enabled(Check): findings = [] for ( - subscription_name, + subscription_id, servers, ) in mysql_client.flexible_servers.items(): + subscription_name = mysql_client.subscriptions.get( + subscription_id, subscription_id + ) for server in servers.values(): report = Check_Report_Azure(metadata=self.metadata(), resource=server) report.status = "FAIL" - report.subscription = subscription_name - report.status_extended = f"Audit log is disabled for server {server.name} in subscription {subscription_name}." + report.subscription = subscription_id + report.status_extended = f"Audit log is disabled for server {server.name} in subscription {subscription_name} ({subscription_id})." if "audit_log_enabled" in server.configurations: report.resource_id = server.configurations[ @@ -23,7 +26,7 @@ class mysql_flexible_server_audit_log_enabled(Check): if server.configurations["audit_log_enabled"].value.lower() == "on": report.status = "PASS" - report.status_extended = f"Audit log is enabled for server {server.name} in subscription {subscription_name}." + report.status_extended = f"Audit log is enabled for server {server.name} in subscription {subscription_name} ({subscription_id})." findings.append(report) diff --git a/prowler/providers/azure/services/mysql/mysql_flexible_server_minimum_tls_version_12/mysql_flexible_server_minimum_tls_version_12.py b/prowler/providers/azure/services/mysql/mysql_flexible_server_minimum_tls_version_12/mysql_flexible_server_minimum_tls_version_12.py index dbd12bd344..d9aa962c92 100644 --- a/prowler/providers/azure/services/mysql/mysql_flexible_server_minimum_tls_version_12/mysql_flexible_server_minimum_tls_version_12.py +++ b/prowler/providers/azure/services/mysql/mysql_flexible_server_minimum_tls_version_12/mysql_flexible_server_minimum_tls_version_12.py @@ -7,27 +7,30 @@ class mysql_flexible_server_minimum_tls_version_12(Check): findings = [] for ( - subscription_name, + subscription_id, servers, ) in mysql_client.flexible_servers.items(): + subscription_name = mysql_client.subscriptions.get( + subscription_id, subscription_id + ) for server in servers.values(): report = Check_Report_Azure(metadata=self.metadata(), resource=server) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "FAIL" - report.status_extended = f"TLS version is not configured in server {server.name} in subscription {subscription_name}." + report.status_extended = f"TLS version is not configured in server {server.name} in subscription {subscription_name} ({subscription_id})." if "tls_version" in server.configurations: report.resource_id = server.configurations[ "tls_version" ].resource_id report.status = "PASS" - report.status_extended = f"TLS version is {server.configurations['tls_version'].value} in server {server.name} in subscription {subscription_name}. This version of TLS is considered secure." + report.status_extended = f"TLS version is {server.configurations['tls_version'].value} in server {server.name} in subscription {subscription_name} ({subscription_id}). This version of TLS is considered secure." tls_aviable = server.configurations["tls_version"].value.split(",") if "TLSv1.0" in tls_aviable or "TLSv1.1" in tls_aviable: report.status = "FAIL" - report.status_extended = f"TLS version is {server.configurations['tls_version'].value} in server {server.name} in subscription {subscription_name}. There is at leat one version of TLS that is considered insecure." + report.status_extended = f"TLS version is {server.configurations['tls_version'].value} in server {server.name} in subscription {subscription_name} ({subscription_id}). There is at leat one version of TLS that is considered insecure." findings.append(report) diff --git a/prowler/providers/azure/services/mysql/mysql_flexible_server_ssl_connection_enabled/mysql_flexible_server_ssl_connection_enabled.py b/prowler/providers/azure/services/mysql/mysql_flexible_server_ssl_connection_enabled/mysql_flexible_server_ssl_connection_enabled.py index 79930de947..03a32736e3 100644 --- a/prowler/providers/azure/services/mysql/mysql_flexible_server_ssl_connection_enabled/mysql_flexible_server_ssl_connection_enabled.py +++ b/prowler/providers/azure/services/mysql/mysql_flexible_server_ssl_connection_enabled/mysql_flexible_server_ssl_connection_enabled.py @@ -7,14 +7,17 @@ class mysql_flexible_server_ssl_connection_enabled(Check): findings = [] for ( - subscription_name, + subscription_id, servers, ) in mysql_client.flexible_servers.items(): + subscription_name = mysql_client.subscriptions.get( + subscription_id, subscription_id + ) for server in servers.values(): report = Check_Report_Azure(metadata=self.metadata(), resource=server) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "FAIL" - report.status_extended = f"SSL connection is disabled for server {server.name} in subscription {subscription_name}." + report.status_extended = f"SSL connection is disabled for server {server.name} in subscription {subscription_name} ({subscription_id})." if "require_secure_transport" in server.configurations: report.resource_id = server.configurations[ @@ -25,7 +28,7 @@ class mysql_flexible_server_ssl_connection_enabled(Check): == "on" ): report.status = "PASS" - report.status_extended = f"SSL connection is enabled for server {server.name} in subscription {subscription_name}." + report.status_extended = f"SSL connection is enabled for server {server.name} in subscription {subscription_name} ({subscription_id})." findings.append(report) diff --git a/prowler/providers/azure/services/mysql/mysql_service.py b/prowler/providers/azure/services/mysql/mysql_service.py index d2f152a492..565e14da01 100644 --- a/prowler/providers/azure/services/mysql/mysql_service.py +++ b/prowler/providers/azure/services/mysql/mysql_service.py @@ -16,12 +16,12 @@ class MySQL(AzureService): def _get_flexible_servers(self): logger.info("MySQL - Getting servers...") servers = {} - for subscription_name, client in self.clients.items(): + for subscription_id, client in self.clients.items(): try: servers_list = client.servers.list() - servers.update({subscription_name: {}}) + servers.update({subscription_id: {}}) for server in servers_list: - servers[subscription_name].update( + servers[subscription_id].update( { server.id: FlexibleServer( resource_id=server.id, @@ -36,7 +36,7 @@ class MySQL(AzureService): ) except Exception as error: logger.error( - f"Subscription name: {subscription_name} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription_id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return servers diff --git a/prowler/providers/azure/services/network/network_bastion_host_exists/network_bastion_host_exists.py b/prowler/providers/azure/services/network/network_bastion_host_exists/network_bastion_host_exists.py index 85e815b3ab..1efc4bde99 100644 --- a/prowler/providers/azure/services/network/network_bastion_host_exists/network_bastion_host_exists.py +++ b/prowler/providers/azure/services/network/network_bastion_host_exists/network_bastion_host_exists.py @@ -6,17 +6,16 @@ class network_bastion_host_exists(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, bastion_hosts in network_client.bastion_hosts.items(): + subscription_name = network_client.subscriptions.get( + subscription, subscription + ) if not bastion_hosts: report = Check_Report_Azure(metadata=self.metadata(), resource={}) report.subscription = subscription report.resource_name = subscription - report.resource_id = ( - f"/subscriptions/{network_client.subscriptions[subscription]}" - ) + report.resource_id = f"/subscriptions/{subscription}" report.status = "FAIL" - report.status_extended = ( - f"Bastion Host from subscription {subscription} does not exist" - ) + report.status_extended = f"Bastion Host from subscription {subscription_name} ({subscription}) does not exist" findings.append(report) else: for bastion_host in bastion_hosts: @@ -25,7 +24,7 @@ class network_bastion_host_exists(Check): ) report.subscription = subscription report.status = "PASS" - report.status_extended = f"Bastion Host {bastion_host.name} exists in subscription {subscription}." + report.status_extended = f"Bastion Host {bastion_host.name} exists in subscription {subscription_name} ({subscription})." findings.append(report) return findings diff --git a/prowler/providers/azure/services/network/network_flow_log_captured_sent/network_flow_log_captured_sent.py b/prowler/providers/azure/services/network/network_flow_log_captured_sent/network_flow_log_captured_sent.py index 7200779202..832fa105c3 100644 --- a/prowler/providers/azure/services/network/network_flow_log_captured_sent/network_flow_log_captured_sent.py +++ b/prowler/providers/azure/services/network/network_flow_log_captured_sent/network_flow_log_captured_sent.py @@ -6,6 +6,9 @@ class network_flow_log_captured_sent(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, network_watchers in network_client.network_watchers.items(): + subscription_name = network_client.subscriptions.get( + subscription, subscription + ) for network_watcher in network_watchers: report = Check_Report_Azure( metadata=self.metadata(), resource=network_watcher @@ -13,24 +16,24 @@ class network_flow_log_captured_sent(Check): report.subscription = subscription if network_watcher.flow_logs: report.status = "PASS" - report.status_extended = f"Network Watcher {network_watcher.name} from subscription {subscription} has flow logs that are captured and sent to Log Analytics workspace" + report.status_extended = f"Network Watcher {network_watcher.name} from subscription {subscription_name} ({subscription}) has flow logs that are captured and sent to Log Analytics workspace" has_failed = False for flow_log in network_watcher.flow_logs: if not has_failed: if not flow_log.enabled: report.status = "FAIL" - report.status_extended = f"Network Watcher {network_watcher.name} from subscription {subscription} has flow logs disabled" + report.status_extended = f"Network Watcher {network_watcher.name} from subscription {subscription_name} ({subscription}) has flow logs disabled" has_failed = True elif not ( flow_log.traffic_analytics_enabled and flow_log.workspace_resource_id ): report.status = "FAIL" - report.status_extended = f"Network Watcher {network_watcher.name} from subscription {subscription} has enabled flow logs that are not configured to send traffic analytics to a Log Analytics workspace" + report.status_extended = f"Network Watcher {network_watcher.name} from subscription {subscription_name} ({subscription}) has enabled flow logs that are not configured to send traffic analytics to a Log Analytics workspace" has_failed = True else: report.status = "FAIL" - report.status_extended = f"Network Watcher {network_watcher.name} from subscription {subscription} has no flow logs" + report.status_extended = f"Network Watcher {network_watcher.name} from subscription {subscription_name} ({subscription}) has no flow logs" findings.append(report) diff --git a/prowler/providers/azure/services/network/network_flow_log_more_than_90_days/network_flow_log_more_than_90_days.py b/prowler/providers/azure/services/network/network_flow_log_more_than_90_days/network_flow_log_more_than_90_days.py index 69d17b5e0a..5030e35211 100644 --- a/prowler/providers/azure/services/network/network_flow_log_more_than_90_days/network_flow_log_more_than_90_days.py +++ b/prowler/providers/azure/services/network/network_flow_log_more_than_90_days/network_flow_log_more_than_90_days.py @@ -6,6 +6,9 @@ class network_flow_log_more_than_90_days(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, network_watchers in network_client.network_watchers.items(): + subscription_name = network_client.subscriptions.get( + subscription, subscription + ) for network_watcher in network_watchers: report = Check_Report_Azure( metadata=self.metadata(), resource=network_watcher @@ -13,24 +16,24 @@ class network_flow_log_more_than_90_days(Check): report.subscription = subscription if network_watcher.flow_logs: report.status = "PASS" - report.status_extended = f"Network Watcher {network_watcher.name} from subscription {subscription} has flow logs enabled for more than 90 days" + report.status_extended = f"Network Watcher {network_watcher.name} from subscription {subscription_name} ({subscription}) has flow logs enabled for more than 90 days" has_failed = False for flow_log in network_watcher.flow_logs: if not has_failed: if not flow_log.enabled: report.status = "FAIL" - report.status_extended = f"Network Watcher {network_watcher.name} from subscription {subscription} has flow logs disabled" + report.status_extended = f"Network Watcher {network_watcher.name} from subscription {subscription_name} ({subscription}) has flow logs disabled" has_failed = True elif ( flow_log.retention_policy.days < 90 and flow_log.retention_policy.days != 0 ) and not has_failed: report.status = "FAIL" - report.status_extended = f"Network Watcher {network_watcher.name} from subscription {subscription} flow logs retention policy is less than 90 days" + report.status_extended = f"Network Watcher {network_watcher.name} from subscription {subscription_name} ({subscription}) flow logs retention policy is less than 90 days" has_failed = True else: report.status = "FAIL" - report.status_extended = f"Network Watcher {network_watcher.name} from subscription {subscription} has no flow logs" + report.status_extended = f"Network Watcher {network_watcher.name} from subscription {subscription_name} ({subscription}) has no flow logs" findings.append(report) return findings diff --git a/prowler/providers/azure/services/network/network_http_internet_access_restricted/network_http_internet_access_restricted.py b/prowler/providers/azure/services/network/network_http_internet_access_restricted/network_http_internet_access_restricted.py index 61f018bae4..1539c3534f 100644 --- a/prowler/providers/azure/services/network/network_http_internet_access_restricted/network_http_internet_access_restricted.py +++ b/prowler/providers/azure/services/network/network_http_internet_access_restricted/network_http_internet_access_restricted.py @@ -6,13 +6,16 @@ class network_http_internet_access_restricted(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, security_groups in network_client.security_groups.items(): + subscription_name = network_client.subscriptions.get( + subscription, subscription + ) for security_group in security_groups: report = Check_Report_Azure( metadata=self.metadata(), resource=security_group ) report.subscription = subscription report.status = "PASS" - report.status_extended = f"Security Group {security_group.name} from subscription {subscription} has HTTP internet access restricted." + report.status_extended = f"Security Group {security_group.name} from subscription {subscription_name} ({subscription}) has HTTP internet access restricted." rule_fail_condition = any( ( rule.destination_port_range == "80" @@ -33,7 +36,7 @@ class network_http_internet_access_restricted(Check): ) if rule_fail_condition: report.status = "FAIL" - report.status_extended = f"Security Group {security_group.name} from subscription {subscription} has HTTP internet access allowed." + report.status_extended = f"Security Group {security_group.name} from subscription {subscription_name} ({subscription}) has HTTP internet access allowed." findings.append(report) return findings diff --git a/prowler/providers/azure/services/network/network_public_ip_shodan/network_public_ip_shodan.py b/prowler/providers/azure/services/network/network_public_ip_shodan/network_public_ip_shodan.py index 83340768eb..98b32a0c3b 100644 --- a/prowler/providers/azure/services/network/network_public_ip_shodan/network_public_ip_shodan.py +++ b/prowler/providers/azure/services/network/network_public_ip_shodan/network_public_ip_shodan.py @@ -12,20 +12,21 @@ class network_public_ip_shodan(Check): if shodan_api_key: api = shodan.Shodan(shodan_api_key) for subscription, public_ips in network_client.public_ip_addresses.items(): + subscription_name = network_client.subscriptions.get( + subscription, subscription + ) for ip in public_ips: report = Check_Report_Azure(metadata=self.metadata(), resource=ip) report.subscription = subscription try: shodan_info = api.host(ip.ip_address) report.status = "FAIL" - report.status_extended = f"Public IP {ip.ip_address} listed in Shodan with open ports {str(shodan_info['ports'])} and ISP {shodan_info['isp']} in {shodan_info['country_name']}. More info at https://www.shodan.io/host/{ip.ip_address}." + report.status_extended = f"Public IP {ip.ip_address} from subscription {subscription_name} ({subscription}) listed in Shodan with open ports {str(shodan_info['ports'])} and ISP {shodan_info['isp']} in {shodan_info['country_name']}. More info at https://www.shodan.io/host/{ip.ip_address}." findings.append(report) except shodan.APIError as error: if "No information available for that IP" in error.value: report.status = "PASS" - report.status_extended = ( - f"Public IP {ip.ip_address} is not listed in Shodan." - ) + report.status_extended = f"Public IP {ip.ip_address} from subscription {subscription_name} ({subscription}) is not listed in Shodan." findings.append(report) continue else: diff --git a/prowler/providers/azure/services/network/network_rdp_internet_access_restricted/network_rdp_internet_access_restricted.py b/prowler/providers/azure/services/network/network_rdp_internet_access_restricted/network_rdp_internet_access_restricted.py index 7d08678d27..2b321ed0c8 100644 --- a/prowler/providers/azure/services/network/network_rdp_internet_access_restricted/network_rdp_internet_access_restricted.py +++ b/prowler/providers/azure/services/network/network_rdp_internet_access_restricted/network_rdp_internet_access_restricted.py @@ -6,13 +6,16 @@ class network_rdp_internet_access_restricted(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, security_groups in network_client.security_groups.items(): + subscription_name = network_client.subscriptions.get( + subscription, subscription + ) for security_group in security_groups: report = Check_Report_Azure( metadata=self.metadata(), resource=security_group ) report.subscription = subscription report.status = "PASS" - report.status_extended = f"Security Group {security_group.name} from subscription {subscription} has RDP internet access restricted." + report.status_extended = f"Security Group {security_group.name} from subscription {subscription_name} ({subscription}) has RDP internet access restricted." rule_fail_condition = any( ( rule.destination_port_range == "3389" @@ -33,7 +36,7 @@ class network_rdp_internet_access_restricted(Check): ) if rule_fail_condition: report.status = "FAIL" - report.status_extended = f"Security Group {security_group.name} from subscription {subscription} has RDP internet access allowed." + report.status_extended = f"Security Group {security_group.name} from subscription {subscription_name} ({subscription}) has RDP internet access allowed." findings.append(report) return findings diff --git a/prowler/providers/azure/services/network/network_service.py b/prowler/providers/azure/services/network/network_service.py index fdbb32f088..0eb6144534 100644 --- a/prowler/providers/azure/services/network/network_service.py +++ b/prowler/providers/azure/services/network/network_service.py @@ -54,7 +54,7 @@ class Network(AzureService): except Exception as error: logger.error( - f"Subscription name: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return security_groups @@ -130,7 +130,7 @@ class Network(AzureService): except Exception as error: logger.error( - f"Subscription name: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return network_watchers @@ -149,12 +149,12 @@ class Network(AzureService): return flow_logs except ResourceNotFoundError as error: logger.warning( - f"Subscription name: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return [] except Exception as error: logger.error( - f"Subscription name: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return [] @@ -176,7 +176,7 @@ class Network(AzureService): except Exception as error: logger.error( - f"Subscription name: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return bastion_hosts @@ -199,7 +199,7 @@ class Network(AzureService): except Exception as error: logger.error( - f"Subscription name: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return public_ip_addresses diff --git a/prowler/providers/azure/services/network/network_ssh_internet_access_restricted/network_ssh_internet_access_restricted.py b/prowler/providers/azure/services/network/network_ssh_internet_access_restricted/network_ssh_internet_access_restricted.py index e4207194e1..f6ad26b9ef 100644 --- a/prowler/providers/azure/services/network/network_ssh_internet_access_restricted/network_ssh_internet_access_restricted.py +++ b/prowler/providers/azure/services/network/network_ssh_internet_access_restricted/network_ssh_internet_access_restricted.py @@ -6,13 +6,16 @@ class network_ssh_internet_access_restricted(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, security_groups in network_client.security_groups.items(): + subscription_name = network_client.subscriptions.get( + subscription, subscription + ) for security_group in security_groups: report = Check_Report_Azure( metadata=self.metadata(), resource=security_group ) report.subscription = subscription report.status = "PASS" - report.status_extended = f"Security Group {security_group.name} from subscription {subscription} has SSH internet access restricted." + report.status_extended = f"Security Group {security_group.name} from subscription {subscription_name} ({subscription}) has SSH internet access restricted." rule_fail_condition = any( ( rule.destination_port_range == "22" @@ -33,7 +36,7 @@ class network_ssh_internet_access_restricted(Check): ) if rule_fail_condition: report.status = "FAIL" - report.status_extended = f"Security Group {security_group.name} from subscription {subscription} has SSH internet access allowed." + report.status_extended = f"Security Group {security_group.name} from subscription {subscription_name} ({subscription}) has SSH internet access allowed." findings.append(report) return findings diff --git a/prowler/providers/azure/services/network/network_udp_internet_access_restricted/network_udp_internet_access_restricted.py b/prowler/providers/azure/services/network/network_udp_internet_access_restricted/network_udp_internet_access_restricted.py index ebd5fc7d50..94e177eb24 100644 --- a/prowler/providers/azure/services/network/network_udp_internet_access_restricted/network_udp_internet_access_restricted.py +++ b/prowler/providers/azure/services/network/network_udp_internet_access_restricted/network_udp_internet_access_restricted.py @@ -6,13 +6,16 @@ class network_udp_internet_access_restricted(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, security_groups in network_client.security_groups.items(): + subscription_name = network_client.subscriptions.get( + subscription, subscription + ) for security_group in security_groups: report = Check_Report_Azure( metadata=self.metadata(), resource=security_group ) report.subscription = subscription report.status = "PASS" - report.status_extended = f"Security Group {security_group.name} from subscription {subscription} has UDP internet access restricted." + report.status_extended = f"Security Group {security_group.name} from subscription {subscription_name} ({subscription}) has UDP internet access restricted." rule_fail_condition = any( ( rule.protocol in ["UDP", "Udp"] @@ -28,7 +31,7 @@ class network_udp_internet_access_restricted(Check): ) if rule_fail_condition: report.status = "FAIL" - report.status_extended = f"Security Group {security_group.name} from subscription {subscription} has UDP internet access allowed." + report.status_extended = f"Security Group {security_group.name} from subscription {subscription_name} ({subscription}) has UDP internet access allowed." findings.append(report) return findings diff --git a/prowler/providers/azure/services/network/network_watcher_enabled/network_watcher_enabled.py b/prowler/providers/azure/services/network/network_watcher_enabled/network_watcher_enabled.py index 5235ac0d73..b2a19f2bca 100644 --- a/prowler/providers/azure/services/network/network_watcher_enabled/network_watcher_enabled.py +++ b/prowler/providers/azure/services/network/network_watcher_enabled/network_watcher_enabled.py @@ -6,6 +6,9 @@ class network_watcher_enabled(Check): def execute(self) -> list[Check_Report_Azure]: findings = [] for subscription, network_watchers in network_client.network_watchers.items(): + subscription_name = network_client.subscriptions.get( + subscription, subscription + ) missing_locations = set(network_client.locations[subscription]) - set( network_watcher.location for network_watcher in network_watchers ) @@ -15,12 +18,10 @@ class network_watcher_enabled(Check): report = Check_Report_Azure(metadata=self.metadata(), resource={}) report.subscription = subscription report.resource_name = subscription - report.resource_id = ( - f"/subscriptions/{network_client.subscriptions[subscription]}" - ) + report.resource_id = f"/subscriptions/{subscription}" report.location = "global" report.status = "FAIL" - report.status_extended = f"Network Watcher is not enabled for the following locations in subscription '{subscription}': {', '.join(missing_locations)}." + report.status_extended = f"Network Watcher is not enabled for the following locations in subscription '{subscription_name} ({subscription})': {', '.join(missing_locations)}." findings.append(report) else: # Report each network watcher that exists @@ -30,7 +31,7 @@ class network_watcher_enabled(Check): ) report.subscription = subscription report.status = "PASS" - report.status_extended = f"Network Watcher {network_watcher.name} is enabled in location {network_watcher.location} in subscription '{subscription}'." + report.status_extended = f"Network Watcher {network_watcher.name} is enabled in location {network_watcher.location} in subscription '{subscription_name} ({subscription})'." findings.append(report) return findings diff --git a/prowler/providers/azure/services/policy/policy_ensure_asc_enforcement_enabled/policy_ensure_asc_enforcement_enabled.py b/prowler/providers/azure/services/policy/policy_ensure_asc_enforcement_enabled/policy_ensure_asc_enforcement_enabled.py index ba9ccf1eea..4c36663a1f 100644 --- a/prowler/providers/azure/services/policy/policy_ensure_asc_enforcement_enabled/policy_ensure_asc_enforcement_enabled.py +++ b/prowler/providers/azure/services/policy/policy_ensure_asc_enforcement_enabled/policy_ensure_asc_enforcement_enabled.py @@ -6,18 +6,21 @@ class policy_ensure_asc_enforcement_enabled(Check): def execute(self) -> Check_Report_Azure: findings = [] - for subscription_name, policies in policy_client.policy_assigments.items(): + for subscription_id, policies in policy_client.policy_assigments.items(): + subscription_name = policy_client.subscriptions.get( + subscription_id, subscription_id + ) if "SecurityCenterBuiltIn" in policies: report = Check_Report_Azure( metadata=self.metadata(), resource=policies["SecurityCenterBuiltIn"], ) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "PASS" - report.status_extended = f"Policy assigment '{policies['SecurityCenterBuiltIn'].id}' is configured with enforcement mode '{policies['SecurityCenterBuiltIn'].enforcement_mode}'." + report.status_extended = f"Policy assigment '{policies['SecurityCenterBuiltIn'].id}' from subscription {subscription_name} ({subscription_id}) is configured with enforcement mode '{policies['SecurityCenterBuiltIn'].enforcement_mode}'." if policies["SecurityCenterBuiltIn"].enforcement_mode != "Default": report.status = "FAIL" - report.status_extended = f"Policy assigment '{policies['SecurityCenterBuiltIn'].id}' is not configured with enforcement mode Default." + report.status_extended = f"Policy assigment '{policies['SecurityCenterBuiltIn'].id}' from subscription {subscription_name} ({subscription_id}) is not configured with enforcement mode Default." findings.append(report) diff --git a/prowler/providers/azure/services/policy/policy_service.py b/prowler/providers/azure/services/policy/policy_service.py index c7950f6d17..1d1381202f 100644 --- a/prowler/providers/azure/services/policy/policy_service.py +++ b/prowler/providers/azure/services/policy/policy_service.py @@ -16,13 +16,13 @@ class Policy(AzureService): logger.info("Policy - Getting policy assigments...") policy_assigments = {} - for subscription_name, client in self.clients.items(): + for subscription_id, client in self.clients.items(): try: policy_assigments_list = client.policy_assignments.list() - policy_assigments.update({subscription_name: {}}) + policy_assigments.update({subscription_id: {}}) for policy_assigment in policy_assigments_list: - policy_assigments[subscription_name].update( + policy_assigments[subscription_id].update( { policy_assigment.name: PolicyAssigment( id=policy_assigment.id, @@ -33,7 +33,7 @@ class Policy(AzureService): ) except Exception as error: logger.error( - f"Subscription name: {subscription_name} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription_id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return policy_assigments diff --git a/prowler/providers/azure/services/postgresql/postgresql_flexible_server_allow_access_services_disabled/postgresql_flexible_server_allow_access_services_disabled.py b/prowler/providers/azure/services/postgresql/postgresql_flexible_server_allow_access_services_disabled/postgresql_flexible_server_allow_access_services_disabled.py index fc78091b5b..0015b959f2 100644 --- a/prowler/providers/azure/services/postgresql/postgresql_flexible_server_allow_access_services_disabled/postgresql_flexible_server_allow_access_services_disabled.py +++ b/prowler/providers/azure/services/postgresql/postgresql_flexible_server_allow_access_services_disabled/postgresql_flexible_server_allow_access_services_disabled.py @@ -11,17 +11,20 @@ class postgresql_flexible_server_allow_access_services_disabled(Check): subscription, flexible_servers, ) in postgresql_client.flexible_servers.items(): + subscription_name = postgresql_client.subscriptions.get( + subscription, subscription + ) for server in flexible_servers: report = Check_Report_Azure(metadata=self.metadata(), resource=server) report.subscription = subscription report.status = "FAIL" - report.status_extended = f"Flexible Postgresql server {server.name} from subscription {subscription} has allow public access from any Azure service enabled" + report.status_extended = f"Flexible Postgresql server {server.name} from subscription {subscription_name} ({subscription}) has allow public access from any Azure service enabled" if not any( rule.start_ip == "0.0.0.0" and rule.end_ip == "0.0.0.0" for rule in server.firewall ): report.status = "PASS" - report.status_extended = f"Flexible Postgresql server {server.name} from subscription {subscription} has allow public access from any Azure service disabled" + report.status_extended = f"Flexible Postgresql server {server.name} from subscription {subscription_name} ({subscription}) has allow public access from any Azure service disabled" findings.append(report) return findings diff --git a/prowler/providers/azure/services/postgresql/postgresql_flexible_server_connection_throttling_on/postgresql_flexible_server_connection_throttling_on.py b/prowler/providers/azure/services/postgresql/postgresql_flexible_server_connection_throttling_on/postgresql_flexible_server_connection_throttling_on.py index a395f605b8..763317d405 100644 --- a/prowler/providers/azure/services/postgresql/postgresql_flexible_server_connection_throttling_on/postgresql_flexible_server_connection_throttling_on.py +++ b/prowler/providers/azure/services/postgresql/postgresql_flexible_server_connection_throttling_on/postgresql_flexible_server_connection_throttling_on.py @@ -11,14 +11,17 @@ class postgresql_flexible_server_connection_throttling_on(Check): subscription, flexible_servers, ) in postgresql_client.flexible_servers.items(): + subscription_name = postgresql_client.subscriptions.get( + subscription, subscription + ) for server in flexible_servers: report = Check_Report_Azure(metadata=self.metadata(), resource=server) report.subscription = subscription report.status = "FAIL" - report.status_extended = f"Flexible Postgresql server {server.name} from subscription {subscription} has connection_throttling disabled" + report.status_extended = f"Flexible Postgresql server {server.name} from subscription {subscription_name} ({subscription}) has connection_throttling disabled" if server.connection_throttling == "ON": report.status = "PASS" - report.status_extended = f"Flexible Postgresql server {server.name} from subscription {subscription} has connection_throttling enabled" + report.status_extended = f"Flexible Postgresql server {server.name} from subscription {subscription_name} ({subscription}) has connection_throttling enabled" findings.append(report) return findings diff --git a/prowler/providers/azure/services/postgresql/postgresql_flexible_server_enforce_ssl_enabled/postgresql_flexible_server_enforce_ssl_enabled.py b/prowler/providers/azure/services/postgresql/postgresql_flexible_server_enforce_ssl_enabled/postgresql_flexible_server_enforce_ssl_enabled.py index 35952cd9a0..b5fa583699 100644 --- a/prowler/providers/azure/services/postgresql/postgresql_flexible_server_enforce_ssl_enabled/postgresql_flexible_server_enforce_ssl_enabled.py +++ b/prowler/providers/azure/services/postgresql/postgresql_flexible_server_enforce_ssl_enabled/postgresql_flexible_server_enforce_ssl_enabled.py @@ -11,14 +11,17 @@ class postgresql_flexible_server_enforce_ssl_enabled(Check): subscription, flexible_servers, ) in postgresql_client.flexible_servers.items(): + subscription_name = postgresql_client.subscriptions.get( + subscription, subscription + ) for server in flexible_servers: report = Check_Report_Azure(metadata=self.metadata(), resource=server) report.subscription = subscription report.status = "FAIL" - report.status_extended = f"Flexible Postgresql server {server.name} from subscription {subscription} has enforce ssl disabled" + report.status_extended = f"Flexible Postgresql server {server.name} from subscription {subscription_name} ({subscription}) has enforce ssl disabled" if server.require_secure_transport == "ON": report.status = "PASS" - report.status_extended = f"Flexible Postgresql server {server.name} from subscription {subscription} has enforce ssl enabled" + report.status_extended = f"Flexible Postgresql server {server.name} from subscription {subscription_name} ({subscription}) has enforce ssl enabled" findings.append(report) return findings diff --git a/prowler/providers/azure/services/postgresql/postgresql_flexible_server_entra_id_authentication_enabled/postgresql_flexible_server_entra_id_authentication_enabled.py b/prowler/providers/azure/services/postgresql/postgresql_flexible_server_entra_id_authentication_enabled/postgresql_flexible_server_entra_id_authentication_enabled.py index c87df50bfa..15aa6b2225 100644 --- a/prowler/providers/azure/services/postgresql/postgresql_flexible_server_entra_id_authentication_enabled/postgresql_flexible_server_entra_id_authentication_enabled.py +++ b/prowler/providers/azure/services/postgresql/postgresql_flexible_server_entra_id_authentication_enabled/postgresql_flexible_server_entra_id_authentication_enabled.py @@ -11,6 +11,9 @@ class postgresql_flexible_server_entra_id_authentication_enabled(Check): subscription, flexible_servers, ) in postgresql_client.flexible_servers.items(): + subscription_name = postgresql_client.subscriptions.get( + subscription, subscription + ) for server in flexible_servers: report = Check_Report_Azure(metadata=self.metadata(), resource=server) report.subscription = subscription @@ -23,7 +26,7 @@ class postgresql_flexible_server_entra_id_authentication_enabled(Check): not server.active_directory_auth or server.active_directory_auth != "ENABLED" ): - report.status_extended = f"Flexible Postgresql server {server.name} from subscription {subscription} has Microsoft Entra ID authentication disabled" + report.status_extended = f"Flexible Postgresql server {server.name} from subscription {subscription_name} ({subscription}) has Microsoft Entra ID authentication disabled" else: # Authentication is enabled, now check for admins admin_count = ( @@ -31,13 +34,13 @@ class postgresql_flexible_server_entra_id_authentication_enabled(Check): ) if admin_count == 0: - report.status_extended = f"Flexible Postgresql server {server.name} from subscription {subscription} has Microsoft Entra ID authentication enabled but no Entra ID administrators configured" + report.status_extended = f"Flexible Postgresql server {server.name} from subscription {subscription_name} ({subscription}) has Microsoft Entra ID authentication enabled but no Entra ID administrators configured" else: report.status = "PASS" admin_text = ( "administrator" if admin_count == 1 else "administrators" ) - report.status_extended = f"Flexible Postgresql server {server.name} from subscription {subscription} has Microsoft Entra ID authentication enabled with {admin_count} {admin_text} configured" + report.status_extended = f"Flexible Postgresql server {server.name} from subscription {subscription_name} ({subscription}) has Microsoft Entra ID authentication enabled with {admin_count} {admin_text} configured" findings.append(report) return findings diff --git a/prowler/providers/azure/services/postgresql/postgresql_flexible_server_log_checkpoints_on/postgresql_flexible_server_log_checkpoints_on.py b/prowler/providers/azure/services/postgresql/postgresql_flexible_server_log_checkpoints_on/postgresql_flexible_server_log_checkpoints_on.py index 4ff3b90e77..f04f20cb69 100644 --- a/prowler/providers/azure/services/postgresql/postgresql_flexible_server_log_checkpoints_on/postgresql_flexible_server_log_checkpoints_on.py +++ b/prowler/providers/azure/services/postgresql/postgresql_flexible_server_log_checkpoints_on/postgresql_flexible_server_log_checkpoints_on.py @@ -11,14 +11,17 @@ class postgresql_flexible_server_log_checkpoints_on(Check): subscription, flexible_servers, ) in postgresql_client.flexible_servers.items(): + subscription_name = postgresql_client.subscriptions.get( + subscription, subscription + ) for server in flexible_servers: report = Check_Report_Azure(metadata=self.metadata(), resource=server) report.subscription = subscription report.status = "FAIL" - report.status_extended = f"Flexible Postgresql server {server.name} from subscription {subscription} has log_checkpoints disabled" + report.status_extended = f"Flexible Postgresql server {server.name} from subscription {subscription_name} ({subscription}) has log_checkpoints disabled" if server.log_checkpoints == "ON": report.status = "PASS" - report.status_extended = f"Flexible Postgresql server {server.name} from subscription {subscription} has log_checkpoints enabled" + report.status_extended = f"Flexible Postgresql server {server.name} from subscription {subscription_name} ({subscription}) has log_checkpoints enabled" findings.append(report) return findings diff --git a/prowler/providers/azure/services/postgresql/postgresql_flexible_server_log_connections_on/postgresql_flexible_server_log_connections_on.py b/prowler/providers/azure/services/postgresql/postgresql_flexible_server_log_connections_on/postgresql_flexible_server_log_connections_on.py index ee7bda8fd9..4d1f1a9749 100644 --- a/prowler/providers/azure/services/postgresql/postgresql_flexible_server_log_connections_on/postgresql_flexible_server_log_connections_on.py +++ b/prowler/providers/azure/services/postgresql/postgresql_flexible_server_log_connections_on/postgresql_flexible_server_log_connections_on.py @@ -11,14 +11,17 @@ class postgresql_flexible_server_log_connections_on(Check): subscription, flexible_servers, ) in postgresql_client.flexible_servers.items(): + subscription_name = postgresql_client.subscriptions.get( + subscription, subscription + ) for server in flexible_servers: report = Check_Report_Azure(metadata=self.metadata(), resource=server) report.subscription = subscription report.status = "FAIL" - report.status_extended = f"Flexible Postgresql server {server.name} from subscription {subscription} has log_connections disabled" + report.status_extended = f"Flexible Postgresql server {server.name} from subscription {subscription_name} ({subscription}) has log_connections disabled" if server.log_connections == "ON": report.status = "PASS" - report.status_extended = f"Flexible Postgresql server {server.name} from subscription {subscription} has log_connections enabled" + report.status_extended = f"Flexible Postgresql server {server.name} from subscription {subscription_name} ({subscription}) has log_connections enabled" findings.append(report) return findings diff --git a/prowler/providers/azure/services/postgresql/postgresql_flexible_server_log_disconnections_on/postgresql_flexible_server_log_disconnections_on.py b/prowler/providers/azure/services/postgresql/postgresql_flexible_server_log_disconnections_on/postgresql_flexible_server_log_disconnections_on.py index af18535948..aecb6f94a4 100644 --- a/prowler/providers/azure/services/postgresql/postgresql_flexible_server_log_disconnections_on/postgresql_flexible_server_log_disconnections_on.py +++ b/prowler/providers/azure/services/postgresql/postgresql_flexible_server_log_disconnections_on/postgresql_flexible_server_log_disconnections_on.py @@ -11,14 +11,17 @@ class postgresql_flexible_server_log_disconnections_on(Check): subscription, flexible_servers, ) in postgresql_client.flexible_servers.items(): + subscription_name = postgresql_client.subscriptions.get( + subscription, subscription + ) for server in flexible_servers: report = Check_Report_Azure(metadata=self.metadata(), resource=server) report.subscription = subscription report.status = "FAIL" - report.status_extended = f"Flexible Postgresql server {server.name} from subscription {subscription} has log_disconnections disabled" + report.status_extended = f"Flexible Postgresql server {server.name} from subscription {subscription_name} ({subscription}) has log_disconnections disabled" if server.log_disconnections == "ON": report.status = "PASS" - report.status_extended = f"Flexible Postgresql server {server.name} from subscription {subscription} has log_disconnections enabled" + report.status_extended = f"Flexible Postgresql server {server.name} from subscription {subscription_name} ({subscription}) has log_disconnections enabled" findings.append(report) return findings diff --git a/prowler/providers/azure/services/postgresql/postgresql_flexible_server_log_retention_days_greater_3/postgresql_flexible_server_log_retention_days_greater_3.py b/prowler/providers/azure/services/postgresql/postgresql_flexible_server_log_retention_days_greater_3/postgresql_flexible_server_log_retention_days_greater_3.py index f1cb0939c8..45a5a64e32 100644 --- a/prowler/providers/azure/services/postgresql/postgresql_flexible_server_log_retention_days_greater_3/postgresql_flexible_server_log_retention_days_greater_3.py +++ b/prowler/providers/azure/services/postgresql/postgresql_flexible_server_log_retention_days_greater_3/postgresql_flexible_server_log_retention_days_greater_3.py @@ -11,13 +11,16 @@ class postgresql_flexible_server_log_retention_days_greater_3(Check): subscription, flexible_servers, ) in postgresql_client.flexible_servers.items(): + subscription_name = postgresql_client.subscriptions.get( + subscription, subscription + ) for server in flexible_servers: report = Check_Report_Azure(metadata=self.metadata(), resource=server) report.subscription = subscription report.status = "FAIL" - report.status_extended = f"Flexible Postgresql server {server.name} from subscription {subscription} has log_retention disabled" + report.status_extended = f"Flexible Postgresql server {server.name} from subscription {subscription_name} ({subscription}) has log_retention disabled" if server.log_retention_days: - report.status_extended = f"Flexible Postgresql server {server.name} from subscription {subscription} has log_retention set to {server.log_retention_days}" + report.status_extended = f"Flexible Postgresql server {server.name} from subscription {subscription_name} ({subscription}) has log_retention set to {server.log_retention_days}" if ( int(server.log_retention_days) > 3 and int(server.log_retention_days) < 8 diff --git a/prowler/providers/azure/services/postgresql/postgresql_service.py b/prowler/providers/azure/services/postgresql/postgresql_service.py index ef9449081c..13081ad270 100644 --- a/prowler/providers/azure/services/postgresql/postgresql_service.py +++ b/prowler/providers/azure/services/postgresql/postgresql_service.py @@ -72,7 +72,7 @@ class PostgreSQL(AzureService): ) except Exception as error: logger.error( - f"Subscription name: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return flexible_servers diff --git a/prowler/providers/azure/services/recovery/recovery_service.py b/prowler/providers/azure/services/recovery/recovery_service.py index efc7630bf7..16b5bc3248 100644 --- a/prowler/providers/azure/services/recovery/recovery_service.py +++ b/prowler/providers/azure/services/recovery/recovery_service.py @@ -54,19 +54,19 @@ class Recovery(AzureService): vaults_dict: dict[str, dict[str, BackupVault]] = {} try: vaults_dict: dict[str, dict[str, BackupVault]] = {} - for subscription_name, client in self.clients.items(): + for subscription_id, client in self.clients.items(): vaults = client.vaults.list_by_subscription_id() - vaults_dict[subscription_name] = {} + vaults_dict[subscription_id] = {} for vault in vaults: vault_obj = BackupVault( id=vault.id, name=vault.name, location=vault.location, ) - vaults_dict[subscription_name][vault_obj.id] = vault_obj + vaults_dict[subscription_id][vault_obj.id] = vault_obj except Exception as error: logger.error( - f"Subscription name: {subscription_name} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription_id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return vaults_dict @@ -76,17 +76,17 @@ class RecoveryBackup(AzureService): self, provider: AzureProvider, vaults: dict[str, dict[str, BackupVault]] ): super().__init__(RecoveryServicesBackupClient, provider) - for subscription_name, vaults in vaults.items(): + for subscription_id, vaults in vaults.items(): for vault in vaults.values(): vault.backup_protected_items = self._get_backup_protected_items( - subscription_name=subscription_name, vault=vault + subscription_id=subscription_id, vault=vault ) vault.backup_policies = self._get_backup_policies( - subscription_name=subscription_name, vault=vault + subscription_id=subscription_id, vault=vault ) def _get_backup_protected_items( - self, subscription_name: str, vault: BackupVault + self, subscription_id: str, vault: BackupVault ) -> dict[str, BackupItem]: """ Retrieve all backup protected items for a given vault. @@ -95,7 +95,7 @@ class RecoveryBackup(AzureService): backup_protected_items_dict: dict[str, BackupItem] = {} try: backup_protected_items = self.clients[ - subscription_name + subscription_id ].backup_protected_items.list( vault_name=vault.name, resource_group_name=vault.id.split("/")[4], @@ -114,12 +114,12 @@ class RecoveryBackup(AzureService): ) except Exception as error: logger.error( - f"Subscription name: {subscription_name} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription_id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return backup_protected_items_dict def _get_backup_policies( - self, subscription_name: str, vault: BackupVault + self, subscription_id: str, vault: BackupVault ) -> dict[str, BackupPolicy]: """ Retrieve all backup policies for a given vault. @@ -132,7 +132,7 @@ class RecoveryBackup(AzureService): if item.backup_policy_id: unique_backup_policies.add(item.backup_policy_id) for policy_id in unique_backup_policies: - policy = self.clients[subscription_name].protection_policies.get( + policy = self.clients[subscription_id].protection_policies.get( vault_name=vault.name, resource_group_name=vault.id.split("/")[4], policy_name=policy_id.split("/")[-1], @@ -160,6 +160,6 @@ class RecoveryBackup(AzureService): ) except Exception as error: logger.error( - f"Subscription name: {subscription_name} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription_id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return backup_policies_dict diff --git a/prowler/providers/azure/services/sqlserver/sqlserver_auditing_enabled/sqlserver_auditing_enabled.py b/prowler/providers/azure/services/sqlserver/sqlserver_auditing_enabled/sqlserver_auditing_enabled.py index f9c7237302..bef7e2f70f 100644 --- a/prowler/providers/azure/services/sqlserver/sqlserver_auditing_enabled/sqlserver_auditing_enabled.py +++ b/prowler/providers/azure/services/sqlserver/sqlserver_auditing_enabled/sqlserver_auditing_enabled.py @@ -6,17 +6,20 @@ class sqlserver_auditing_enabled(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, sql_servers in sqlserver_client.sql_servers.items(): + subscription_name = sqlserver_client.subscriptions.get( + subscription, subscription + ) for sql_server in sql_servers: report = Check_Report_Azure( metadata=self.metadata(), resource=sql_server ) report.subscription = subscription report.status = "PASS" - report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription} has an auditing policy configured." + report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription_name} ({subscription}) has an auditing policy configured." for auditing_policy in sql_server.auditing_policies: if auditing_policy.state == "Disabled": report.status = "FAIL" - report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription} does not have any auditing policy configured." + report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription_name} ({subscription}) does not have any auditing policy configured." break findings.append(report) diff --git a/prowler/providers/azure/services/sqlserver/sqlserver_auditing_retention_90_days/sqlserver_auditing_retention_90_days.py b/prowler/providers/azure/services/sqlserver/sqlserver_auditing_retention_90_days/sqlserver_auditing_retention_90_days.py index 5b1120b00e..93f2914564 100644 --- a/prowler/providers/azure/services/sqlserver/sqlserver_auditing_retention_90_days/sqlserver_auditing_retention_90_days.py +++ b/prowler/providers/azure/services/sqlserver/sqlserver_auditing_retention_90_days/sqlserver_auditing_retention_90_days.py @@ -6,6 +6,9 @@ class sqlserver_auditing_retention_90_days(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, sql_servers in sqlserver_client.sql_servers.items(): + subscription_name = sqlserver_client.subscriptions.get( + subscription, subscription + ) for sql_server in sql_servers: report = Check_Report_Azure( metadata=self.metadata(), resource=sql_server @@ -20,14 +23,14 @@ class sqlserver_auditing_retention_90_days(Check): if policy.state == "Enabled": if policy.retention_days <= 90: report.status = "FAIL" - report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription} has auditing retention less than 91 days." + report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription_name} ({subscription}) has auditing retention less than 91 days." has_failed = True else: report.status = "PASS" - report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription} has auditing retention greater than 90 days." + report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription_name} ({subscription}) has auditing retention greater than 90 days." else: report.status = "FAIL" - report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription} has auditing disabled." + report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription_name} ({subscription}) has auditing disabled." has_failed = True if has_policy: findings.append(report) diff --git a/prowler/providers/azure/services/sqlserver/sqlserver_azuread_administrator_enabled/sqlserver_azuread_administrator_enabled.py b/prowler/providers/azure/services/sqlserver/sqlserver_azuread_administrator_enabled/sqlserver_azuread_administrator_enabled.py index 6d5b1c265d..234ccf1a3f 100644 --- a/prowler/providers/azure/services/sqlserver/sqlserver_azuread_administrator_enabled/sqlserver_azuread_administrator_enabled.py +++ b/prowler/providers/azure/services/sqlserver/sqlserver_azuread_administrator_enabled/sqlserver_azuread_administrator_enabled.py @@ -6,20 +6,23 @@ class sqlserver_azuread_administrator_enabled(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, sql_servers in sqlserver_client.sql_servers.items(): + subscription_name = sqlserver_client.subscriptions.get( + subscription, subscription + ) for sql_server in sql_servers: report = Check_Report_Azure( metadata=self.metadata(), resource=sql_server ) report.subscription = subscription report.status = "PASS" - report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription} has an Active Directory administrator." + report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription_name} ({subscription}) has an Active Directory administrator." if ( sql_server.administrators is None or sql_server.administrators.administrator_type != "ActiveDirectory" ): report.status = "FAIL" - report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription} does not have an Active Directory administrator." + report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription_name} ({subscription}) does not have an Active Directory administrator." findings.append(report) diff --git a/prowler/providers/azure/services/sqlserver/sqlserver_microsoft_defender_enabled/sqlserver_microsoft_defender_enabled.py b/prowler/providers/azure/services/sqlserver/sqlserver_microsoft_defender_enabled/sqlserver_microsoft_defender_enabled.py index de2934bcf9..0b86e67e2e 100644 --- a/prowler/providers/azure/services/sqlserver/sqlserver_microsoft_defender_enabled/sqlserver_microsoft_defender_enabled.py +++ b/prowler/providers/azure/services/sqlserver/sqlserver_microsoft_defender_enabled/sqlserver_microsoft_defender_enabled.py @@ -6,6 +6,9 @@ class sqlserver_microsoft_defender_enabled(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, sql_servers in sqlserver_client.sql_servers.items(): + subscription_name = sqlserver_client.subscriptions.get( + subscription, subscription + ) for sql_server in sql_servers: if sql_server.security_alert_policies: report = Check_Report_Azure( @@ -13,10 +16,10 @@ class sqlserver_microsoft_defender_enabled(Check): ) report.subscription = subscription report.status = "FAIL" - report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription} has microsoft defender disabled." + report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription_name} ({subscription}) has microsoft defender disabled." if sql_server.security_alert_policies.state == "Enabled": report.status = "PASS" - report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription} has microsoft defender enabled." + report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription_name} ({subscription}) has microsoft defender enabled." findings.append(report) return findings diff --git a/prowler/providers/azure/services/sqlserver/sqlserver_recommended_minimal_tls_version/sqlserver_recommended_minimal_tls_version.py b/prowler/providers/azure/services/sqlserver/sqlserver_recommended_minimal_tls_version/sqlserver_recommended_minimal_tls_version.py index 2f55951436..fb88662776 100644 --- a/prowler/providers/azure/services/sqlserver/sqlserver_recommended_minimal_tls_version/sqlserver_recommended_minimal_tls_version.py +++ b/prowler/providers/azure/services/sqlserver/sqlserver_recommended_minimal_tls_version/sqlserver_recommended_minimal_tls_version.py @@ -11,15 +11,18 @@ class sqlserver_recommended_minimal_tls_version(Check): "recommended_minimal_tls_versions", ["1.2", "1.3"] ) for subscription, sql_servers in sqlserver_client.sql_servers.items(): + subscription_name = sqlserver_client.subscriptions.get( + subscription, subscription + ) for sql_server in sql_servers: report = Check_Report_Azure( metadata=self.metadata(), resource=sql_server ) report.subscription = subscription report.status = "FAIL" - report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription} is using TLS version {sql_server.minimal_tls_version} as minimal accepted which is not recommended. Please use one of the recommended versions: {', '.join(recommended_minimal_tls_versions)}." + report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription_name} ({subscription}) is using TLS version {sql_server.minimal_tls_version} as minimal accepted which is not recommended. Please use one of the recommended versions: {', '.join(recommended_minimal_tls_versions)}." if sql_server.minimal_tls_version in recommended_minimal_tls_versions: - report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription} is using version {sql_server.minimal_tls_version} as minimal accepted which is recommended." + report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription_name} ({subscription}) is using version {sql_server.minimal_tls_version} as minimal accepted which is recommended." report.status = "PASS" findings.append(report) diff --git a/prowler/providers/azure/services/sqlserver/sqlserver_service.py b/prowler/providers/azure/services/sqlserver/sqlserver_service.py index 4d4ca00ebc..af02dace0d 100644 --- a/prowler/providers/azure/services/sqlserver/sqlserver_service.py +++ b/prowler/providers/azure/services/sqlserver/sqlserver_service.py @@ -72,7 +72,7 @@ class SQLServer(AzureService): ) except Exception as error: logger.error( - f"Subscription name: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return sql_servers @@ -141,7 +141,7 @@ class SQLServer(AzureService): ) except Exception as error: logger.error( - f"Subscription name: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return databases diff --git a/prowler/providers/azure/services/sqlserver/sqlserver_tde_encrypted_with_cmk/sqlserver_tde_encrypted_with_cmk.py b/prowler/providers/azure/services/sqlserver/sqlserver_tde_encrypted_with_cmk/sqlserver_tde_encrypted_with_cmk.py index 2b4cd94d1b..5f44e2663c 100644 --- a/prowler/providers/azure/services/sqlserver/sqlserver_tde_encrypted_with_cmk/sqlserver_tde_encrypted_with_cmk.py +++ b/prowler/providers/azure/services/sqlserver/sqlserver_tde_encrypted_with_cmk/sqlserver_tde_encrypted_with_cmk.py @@ -6,10 +6,17 @@ class sqlserver_tde_encrypted_with_cmk(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, sql_servers in sqlserver_client.sql_servers.items(): + subscription_name = sqlserver_client.subscriptions.get( + subscription, subscription + ) for sql_server in sql_servers: - databases = ( - sql_server.databases if sql_server.databases is not None else [] - ) + databases = [ + database + for database in ( + sql_server.databases if sql_server.databases is not None else [] + ) + if database.name.lower() != "master" + ] if len(databases) > 0: report = Check_Report_Azure( metadata=self.metadata(), resource=sql_server @@ -25,14 +32,14 @@ class sqlserver_tde_encrypted_with_cmk(Check): break if database.tde_encryption.status == "Enabled": report.status = "PASS" - report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription} has TDE enabled with CMK." + report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription_name} ({subscription}) has TDE enabled with CMK." else: report.status = "FAIL" - report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription} has TDE disabled with CMK." + report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription_name} ({subscription}) has TDE disabled with CMK." found_disabled = True else: report.status = "FAIL" - report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription} has TDE disabled without CMK." + report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription_name} ({subscription}) has TDE disabled without CMK." findings.append(report) return findings diff --git a/prowler/providers/azure/services/sqlserver/sqlserver_tde_encryption_enabled/sqlserver_tde_encryption_enabled.py b/prowler/providers/azure/services/sqlserver/sqlserver_tde_encryption_enabled/sqlserver_tde_encryption_enabled.py index 05de0efc7a..b7bda558a2 100644 --- a/prowler/providers/azure/services/sqlserver/sqlserver_tde_encryption_enabled/sqlserver_tde_encryption_enabled.py +++ b/prowler/providers/azure/services/sqlserver/sqlserver_tde_encryption_enabled/sqlserver_tde_encryption_enabled.py @@ -6,6 +6,9 @@ class sqlserver_tde_encryption_enabled(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, sql_servers in sqlserver_client.sql_servers.items(): + subscription_name = sqlserver_client.subscriptions.get( + subscription, subscription + ) for sql_server in sql_servers: databases = ( sql_server.databases if sql_server.databases is not None else [] @@ -20,10 +23,10 @@ class sqlserver_tde_encryption_enabled(Check): report.subscription = subscription if database.tde_encryption.status == "Enabled": report.status = "PASS" - report.status_extended = f"Database {database.name} from SQL Server {sql_server.name} from subscription {subscription} has TDE enabled" + report.status_extended = f"Database {database.name} from SQL Server {sql_server.name} from subscription {subscription_name} ({subscription}) has TDE enabled" else: report.status = "FAIL" - report.status_extended = f"Database {database.name} from SQL Server {sql_server.name} from subscription {subscription} has TDE disabled" + report.status_extended = f"Database {database.name} from SQL Server {sql_server.name} from subscription {subscription_name} ({subscription}) has TDE disabled" findings.append(report) return findings diff --git a/prowler/providers/azure/services/sqlserver/sqlserver_unrestricted_inbound_access/sqlserver_unrestricted_inbound_access.py b/prowler/providers/azure/services/sqlserver/sqlserver_unrestricted_inbound_access/sqlserver_unrestricted_inbound_access.py index 9936a9a077..d9e84eaf96 100644 --- a/prowler/providers/azure/services/sqlserver/sqlserver_unrestricted_inbound_access/sqlserver_unrestricted_inbound_access.py +++ b/prowler/providers/azure/services/sqlserver/sqlserver_unrestricted_inbound_access/sqlserver_unrestricted_inbound_access.py @@ -6,20 +6,23 @@ class sqlserver_unrestricted_inbound_access(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, sql_servers in sqlserver_client.sql_servers.items(): + subscription_name = sqlserver_client.subscriptions.get( + subscription, subscription + ) for sql_server in sql_servers: report = Check_Report_Azure( metadata=self.metadata(), resource=sql_server ) report.subscription = subscription report.status = "PASS" - report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription} does not have firewall rules allowing 0.0.0.0-255.255.255.255." + report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription_name} ({subscription}) does not have firewall rules allowing 0.0.0.0-255.255.255.255." for firewall_rule in sql_server.firewall_rules: if ( firewall_rule.start_ip_address == "0.0.0.0" and firewall_rule.end_ip_address == "255.255.255.255" ): report.status = "FAIL" - report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription} has firewall rules allowing 0.0.0.0-255.255.255.255." + report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription_name} ({subscription}) has firewall rules allowing 0.0.0.0-255.255.255.255." break findings.append(report) diff --git a/prowler/providers/azure/services/sqlserver/sqlserver_va_emails_notifications_admins_enabled/sqlserver_va_emails_notifications_admins_enabled.py b/prowler/providers/azure/services/sqlserver/sqlserver_va_emails_notifications_admins_enabled/sqlserver_va_emails_notifications_admins_enabled.py index 62a6a1d458..d853a9ea5e 100644 --- a/prowler/providers/azure/services/sqlserver/sqlserver_va_emails_notifications_admins_enabled/sqlserver_va_emails_notifications_admins_enabled.py +++ b/prowler/providers/azure/services/sqlserver/sqlserver_va_emails_notifications_admins_enabled/sqlserver_va_emails_notifications_admins_enabled.py @@ -6,24 +6,27 @@ class sqlserver_va_emails_notifications_admins_enabled(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, sql_servers in sqlserver_client.sql_servers.items(): + subscription_name = sqlserver_client.subscriptions.get( + subscription, subscription + ) for sql_server in sql_servers: report = Check_Report_Azure( metadata=self.metadata(), resource=sql_server ) report.subscription = subscription report.status = "FAIL" - report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription} has vulnerability assessment disabled." + report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription_name} ({subscription}) has vulnerability assessment disabled." if ( sql_server.vulnerability_assessment and sql_server.vulnerability_assessment.storage_container_path ): - report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription} has vulnerability assessment enabled but no scan reports configured for subscription admins." + report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription_name} ({subscription}) has vulnerability assessment enabled but no scan reports configured for subscription admins." if ( sql_server.vulnerability_assessment.recurring_scans and sql_server.vulnerability_assessment.recurring_scans.email_subscription_admins ): report.status = "PASS" - report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription} has vulnerability assessment enabled and scan reports configured for subscription admins." + report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription_name} ({subscription}) has vulnerability assessment enabled and scan reports configured for subscription admins." findings.append(report) return findings diff --git a/prowler/providers/azure/services/sqlserver/sqlserver_va_periodic_recurring_scans_enabled/sqlserver_va_periodic_recurring_scans_enabled.py b/prowler/providers/azure/services/sqlserver/sqlserver_va_periodic_recurring_scans_enabled/sqlserver_va_periodic_recurring_scans_enabled.py index 2aaf40a99a..45798248f4 100644 --- a/prowler/providers/azure/services/sqlserver/sqlserver_va_periodic_recurring_scans_enabled/sqlserver_va_periodic_recurring_scans_enabled.py +++ b/prowler/providers/azure/services/sqlserver/sqlserver_va_periodic_recurring_scans_enabled/sqlserver_va_periodic_recurring_scans_enabled.py @@ -6,24 +6,27 @@ class sqlserver_va_periodic_recurring_scans_enabled(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, sql_servers in sqlserver_client.sql_servers.items(): + subscription_name = sqlserver_client.subscriptions.get( + subscription, subscription + ) for sql_server in sql_servers: report = Check_Report_Azure( metadata=self.metadata(), resource=sql_server ) report.subscription = subscription report.status = "FAIL" - report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription} has vulnerability assessment disabled." + report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription_name} ({subscription}) has vulnerability assessment disabled." if ( sql_server.vulnerability_assessment and sql_server.vulnerability_assessment.storage_container_path ): - report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription} has vulnerability assessment enabled but no recurring scans." + report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription_name} ({subscription}) has vulnerability assessment enabled but no recurring scans." if ( sql_server.vulnerability_assessment.recurring_scans and sql_server.vulnerability_assessment.recurring_scans.is_enabled ): report.status = "PASS" - report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription} has periodic recurring scans enabled." + report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription_name} ({subscription}) has periodic recurring scans enabled." findings.append(report) return findings diff --git a/prowler/providers/azure/services/sqlserver/sqlserver_va_scan_reports_configured/sqlserver_va_scan_reports_configured.py b/prowler/providers/azure/services/sqlserver/sqlserver_va_scan_reports_configured/sqlserver_va_scan_reports_configured.py index 727696225c..a0c8b55e8f 100644 --- a/prowler/providers/azure/services/sqlserver/sqlserver_va_scan_reports_configured/sqlserver_va_scan_reports_configured.py +++ b/prowler/providers/azure/services/sqlserver/sqlserver_va_scan_reports_configured/sqlserver_va_scan_reports_configured.py @@ -6,18 +6,21 @@ class sqlserver_va_scan_reports_configured(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, sql_servers in sqlserver_client.sql_servers.items(): + subscription_name = sqlserver_client.subscriptions.get( + subscription, subscription + ) for sql_server in sql_servers: report = Check_Report_Azure( metadata=self.metadata(), resource=sql_server ) report.subscription = subscription report.status = "FAIL" - report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription} has vulnerability assessment disabled." + report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription_name} ({subscription}) has vulnerability assessment disabled." if ( sql_server.vulnerability_assessment and sql_server.vulnerability_assessment.storage_container_path ): - report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription} has vulnerability assessment enabled but no scan reports configured." + report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription_name} ({subscription}) has vulnerability assessment enabled but no scan reports configured." if sql_server.vulnerability_assessment.recurring_scans and ( ( sql_server.vulnerability_assessment.recurring_scans.email_subscription_admins @@ -31,7 +34,7 @@ class sqlserver_va_scan_reports_configured(Check): ) ): report.status = "PASS" - report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription} has vulnerability assessment enabled and scan reports configured." + report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription_name} ({subscription}) has vulnerability assessment enabled and scan reports configured." findings.append(report) return findings diff --git a/prowler/providers/azure/services/sqlserver/sqlserver_vulnerability_assessment_enabled/sqlserver_vulnerability_assessment_enabled.py b/prowler/providers/azure/services/sqlserver/sqlserver_vulnerability_assessment_enabled/sqlserver_vulnerability_assessment_enabled.py index caf0ee0081..62a97abd07 100644 --- a/prowler/providers/azure/services/sqlserver/sqlserver_vulnerability_assessment_enabled/sqlserver_vulnerability_assessment_enabled.py +++ b/prowler/providers/azure/services/sqlserver/sqlserver_vulnerability_assessment_enabled/sqlserver_vulnerability_assessment_enabled.py @@ -6,20 +6,23 @@ class sqlserver_vulnerability_assessment_enabled(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, sql_servers in sqlserver_client.sql_servers.items(): + subscription_name = sqlserver_client.subscriptions.get( + subscription, subscription + ) for sql_server in sql_servers: report = Check_Report_Azure( metadata=self.metadata(), resource=sql_server ) report.subscription = subscription report.status = "FAIL" - report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription} has vulnerability assessment disabled." + report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription_name} ({subscription}) has vulnerability assessment disabled." if ( sql_server.vulnerability_assessment and sql_server.vulnerability_assessment.storage_container_path is not None ): report.status = "PASS" - report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription} has vulnerability assessment enabled." + report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription_name} ({subscription}) has vulnerability assessment enabled." findings.append(report) return findings diff --git a/prowler/providers/azure/services/storage/storage_account_key_access_disabled/storage_account_key_access_disabled.py b/prowler/providers/azure/services/storage/storage_account_key_access_disabled/storage_account_key_access_disabled.py index c4b946c7cf..75ad2d0544 100644 --- a/prowler/providers/azure/services/storage/storage_account_key_access_disabled/storage_account_key_access_disabled.py +++ b/prowler/providers/azure/services/storage/storage_account_key_access_disabled/storage_account_key_access_disabled.py @@ -19,6 +19,9 @@ class storage_account_key_access_disabled(Check): """ findings = [] for subscription, storage_accounts in storage_client.storage_accounts.items(): + subscription_name = storage_client.subscriptions.get( + subscription, subscription + ) for storage_account in storage_accounts: report = Check_Report_Azure( metadata=self.metadata(), resource=storage_account @@ -26,9 +29,9 @@ class storage_account_key_access_disabled(Check): report.subscription = subscription if not storage_account.allow_shared_key_access: report.status = "PASS" - report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} has shared key access disabled." + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription_name} ({subscription}) has shared key access disabled." else: report.status = "FAIL" - report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} has shared key access enabled." + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription_name} ({subscription}) has shared key access enabled." findings.append(report) return findings diff --git a/tests/providers/googleworkspace/services/gmail/gmail_shortener_scanning_enabled/__init__.py b/prowler/providers/azure/services/storage/storage_account_public_network_access_disabled/__init__.py similarity index 100% rename from tests/providers/googleworkspace/services/gmail/gmail_shortener_scanning_enabled/__init__.py rename to prowler/providers/azure/services/storage/storage_account_public_network_access_disabled/__init__.py diff --git a/prowler/providers/azure/services/storage/storage_account_public_network_access_disabled/storage_account_public_network_access_disabled.metadata.json b/prowler/providers/azure/services/storage/storage_account_public_network_access_disabled/storage_account_public_network_access_disabled.metadata.json new file mode 100644 index 0000000000..142b1082cf --- /dev/null +++ b/prowler/providers/azure/services/storage/storage_account_public_network_access_disabled/storage_account_public_network_access_disabled.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "azure", + "CheckID": "storage_account_public_network_access_disabled", + "CheckTitle": "Storage account has 'Public Network Access' disabled", + "CheckType": [], + "ServiceName": "storage", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "microsoft.storage/storageaccounts", + "ResourceGroup": "storage", + "Description": "**Azure Storage accounts** with **public network access** disabled cannot be reached from public networks. Setting `publicNetworkAccess` to `Disabled` overrides the public access settings of individual containers and forces access through private endpoints or trusted services. This is independent from the 'Allow Blob Anonymous Access' setting.", + "Risk": "Leaving **public network access** enabled exposes the storage account endpoints to the **public Internet**, widening the attack surface and undermining **defense in depth**.\n\nThis increases the risk of **unauthorized access**, **data exfiltration**, and reconnaissance against the account, especially when combined with weak network rules or overly permissive access policies.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/azure/storage/common/storage-network-security?tabs=azure-portal#change-the-default-network-access-rule", + "https://learn.microsoft.com/en-us/azure/storage/common/storage-network-security-set-default-access" + ], + "Remediation": { + "Code": { + "CLI": "az storage account update --name --resource-group --public-network-access Disabled", + "NativeIaC": "```bicep\n// Storage account with public network access disabled\nresource sa 'Microsoft.Storage/storageAccounts@2023-01-01' = {\n name: ''\n location: resourceGroup().location\n kind: 'StorageV2'\n sku: { name: 'Standard_LRS' }\n properties: {\n publicNetworkAccess: 'Disabled' // Critical: disables public network access to the account\n }\n}\n```", + "Other": "1. In the Azure portal, go to Storage accounts and select the target account\n2. Under Security + networking, click Networking\n3. Set Public network access to Disabled\n4. Click Save", + "Terraform": "```hcl\nresource \"azurerm_storage_account\" \"\" {\n name = \"\"\n resource_group_name = \"\"\n location = \"\"\n account_tier = \"Standard\"\n account_replication_type = \"LRS\"\n public_network_access_enabled = false # Critical: disables public network access\n}\n```" + }, + "Recommendation": { + "Text": "Disable **public network access** on the storage account and reach it through **private endpoints** or trusted Azure services only. Combine this with **least privilege** RBAC, short-lived `SAS` tokens, and network restrictions. Validate client connectivity before disabling public access in production.", + "Url": "https://hub.prowler.com/check/storage_account_public_network_access_disabled" + } + }, + "Categories": [ + "internet-exposed" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "This check evaluates the storage account publicNetworkAccess property. It is independent from the 'Allow Blob Anonymous Access' setting evaluated by storage_blob_public_access_level_is_disabled." +} diff --git a/prowler/providers/azure/services/storage/storage_account_public_network_access_disabled/storage_account_public_network_access_disabled.py b/prowler/providers/azure/services/storage/storage_account_public_network_access_disabled/storage_account_public_network_access_disabled.py new file mode 100644 index 0000000000..3db62c5256 --- /dev/null +++ b/prowler/providers/azure/services/storage/storage_account_public_network_access_disabled/storage_account_public_network_access_disabled.py @@ -0,0 +1,38 @@ +from prowler.lib.check.models import Check, Check_Report_Azure +from prowler.providers.azure.services.storage.storage_client import storage_client + + +class storage_account_public_network_access_disabled(Check): + """ + Ensure that 'Public Network Access' is 'Disabled' for storage accounts. + + This check evaluates the storage account's publicNetworkAccess property, which controls + whether the account is reachable from public networks. It is independent from the + 'Allow Blob Anonymous Access' setting (covered by + storage_blob_public_access_level_is_disabled). + - PASS: The storage account has public network access disabled. + - FAIL: The storage account has public network access enabled (or unset, which Azure treats as enabled). + """ + + def execute(self) -> list[Check_Report_Azure]: + findings = [] + for subscription, storage_accounts in storage_client.storage_accounts.items(): + subscription_name = storage_client.subscriptions.get( + subscription, subscription + ) + for storage_account in storage_accounts: + report = Check_Report_Azure( + metadata=self.metadata(), resource=storage_account + ) + report.subscription = subscription + + if storage_account.public_network_access == "Disabled": + report.status = "PASS" + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription_name} ({subscription}) has public network access disabled." + else: + report.status = "FAIL" + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription_name} ({subscription}) has public network access enabled." + + findings.append(report) + + return findings diff --git a/prowler/providers/azure/services/storage/storage_blob_public_access_level_is_disabled/storage_blob_public_access_level_is_disabled.metadata.json b/prowler/providers/azure/services/storage/storage_blob_public_access_level_is_disabled/storage_blob_public_access_level_is_disabled.metadata.json index 079376899b..1cf3e35569 100644 --- a/prowler/providers/azure/services/storage/storage_blob_public_access_level_is_disabled/storage_blob_public_access_level_is_disabled.metadata.json +++ b/prowler/providers/azure/services/storage/storage_blob_public_access_level_is_disabled/storage_blob_public_access_level_is_disabled.metadata.json @@ -1,7 +1,7 @@ { "Provider": "azure", "CheckID": "storage_blob_public_access_level_is_disabled", - "CheckTitle": "Storage account has 'Allow blob public access' disabled", + "CheckTitle": "Storage account has 'Allow Blob Anonymous Access' disabled", "CheckType": [], "ServiceName": "storage", "SubServiceName": "", @@ -9,7 +9,7 @@ "Severity": "high", "ResourceType": "microsoft.storage/storageaccounts", "ResourceGroup": "storage", - "Description": "**Azure Storage accounts** with **blob public access** disabled prevent containers or blobs from being set to a public access level. Setting `allow blob public access` to `false` enforces no anonymous reads across the account.", + "Description": "**Azure Storage accounts** with **blob anonymous (public) access** disabled prevent containers or blobs from being set to a public access level. Setting `allowBlobPublicAccess` to `false` enforces no anonymous reads across the account. This is independent from the account's 'Public Network Access' setting, which is evaluated by storage_account_public_network_access_disabled.", "Risk": "Allowing public access permits unauthenticated users to read blob data or enumerate container contents when any container is made public, compromising confidentiality.\n\nExposed objects can be scraped at scale, enabling data exfiltration and intelligence gathering without audit attribution.", "RelatedUrl": "", "AdditionalURLs": [ @@ -33,5 +33,5 @@ ], "DependsOn": [], "RelatedTo": [], - "Notes": "" + "Notes": "This check evaluates the 'Allow Blob Anonymous Access' (allowBlobPublicAccess) setting. The account's 'Public Network Access' (publicNetworkAccess) setting is evaluated by storage_account_public_network_access_disabled." } diff --git a/prowler/providers/azure/services/storage/storage_blob_public_access_level_is_disabled/storage_blob_public_access_level_is_disabled.py b/prowler/providers/azure/services/storage/storage_blob_public_access_level_is_disabled/storage_blob_public_access_level_is_disabled.py index 38759f0569..f1edb63b90 100644 --- a/prowler/providers/azure/services/storage/storage_blob_public_access_level_is_disabled/storage_blob_public_access_level_is_disabled.py +++ b/prowler/providers/azure/services/storage/storage_blob_public_access_level_is_disabled/storage_blob_public_access_level_is_disabled.py @@ -6,17 +6,20 @@ class storage_blob_public_access_level_is_disabled(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, storage_accounts in storage_client.storage_accounts.items(): + subscription_name = storage_client.subscriptions.get( + subscription, subscription + ) for storage_account in storage_accounts: report = Check_Report_Azure( metadata=self.metadata(), resource=storage_account ) report.subscription = subscription report.status = "FAIL" - report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} has allow blob public access enabled." + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription_name} ({subscription}) has allow blob public access enabled." if not storage_account.allow_blob_public_access: report.status = "PASS" - report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} has allow blob public access disabled." + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription_name} ({subscription}) has allow blob public access disabled." findings.append(report) diff --git a/prowler/providers/azure/services/storage/storage_blob_versioning_is_enabled/storage_blob_versioning_is_enabled.py b/prowler/providers/azure/services/storage/storage_blob_versioning_is_enabled/storage_blob_versioning_is_enabled.py index cf55d6f830..73a40dcfd4 100644 --- a/prowler/providers/azure/services/storage/storage_blob_versioning_is_enabled/storage_blob_versioning_is_enabled.py +++ b/prowler/providers/azure/services/storage/storage_blob_versioning_is_enabled/storage_blob_versioning_is_enabled.py @@ -6,6 +6,9 @@ class storage_blob_versioning_is_enabled(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, storage_accounts in storage_client.storage_accounts.items(): + subscription_name = storage_client.subscriptions.get( + subscription, subscription + ) for storage_account in storage_accounts: if storage_account.blob_properties: report = Check_Report_Azure( @@ -16,9 +19,9 @@ class storage_blob_versioning_is_enabled(Check): storage_account.blob_properties, "versioning_enabled", False ): report.status = "PASS" - report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} has blob versioning enabled." + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription_name} ({subscription}) has blob versioning enabled." else: report.status = "FAIL" - report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} does not have blob versioning enabled." + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription_name} ({subscription}) does not have blob versioning enabled." findings.append(report) return findings diff --git a/prowler/providers/azure/services/storage/storage_cross_tenant_replication_disabled/storage_cross_tenant_replication_disabled.py b/prowler/providers/azure/services/storage/storage_cross_tenant_replication_disabled/storage_cross_tenant_replication_disabled.py index 65ed50545e..6b23a5b050 100644 --- a/prowler/providers/azure/services/storage/storage_cross_tenant_replication_disabled/storage_cross_tenant_replication_disabled.py +++ b/prowler/providers/azure/services/storage/storage_cross_tenant_replication_disabled/storage_cross_tenant_replication_disabled.py @@ -19,6 +19,9 @@ class storage_cross_tenant_replication_disabled(Check): """ findings = [] for subscription, storage_accounts in storage_client.storage_accounts.items(): + subscription_name = storage_client.subscriptions.get( + subscription, subscription + ) for storage_account in storage_accounts: report = Check_Report_Azure( metadata=self.metadata(), resource=storage_account @@ -26,9 +29,9 @@ class storage_cross_tenant_replication_disabled(Check): report.subscription = subscription if not storage_account.allow_cross_tenant_replication: report.status = "PASS" - report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} has cross-tenant replication disabled." + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription_name} ({subscription}) has cross-tenant replication disabled." else: report.status = "FAIL" - report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} has cross-tenant replication enabled." + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription_name} ({subscription}) has cross-tenant replication enabled." findings.append(report) return findings diff --git a/prowler/providers/azure/services/storage/storage_default_network_access_rule_is_denied/storage_default_network_access_rule_is_denied.py b/prowler/providers/azure/services/storage/storage_default_network_access_rule_is_denied/storage_default_network_access_rule_is_denied.py index 4b9210eef5..1f26f8e535 100644 --- a/prowler/providers/azure/services/storage/storage_default_network_access_rule_is_denied/storage_default_network_access_rule_is_denied.py +++ b/prowler/providers/azure/services/storage/storage_default_network_access_rule_is_denied/storage_default_network_access_rule_is_denied.py @@ -6,17 +6,20 @@ class storage_default_network_access_rule_is_denied(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, storage_accounts in storage_client.storage_accounts.items(): + subscription_name = storage_client.subscriptions.get( + subscription, subscription + ) for storage_account in storage_accounts: report = Check_Report_Azure( metadata=self.metadata(), resource=storage_account ) report.subscription = subscription report.status = "PASS" - report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} has network access rule set to Deny." + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription_name} ({subscription}) has network access rule set to Deny." if storage_account.network_rule_set.default_action == "Allow": report.status = "FAIL" - report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} has network access rule set to Allow." + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription_name} ({subscription}) has network access rule set to Allow." findings.append(report) diff --git a/prowler/providers/azure/services/storage/storage_default_to_entra_authorization_enabled/storage_default_to_entra_authorization_enabled.py b/prowler/providers/azure/services/storage/storage_default_to_entra_authorization_enabled/storage_default_to_entra_authorization_enabled.py index 4b82c363e5..39d42c3133 100644 --- a/prowler/providers/azure/services/storage/storage_default_to_entra_authorization_enabled/storage_default_to_entra_authorization_enabled.py +++ b/prowler/providers/azure/services/storage/storage_default_to_entra_authorization_enabled/storage_default_to_entra_authorization_enabled.py @@ -20,6 +20,9 @@ class storage_default_to_entra_authorization_enabled(Check): """ findings = [] for subscription, storage_accounts in storage_client.storage_accounts.items(): + subscription_name = storage_client.subscriptions.get( + subscription, subscription + ) for storage_account in storage_accounts: report = Check_Report_Azure( metadata=self.metadata(), resource=storage_account @@ -28,11 +31,11 @@ class storage_default_to_entra_authorization_enabled(Check): report.resource_name = storage_account.name report.resource_id = storage_account.id report.status = "FAIL" - report.status_extended = f"Default to Microsoft Entra authorization is not enabled for storage account {storage_account.name}." + report.status_extended = f"Default to Microsoft Entra authorization is not enabled for storage account {storage_account.name} from subscription {subscription_name} ({subscription})." if storage_account.default_to_entra_authorization: report.status = "PASS" - report.status_extended = f"Default to Microsoft Entra authorization is enabled for storage account {storage_account.name}." + report.status_extended = f"Default to Microsoft Entra authorization is enabled for storage account {storage_account.name} from subscription {subscription_name} ({subscription})." findings.append(report) return findings diff --git a/prowler/providers/azure/services/storage/storage_ensure_azure_services_are_trusted_to_access_is_enabled/storage_ensure_azure_services_are_trusted_to_access_is_enabled.py b/prowler/providers/azure/services/storage/storage_ensure_azure_services_are_trusted_to_access_is_enabled/storage_ensure_azure_services_are_trusted_to_access_is_enabled.py index a8109d90f1..7d23dd5268 100644 --- a/prowler/providers/azure/services/storage/storage_ensure_azure_services_are_trusted_to_access_is_enabled/storage_ensure_azure_services_are_trusted_to_access_is_enabled.py +++ b/prowler/providers/azure/services/storage/storage_ensure_azure_services_are_trusted_to_access_is_enabled/storage_ensure_azure_services_are_trusted_to_access_is_enabled.py @@ -6,17 +6,20 @@ class storage_ensure_azure_services_are_trusted_to_access_is_enabled(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, storage_accounts in storage_client.storage_accounts.items(): + subscription_name = storage_client.subscriptions.get( + subscription, subscription + ) for storage_account in storage_accounts: report = Check_Report_Azure( metadata=self.metadata(), resource=storage_account ) report.subscription = subscription report.status = "PASS" - report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} allows trusted Microsoft services to access this storage account." + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription_name} ({subscription}) allows trusted Microsoft services to access this storage account." if "AzureServices" not in storage_account.network_rule_set.bypass: report.status = "FAIL" - report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} does not allow trusted Microsoft services to access this storage account." + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription_name} ({subscription}) does not allow trusted Microsoft services to access this storage account." findings.append(report) diff --git a/prowler/providers/azure/services/storage/storage_ensure_encryption_with_customer_managed_keys/storage_ensure_encryption_with_customer_managed_keys.py b/prowler/providers/azure/services/storage/storage_ensure_encryption_with_customer_managed_keys/storage_ensure_encryption_with_customer_managed_keys.py index f58fd33702..a704d14d0e 100644 --- a/prowler/providers/azure/services/storage/storage_ensure_encryption_with_customer_managed_keys/storage_ensure_encryption_with_customer_managed_keys.py +++ b/prowler/providers/azure/services/storage/storage_ensure_encryption_with_customer_managed_keys/storage_ensure_encryption_with_customer_managed_keys.py @@ -6,17 +6,20 @@ class storage_ensure_encryption_with_customer_managed_keys(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, storage_accounts in storage_client.storage_accounts.items(): + subscription_name = storage_client.subscriptions.get( + subscription, subscription + ) for storage_account in storage_accounts: report = Check_Report_Azure( metadata=self.metadata(), resource=storage_account ) report.subscription = subscription report.status = "PASS" - report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} encrypts with CMKs." + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription_name} ({subscription}) encrypts with CMKs." if storage_account.encryption_type != "Microsoft.Keyvault": report.status = "FAIL" - report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} does not encrypt with CMKs." + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription_name} ({subscription}) does not encrypt with CMKs." findings.append(report) diff --git a/prowler/providers/azure/services/storage/storage_ensure_file_shares_soft_delete_is_enabled/storage_ensure_file_shares_soft_delete_is_enabled.py b/prowler/providers/azure/services/storage/storage_ensure_file_shares_soft_delete_is_enabled/storage_ensure_file_shares_soft_delete_is_enabled.py index cf92ee25f3..ba5b0f065f 100644 --- a/prowler/providers/azure/services/storage/storage_ensure_file_shares_soft_delete_is_enabled/storage_ensure_file_shares_soft_delete_is_enabled.py +++ b/prowler/providers/azure/services/storage/storage_ensure_file_shares_soft_delete_is_enabled/storage_ensure_file_shares_soft_delete_is_enabled.py @@ -6,6 +6,9 @@ class storage_ensure_file_shares_soft_delete_is_enabled(Check): def execute(self) -> list: findings = [] for subscription, storage_accounts in storage_client.storage_accounts.items(): + subscription_name = storage_client.subscriptions.get( + subscription, subscription + ) for storage_account in storage_accounts: if getattr(storage_account, "file_service_properties", None): report = Check_Report_Azure( @@ -20,10 +23,10 @@ class storage_ensure_file_shares_soft_delete_is_enabled(Check): storage_account.file_service_properties.share_delete_retention_policy.enabled ): report.status = "PASS" - report.status_extended = f"File share soft delete is enabled for storage account {storage_account.name} with a retention period of {storage_account.file_service_properties.share_delete_retention_policy.days} days." + report.status_extended = f"File share soft delete is enabled for storage account {storage_account.name} from subscription {subscription_name} ({subscription}) with a retention period of {storage_account.file_service_properties.share_delete_retention_policy.days} days." else: report.status = "FAIL" - report.status_extended = f"File share soft delete is not enabled for storage account {storage_account.name}." + report.status_extended = f"File share soft delete is not enabled for storage account {storage_account.name} from subscription {subscription_name} ({subscription})." findings.append(report) diff --git a/prowler/providers/azure/services/storage/storage_ensure_minimum_tls_version_12/storage_ensure_minimum_tls_version_12.py b/prowler/providers/azure/services/storage/storage_ensure_minimum_tls_version_12/storage_ensure_minimum_tls_version_12.py index d63b3bfc9c..8e5b0c84de 100644 --- a/prowler/providers/azure/services/storage/storage_ensure_minimum_tls_version_12/storage_ensure_minimum_tls_version_12.py +++ b/prowler/providers/azure/services/storage/storage_ensure_minimum_tls_version_12/storage_ensure_minimum_tls_version_12.py @@ -6,17 +6,20 @@ class storage_ensure_minimum_tls_version_12(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, storage_accounts in storage_client.storage_accounts.items(): + subscription_name = storage_client.subscriptions.get( + subscription, subscription + ) for storage_account in storage_accounts: report = Check_Report_Azure( metadata=self.metadata(), resource=storage_account ) report.subscription = subscription report.status = "PASS" - report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} has TLS version set to 1.2." + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription_name} ({subscription}) has TLS version set to 1.2." if storage_account.minimum_tls_version != "TLS1_2": report.status = "FAIL" - report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} does not have TLS version set to 1.2." + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription_name} ({subscription}) does not have TLS version set to 1.2." findings.append(report) diff --git a/prowler/providers/azure/services/storage/storage_ensure_private_endpoints_in_storage_accounts/storage_ensure_private_endpoints_in_storage_accounts.py b/prowler/providers/azure/services/storage/storage_ensure_private_endpoints_in_storage_accounts/storage_ensure_private_endpoints_in_storage_accounts.py index 7b73759922..e344c1ac49 100644 --- a/prowler/providers/azure/services/storage/storage_ensure_private_endpoints_in_storage_accounts/storage_ensure_private_endpoints_in_storage_accounts.py +++ b/prowler/providers/azure/services/storage/storage_ensure_private_endpoints_in_storage_accounts/storage_ensure_private_endpoints_in_storage_accounts.py @@ -6,6 +6,9 @@ class storage_ensure_private_endpoints_in_storage_accounts(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, storage_accounts in storage_client.storage_accounts.items(): + subscription_name = storage_client.subscriptions.get( + subscription, subscription + ) for storage_account in storage_accounts: report = Check_Report_Azure( metadata=self.metadata(), resource=storage_account @@ -13,10 +16,10 @@ class storage_ensure_private_endpoints_in_storage_accounts(Check): report.subscription = subscription if storage_account.private_endpoint_connections: report.status = "PASS" - report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} has private endpoint connections." + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription_name} ({subscription}) has private endpoint connections." else: report.status = "FAIL" - report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} does not have private endpoint connections." + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription_name} ({subscription}) does not have private endpoint connections." findings.append(report) return findings diff --git a/prowler/providers/azure/services/storage/storage_ensure_soft_delete_is_enabled/storage_ensure_soft_delete_is_enabled.py b/prowler/providers/azure/services/storage/storage_ensure_soft_delete_is_enabled/storage_ensure_soft_delete_is_enabled.py index 7f8d3b39f4..965acfb5a5 100644 --- a/prowler/providers/azure/services/storage/storage_ensure_soft_delete_is_enabled/storage_ensure_soft_delete_is_enabled.py +++ b/prowler/providers/azure/services/storage/storage_ensure_soft_delete_is_enabled/storage_ensure_soft_delete_is_enabled.py @@ -6,6 +6,9 @@ class storage_ensure_soft_delete_is_enabled(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, storage_accounts in storage_client.storage_accounts.items(): + subscription_name = storage_client.subscriptions.get( + subscription, subscription + ) for storage_account in storage_accounts: if storage_account.blob_properties: report = Check_Report_Azure( @@ -18,10 +21,10 @@ class storage_ensure_soft_delete_is_enabled(Check): False, ): report.status = "PASS" - report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} has soft delete enabled." + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription_name} ({subscription}) has soft delete enabled." else: report.status = "FAIL" - report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} has soft delete disabled." + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription_name} ({subscription}) has soft delete disabled." findings.append(report) return findings diff --git a/prowler/providers/azure/services/storage/storage_geo_redundant_enabled/storage_geo_redundant_enabled.py b/prowler/providers/azure/services/storage/storage_geo_redundant_enabled/storage_geo_redundant_enabled.py index ee2d49e53d..8c71ee4819 100644 --- a/prowler/providers/azure/services/storage/storage_geo_redundant_enabled/storage_geo_redundant_enabled.py +++ b/prowler/providers/azure/services/storage/storage_geo_redundant_enabled/storage_geo_redundant_enabled.py @@ -19,6 +19,9 @@ class storage_geo_redundant_enabled(Check): """ findings = [] for subscription, storage_accounts in storage_client.storage_accounts.items(): + subscription_name = storage_client.subscriptions.get( + subscription, subscription + ) for storage_account in storage_accounts: report = Check_Report_Azure( metadata=self.metadata(), resource=storage_account @@ -32,10 +35,10 @@ class storage_geo_redundant_enabled(Check): or storage_account.replication_settings == "Standard_RAGZRS" ): report.status = "PASS" - report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} has Geo-redundant storage {storage_account.replication_settings} enabled." + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription_name} ({subscription}) has Geo-redundant storage {storage_account.replication_settings} enabled." else: report.status = "FAIL" - report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} does not have Geo-redundant storage enabled, it has {storage_account.replication_settings} instead." + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription_name} ({subscription}) does not have Geo-redundant storage enabled, it has {storage_account.replication_settings} instead." findings.append(report) diff --git a/prowler/providers/azure/services/storage/storage_infrastructure_encryption_is_enabled/storage_infrastructure_encryption_is_enabled.py b/prowler/providers/azure/services/storage/storage_infrastructure_encryption_is_enabled/storage_infrastructure_encryption_is_enabled.py index cb969975c2..4294419d24 100644 --- a/prowler/providers/azure/services/storage/storage_infrastructure_encryption_is_enabled/storage_infrastructure_encryption_is_enabled.py +++ b/prowler/providers/azure/services/storage/storage_infrastructure_encryption_is_enabled/storage_infrastructure_encryption_is_enabled.py @@ -6,16 +6,19 @@ class storage_infrastructure_encryption_is_enabled(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, storage_accounts in storage_client.storage_accounts.items(): + subscription_name = storage_client.subscriptions.get( + subscription, subscription + ) for storage_account in storage_accounts: report = Check_Report_Azure( metadata=self.metadata(), resource=storage_account ) report.subscription = subscription report.status = "PASS" - report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} has infrastructure encryption enabled." + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription_name} ({subscription}) has infrastructure encryption enabled." if not storage_account.infrastructure_encryption: report.status = "FAIL" - report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} has infrastructure encryption disabled." + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription_name} ({subscription}) has infrastructure encryption disabled." findings.append(report) diff --git a/prowler/providers/azure/services/storage/storage_key_rotation_90_days/storage_key_rotation_90_days.py b/prowler/providers/azure/services/storage/storage_key_rotation_90_days/storage_key_rotation_90_days.py index 0007b51e6b..9fa07d029c 100644 --- a/prowler/providers/azure/services/storage/storage_key_rotation_90_days/storage_key_rotation_90_days.py +++ b/prowler/providers/azure/services/storage/storage_key_rotation_90_days/storage_key_rotation_90_days.py @@ -6,6 +6,9 @@ class storage_key_rotation_90_days(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, storage_accounts in storage_client.storage_accounts.items(): + subscription_name = storage_client.subscriptions.get( + subscription, subscription + ) for storage_account in storage_accounts: report = Check_Report_Azure( metadata=self.metadata(), resource=storage_account @@ -13,14 +16,14 @@ class storage_key_rotation_90_days(Check): report.subscription = subscription if not storage_account.key_expiration_period_in_days: report.status = "FAIL" - report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} has no key expiration period set." + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription_name} ({subscription}) has no key expiration period set." else: if storage_account.key_expiration_period_in_days > 90: report.status = "FAIL" - report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} has an invalid key expiration period of {storage_account.key_expiration_period_in_days} days." + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription_name} ({subscription}) has an invalid key expiration period of {storage_account.key_expiration_period_in_days} days." else: report.status = "PASS" - report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} has a key expiration period of {storage_account.key_expiration_period_in_days} days." + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription_name} ({subscription}) has a key expiration period of {storage_account.key_expiration_period_in_days} days." findings.append(report) return findings diff --git a/prowler/providers/azure/services/storage/storage_secure_transfer_required_is_enabled/storage_secure_transfer_required_is_enabled.py b/prowler/providers/azure/services/storage/storage_secure_transfer_required_is_enabled/storage_secure_transfer_required_is_enabled.py index 0711ab3991..ec4e4e0bfa 100644 --- a/prowler/providers/azure/services/storage/storage_secure_transfer_required_is_enabled/storage_secure_transfer_required_is_enabled.py +++ b/prowler/providers/azure/services/storage/storage_secure_transfer_required_is_enabled/storage_secure_transfer_required_is_enabled.py @@ -6,16 +6,19 @@ class storage_secure_transfer_required_is_enabled(Check): def execute(self) -> Check_Report_Azure: findings = [] for subscription, storage_accounts in storage_client.storage_accounts.items(): + subscription_name = storage_client.subscriptions.get( + subscription, subscription + ) for storage_account in storage_accounts: report = Check_Report_Azure( metadata=self.metadata(), resource=storage_account ) report.subscription = subscription report.status = "PASS" - report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} has secure transfer required enabled." + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription_name} ({subscription}) has secure transfer required enabled." if not storage_account.enable_https_traffic_only: report.status = "FAIL" - report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} has secure transfer required disabled." + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription_name} ({subscription}) has secure transfer required disabled." findings.append(report) diff --git a/prowler/providers/azure/services/storage/storage_service.py b/prowler/providers/azure/services/storage/storage_service.py index 429f5ba7e3..74b8b3da30 100644 --- a/prowler/providers/azure/services/storage/storage_service.py +++ b/prowler/providers/azure/services/storage/storage_service.py @@ -42,6 +42,9 @@ class Storage(AzureService): enable_https_traffic_only=storage_account.enable_https_traffic_only, infrastructure_encryption=storage_account.encryption.require_infrastructure_encryption, allow_blob_public_access=storage_account.allow_blob_public_access, + public_network_access=getattr( + storage_account, "public_network_access", None + ), network_rule_set=NetworkRuleSet( bypass=getattr( storage_account.network_rule_set, @@ -111,7 +114,7 @@ class Storage(AzureService): ) except Exception as error: logger.error( - f"Subscription name: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return storage_accounts @@ -156,16 +159,16 @@ class Storage(AzureService): in str(error).strip() ): logger.warning( - f"Subscription name: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) continue logger.error( - f"Subscription name: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) except Exception as error: logger.error( - f"Subscription name: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) def _get_file_share_properties(self): @@ -247,11 +250,11 @@ class Storage(AzureService): except Exception as error: if "File is not supported for the account." in str(error).strip(): logger.warning( - f"Subscription name: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) continue logger.error( - f"Subscription name: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) @@ -301,6 +304,7 @@ class Account(BaseModel): enable_https_traffic_only: bool infrastructure_encryption: Optional[bool] = None allow_blob_public_access: bool + public_network_access: Optional[str] = None network_rule_set: NetworkRuleSet encryption_type: str minimum_tls_version: str diff --git a/prowler/providers/azure/services/storage/storage_smb_channel_encryption_with_secure_algorithm/storage_smb_channel_encryption_with_secure_algorithm.metadata.json b/prowler/providers/azure/services/storage/storage_smb_channel_encryption_with_secure_algorithm/storage_smb_channel_encryption_with_secure_algorithm.metadata.json index 0649b30110..3bffd83e67 100644 --- a/prowler/providers/azure/services/storage/storage_smb_channel_encryption_with_secure_algorithm/storage_smb_channel_encryption_with_secure_algorithm.metadata.json +++ b/prowler/providers/azure/services/storage/storage_smb_channel_encryption_with_secure_algorithm/storage_smb_channel_encryption_with_secure_algorithm.metadata.json @@ -34,5 +34,5 @@ ], "DependsOn": [], "RelatedTo": [], - "Notes": "This check passes if SMB channel encryption is set to a secure algorithm." + "Notes": "This check passes only if every SMB channel encryption algorithm allowed on the file shares is in the recommended list, which is configurable via azure.recommended_smb_channel_encryption_algorithms and defaults to AES-256-GCM only, as required by CIS." } diff --git a/prowler/providers/azure/services/storage/storage_smb_channel_encryption_with_secure_algorithm/storage_smb_channel_encryption_with_secure_algorithm.py b/prowler/providers/azure/services/storage/storage_smb_channel_encryption_with_secure_algorithm/storage_smb_channel_encryption_with_secure_algorithm.py index c8e9f1da3c..61d4abc185 100644 --- a/prowler/providers/azure/services/storage/storage_smb_channel_encryption_with_secure_algorithm/storage_smb_channel_encryption_with_secure_algorithm.py +++ b/prowler/providers/azure/services/storage/storage_smb_channel_encryption_with_secure_algorithm/storage_smb_channel_encryption_with_secure_algorithm.py @@ -1,29 +1,38 @@ from prowler.lib.check.models import Check, Check_Report_Azure from prowler.providers.azure.services.storage.storage_client import storage_client -SECURE_ENCRYPTION_ALGORITHMS = ["AES-256-GCM"] +DEFAULT_SECURE_ENCRYPTION_ALGORITHMS = ["AES-256-GCM"] class storage_smb_channel_encryption_with_secure_algorithm(Check): """ - Ensure SMB channel encryption for file shares is set to the recommended algorithm (AES-256-GCM or higher). + Ensure SMB channel encryption for file shares only allows secure algorithms (AES-256-GCM or higher by default). + + The list of allowed algorithms is configurable via + azure.recommended_smb_channel_encryption_algorithms in the Prowler configuration file. This check evaluates whether SMB file shares are configured to use only the recommended SMB channel encryption algorithms. - - PASS: Storage account has the recommended SMB channel encryption (AES-256-GCM or higher) enabled for file shares. - - FAIL: Storage account does not have the recommended SMB channel encryption enabled for file shares or uses an unsupported algorithm. + - PASS: Storage account only allows secure SMB channel encryption algorithms for file shares. + - FAIL: Storage account does not have SMB channel encryption enabled, or it allows at least one algorithm that is not in the recommended list. """ def execute(self) -> list[Check_Report_Azure]: findings = [] + secure_encryption_algorithms = storage_client.audit_config.get( + "recommended_smb_channel_encryption_algorithms", + DEFAULT_SECURE_ENCRYPTION_ALGORITHMS, + ) for subscription, storage_accounts in storage_client.storage_accounts.items(): + subscription_name = storage_client.subscriptions.get( + subscription, subscription + ) for account in storage_accounts: if account.file_service_properties: + channel_encryption = ( + account.file_service_properties.smb_protocol_settings.channel_encryption + ) pretty_current_algorithms = ( - ", ".join( - account.file_service_properties.smb_protocol_settings.channel_encryption - ) - if account.file_service_properties.smb_protocol_settings.channel_encryption - else "none" + ", ".join(channel_encryption) if channel_encryption else "none" ) report = Check_Report_Azure( metadata=self.metadata(), @@ -32,20 +41,18 @@ class storage_smb_channel_encryption_with_secure_algorithm(Check): report.subscription = subscription report.resource_name = account.name - if ( - not account.file_service_properties.smb_protocol_settings.channel_encryption - ): + if not channel_encryption: report.status = "FAIL" - report.status_extended = f"Storage account {account.name} from subscription {subscription} does not have SMB channel encryption enabled for file shares." - elif any( - algorithm in SECURE_ENCRYPTION_ALGORITHMS - for algorithm in account.file_service_properties.smb_protocol_settings.channel_encryption + report.status_extended = f"Storage account {account.name} from subscription {subscription_name} ({subscription}) does not have SMB channel encryption enabled for file shares." + elif all( + algorithm in secure_encryption_algorithms + for algorithm in channel_encryption ): report.status = "PASS" - report.status_extended = f"Storage account {account.name} from subscription {subscription} has a secure algorithm for SMB channel encryption ({', '.join(SECURE_ENCRYPTION_ALGORITHMS)}) enabled for file shares since it supports {pretty_current_algorithms}." + report.status_extended = f"Storage account {account.name} from subscription {subscription_name} ({subscription}) only allows secure algorithms for SMB channel encryption on file shares since it supports {pretty_current_algorithms}." else: report.status = "FAIL" - report.status_extended = f"Storage account {account.name} from subscription {subscription} does not have SMB channel encryption with a secure algorithm for file shares since it supports {pretty_current_algorithms}." + report.status_extended = f"Storage account {account.name} from subscription {subscription_name} ({subscription}) allows insecure algorithms for SMB channel encryption on file shares since it supports {pretty_current_algorithms} and only {', '.join(secure_encryption_algorithms)} is recommended." findings.append(report) return findings diff --git a/prowler/providers/azure/services/storage/storage_smb_protocol_version_is_latest/storage_smb_protocol_version_is_latest.py b/prowler/providers/azure/services/storage/storage_smb_protocol_version_is_latest/storage_smb_protocol_version_is_latest.py index 19f2d37765..7bc928756d 100644 --- a/prowler/providers/azure/services/storage/storage_smb_protocol_version_is_latest/storage_smb_protocol_version_is_latest.py +++ b/prowler/providers/azure/services/storage/storage_smb_protocol_version_is_latest/storage_smb_protocol_version_is_latest.py @@ -16,6 +16,9 @@ class storage_smb_protocol_version_is_latest(Check): findings = [] for subscription, storage_accounts in storage_client.storage_accounts.items(): + subscription_name = storage_client.subscriptions.get( + subscription, subscription + ) for account in storage_accounts: if getattr(account, "file_service_properties", None) and getattr( account.file_service_properties.smb_protocol_settings, @@ -40,9 +43,9 @@ class storage_smb_protocol_version_is_latest(Check): == LATEST_SMB_VERSION ): report.status = "PASS" - report.status_extended = f"Storage account {account.name} from subscription {subscription} allows only the latest SMB protocol version ({LATEST_SMB_VERSION}) for file shares." + report.status_extended = f"Storage account {account.name} from subscription {subscription_name} ({subscription}) allows only the latest SMB protocol version ({LATEST_SMB_VERSION}) for file shares." else: report.status = "FAIL" - report.status_extended = f"Storage account {account.name} from subscription {subscription} allows SMB protocol versions: {', '.join(account.file_service_properties.smb_protocol_settings.supported_versions) if account.file_service_properties.smb_protocol_settings.supported_versions else 'None'}. Only the latest SMB protocol version ({LATEST_SMB_VERSION}) should be allowed." + report.status_extended = f"Storage account {account.name} from subscription {subscription_name} ({subscription}) allows SMB protocol versions: {', '.join(account.file_service_properties.smb_protocol_settings.supported_versions) if account.file_service_properties.smb_protocol_settings.supported_versions else 'None'}. Only the latest SMB protocol version ({LATEST_SMB_VERSION}) should be allowed." findings.append(report) return findings diff --git a/prowler/providers/azure/services/vm/vm_backup_enabled/vm_backup_enabled.py b/prowler/providers/azure/services/vm/vm_backup_enabled/vm_backup_enabled.py index d5865937f9..1238b75d2f 100644 --- a/prowler/providers/azure/services/vm/vm_backup_enabled/vm_backup_enabled.py +++ b/prowler/providers/azure/services/vm/vm_backup_enabled/vm_backup_enabled.py @@ -22,8 +22,11 @@ class vm_backup_enabled(Check): A list of reports containing the result of the check. """ findings = [] - for subscription_name, vms in vm_client.virtual_machines.items(): - vaults = recovery_client.vaults.get(subscription_name, {}) + for subscription_id, vms in vm_client.virtual_machines.items(): + subscription_name = recovery_client.subscriptions.get( + subscription_id, subscription_id + ) + vaults = recovery_client.vaults.get(subscription_id, {}) for vm in vms.values(): found = False found_vault_name = None @@ -40,12 +43,12 @@ class vm_backup_enabled(Check): if found: break report = Check_Report_Azure(metadata=self.metadata(), resource=vm) - report.subscription = subscription_name + report.subscription = subscription_id if found: report.status = "PASS" - report.status_extended = f"VM {vm.resource_name} in subscription {subscription_name} is protected by Azure Backup (vault: {found_vault_name})." + report.status_extended = f"VM {vm.resource_name} in subscription {subscription_name} ({subscription_id}) is protected by Azure Backup (vault: {found_vault_name})." else: report.status = "FAIL" - report.status_extended = f"VM {vm.resource_name} in subscription {subscription_name} is not protected by Azure Backup." + report.status_extended = f"VM {vm.resource_name} in subscription {subscription_name} ({subscription_id}) is not protected by Azure Backup." findings.append(report) return findings diff --git a/prowler/providers/azure/services/vm/vm_desired_sku_size/vm_desired_sku_size.py b/prowler/providers/azure/services/vm/vm_desired_sku_size/vm_desired_sku_size.py index ac3df970b4..68164a7282 100644 --- a/prowler/providers/azure/services/vm/vm_desired_sku_size/vm_desired_sku_size.py +++ b/prowler/providers/azure/services/vm/vm_desired_sku_size/vm_desired_sku_size.py @@ -32,17 +32,20 @@ class vm_desired_sku_size(Check): ], ) - for subscription_name, vms in vm_client.virtual_machines.items(): + for subscription_id, vms in vm_client.virtual_machines.items(): + subscription_name = vm_client.subscriptions.get( + subscription_id, subscription_id + ) for vm in vms.values(): report = Check_Report_Azure(metadata=self.metadata(), resource=vm) - report.subscription = subscription_name + report.subscription = subscription_id if vm.vm_size in DESIRED_SKU_SIZES: report.status = "PASS" - report.status_extended = f"VM {vm.resource_name} is using desired SKU size {vm.vm_size} in subscription {subscription_name}." + report.status_extended = f"VM {vm.resource_name} is using desired SKU size {vm.vm_size} in subscription {subscription_name} ({subscription_id})." else: report.status = "FAIL" - report.status_extended = f"VM {vm.resource_name} is using {vm.vm_size} which is not a desired SKU size in subscription {subscription_name}." + report.status_extended = f"VM {vm.resource_name} is using {vm.vm_size} which is not a desired SKU size in subscription {subscription_name} ({subscription_id})." findings.append(report) diff --git a/prowler/providers/azure/services/vm/vm_ensure_attached_disks_encrypted_with_cmk/vm_ensure_attached_disks_encrypted_with_cmk.py b/prowler/providers/azure/services/vm/vm_ensure_attached_disks_encrypted_with_cmk/vm_ensure_attached_disks_encrypted_with_cmk.py index e803110a9c..029f9c8775 100644 --- a/prowler/providers/azure/services/vm/vm_ensure_attached_disks_encrypted_with_cmk/vm_ensure_attached_disks_encrypted_with_cmk.py +++ b/prowler/providers/azure/services/vm/vm_ensure_attached_disks_encrypted_with_cmk/vm_ensure_attached_disks_encrypted_with_cmk.py @@ -6,20 +6,23 @@ class vm_ensure_attached_disks_encrypted_with_cmk(Check): def execute(self) -> Check_Report_Azure: findings = [] - for subscription_name, disks in vm_client.disks.items(): + for subscription_id, disks in vm_client.disks.items(): + subscription_name = vm_client.subscriptions.get( + subscription_id, subscription_id + ) for disk_id, disk in disks.items(): if disk.vms_attached: report = Check_Report_Azure(metadata=self.metadata(), resource=disk) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "PASS" - report.status_extended = f"Disk '{disk_id}' is encrypted with a customer-managed key in subscription {subscription_name}." + report.status_extended = f"Disk '{disk_id}' is encrypted with a customer-managed key in subscription {subscription_name} ({subscription_id})." if ( not disk.encryption_type or disk.encryption_type == "EncryptionAtRestWithPlatformKey" ): report.status = "FAIL" - report.status_extended = f"Disk '{disk_id}' is not encrypted with a customer-managed key in subscription {subscription_name}." + report.status_extended = f"Disk '{disk_id}' is not encrypted with a customer-managed key in subscription {subscription_name} ({subscription_id})." findings.append(report) diff --git a/prowler/providers/azure/services/vm/vm_ensure_unattached_disks_encrypted_with_cmk/vm_ensure_unattached_disks_encrypted_with_cmk.py b/prowler/providers/azure/services/vm/vm_ensure_unattached_disks_encrypted_with_cmk/vm_ensure_unattached_disks_encrypted_with_cmk.py index ecf9cd0f87..f4e86296f4 100644 --- a/prowler/providers/azure/services/vm/vm_ensure_unattached_disks_encrypted_with_cmk/vm_ensure_unattached_disks_encrypted_with_cmk.py +++ b/prowler/providers/azure/services/vm/vm_ensure_unattached_disks_encrypted_with_cmk/vm_ensure_unattached_disks_encrypted_with_cmk.py @@ -6,20 +6,23 @@ class vm_ensure_unattached_disks_encrypted_with_cmk(Check): def execute(self) -> Check_Report_Azure: findings = [] - for subscription_name, disks in vm_client.disks.items(): + for subscription_id, disks in vm_client.disks.items(): + subscription_name = vm_client.subscriptions.get( + subscription_id, subscription_id + ) for disk_id, disk in disks.items(): if not disk.vms_attached: report = Check_Report_Azure(metadata=self.metadata(), resource=disk) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "PASS" - report.status_extended = f"Disk '{disk_id}' is encrypted with a customer-managed key in subscription {subscription_name}." + report.status_extended = f"Disk '{disk_id}' is encrypted with a customer-managed key in subscription {subscription_name} ({subscription_id})." if ( not disk.encryption_type or disk.encryption_type == "EncryptionAtRestWithPlatformKey" ): report.status = "FAIL" - report.status_extended = f"Disk '{disk_id}' is not encrypted with a customer-managed key in subscription {subscription_name}." + report.status_extended = f"Disk '{disk_id}' is not encrypted with a customer-managed key in subscription {subscription_name} ({subscription_id})." findings.append(report) diff --git a/prowler/providers/azure/services/vm/vm_ensure_using_approved_images/vm_ensure_using_approved_images.py b/prowler/providers/azure/services/vm/vm_ensure_using_approved_images/vm_ensure_using_approved_images.py index 4f6c378777..efd54edac4 100644 --- a/prowler/providers/azure/services/vm/vm_ensure_using_approved_images/vm_ensure_using_approved_images.py +++ b/prowler/providers/azure/services/vm/vm_ensure_using_approved_images/vm_ensure_using_approved_images.py @@ -14,10 +14,13 @@ class vm_ensure_using_approved_images(Check): def execute(self): findings = [] - for subscription_name, vms in vm_client.virtual_machines.items(): + for subscription_id, vms in vm_client.virtual_machines.items(): + subscription_name = vm_client.subscriptions.get( + subscription_id, subscription_id + ) for vm in vms.values(): report = Check_Report_Azure(metadata=self.metadata(), resource=vm) - report.subscription = subscription_name + report.subscription = subscription_id image_id = getattr(vm, "image_reference", None) if ( image_id @@ -25,9 +28,9 @@ class vm_ensure_using_approved_images(Check): and "/providers/Microsoft.Compute/images/" in image_id ): report.status = "PASS" - report.status_extended = f"VM {vm.resource_name} in subscription {subscription_name} is using an approved machine image: {image_id.split('/')[-1]}." + report.status_extended = f"VM {vm.resource_name} in subscription {subscription_name} ({subscription_id}) is using an approved machine image: {image_id.split('/')[-1]}." else: report.status = "FAIL" - report.status_extended = f"VM {vm.resource_name} in subscription {subscription_name} is not using an approved machine image." + report.status_extended = f"VM {vm.resource_name} in subscription {subscription_name} ({subscription_id}) is not using an approved machine image." findings.append(report) return findings diff --git a/prowler/providers/azure/services/vm/vm_ensure_using_managed_disks/vm_ensure_using_managed_disks.py b/prowler/providers/azure/services/vm/vm_ensure_using_managed_disks/vm_ensure_using_managed_disks.py index e771fc80e5..316cc426a0 100644 --- a/prowler/providers/azure/services/vm/vm_ensure_using_managed_disks/vm_ensure_using_managed_disks.py +++ b/prowler/providers/azure/services/vm/vm_ensure_using_managed_disks/vm_ensure_using_managed_disks.py @@ -6,12 +6,15 @@ class vm_ensure_using_managed_disks(Check): def execute(self) -> Check_Report_Azure: findings = [] - for subscription_name, vms in vm_client.virtual_machines.items(): + for subscription_id, vms in vm_client.virtual_machines.items(): + subscription_name = vm_client.subscriptions.get( + subscription_id, subscription_id + ) for vm in vms.values(): report = Check_Report_Azure(metadata=self.metadata(), resource=vm) report.status = "PASS" - report.subscription = subscription_name - report.status_extended = f"VM {vm.resource_name} is using managed disks in subscription {subscription_name}" + report.subscription = subscription_id + report.status_extended = f"VM {vm.resource_name} is using managed disks in subscription {subscription_name} ({subscription_id})" using_managed_disks = ( True @@ -31,7 +34,7 @@ class vm_ensure_using_managed_disks(Check): if not using_managed_disks: report.status = "FAIL" - report.status_extended = f"VM {vm.resource_name} is not using managed disks in subscription {subscription_name}" + report.status_extended = f"VM {vm.resource_name} is not using managed disks in subscription {subscription_name} ({subscription_id})" findings.append(report) diff --git a/prowler/providers/azure/services/vm/vm_jit_access_enabled/vm_jit_access_enabled.py b/prowler/providers/azure/services/vm/vm_jit_access_enabled/vm_jit_access_enabled.py index 9434608454..913b972b9b 100644 --- a/prowler/providers/azure/services/vm/vm_jit_access_enabled/vm_jit_access_enabled.py +++ b/prowler/providers/azure/services/vm/vm_jit_access_enabled/vm_jit_access_enabled.py @@ -15,19 +15,22 @@ class vm_jit_access_enabled(Check): def execute(self): findings = [] jit_enabled_vms = set() - for subscription_name, vms in vm_client.virtual_machines.items(): - for jit_policy in defender_client.jit_policies[subscription_name].values(): + for subscription_id, vms in vm_client.virtual_machines.items(): + subscription_name = defender_client.subscriptions.get( + subscription_id, subscription_id + ) + for jit_policy in defender_client.jit_policies[subscription_id].values(): jit_enabled_vms.update(jit_policy.vm_ids) for vm in vms.values(): report = Check_Report_Azure(metadata=self.metadata(), resource=vm) - report.subscription = subscription_name + report.subscription = subscription_id if vm.resource_id.lower() in { vm_id.lower() for vm_id in jit_enabled_vms }: report.status = "PASS" - report.status_extended = f"VM {vm.resource_name} in subscription {subscription_name} has JIT (Just-in-Time) access enabled." + report.status_extended = f"VM {vm.resource_name} in subscription {subscription_name} ({subscription_id}) has JIT (Just-in-Time) access enabled." else: report.status = "FAIL" - report.status_extended = f"VM {vm.resource_name} in subscription {subscription_name} does not have JIT (Just-in-Time) access enabled." + report.status_extended = f"VM {vm.resource_name} in subscription {subscription_name} ({subscription_id}) does not have JIT (Just-in-Time) access enabled." findings.append(report) return findings diff --git a/prowler/providers/azure/services/vm/vm_linux_enforce_ssh_authentication/vm_linux_enforce_ssh_authentication.py b/prowler/providers/azure/services/vm/vm_linux_enforce_ssh_authentication/vm_linux_enforce_ssh_authentication.py index c31b2c9a18..7783a42009 100644 --- a/prowler/providers/azure/services/vm/vm_linux_enforce_ssh_authentication/vm_linux_enforce_ssh_authentication.py +++ b/prowler/providers/azure/services/vm/vm_linux_enforce_ssh_authentication/vm_linux_enforce_ssh_authentication.py @@ -13,17 +13,20 @@ class vm_linux_enforce_ssh_authentication(Check): def execute(self) -> list[Check_Report_Azure]: findings = [] - for subscription_name, vms in vm_client.virtual_machines.items(): + for subscription_id, vms in vm_client.virtual_machines.items(): + subscription_name = vm_client.subscriptions.get( + subscription_id, subscription_id + ) for vm in vms.values(): if vm.linux_configuration: report = Check_Report_Azure(metadata=self.metadata(), resource=vm) - report.subscription = subscription_name + report.subscription = subscription_id if vm.linux_configuration.disable_password_authentication: report.status = "PASS" - report.status_extended = f"VM {vm.resource_name} in subscription {subscription_name} has password authentication disabled (SSH key authentication enforced)." + report.status_extended = f"VM {vm.resource_name} in subscription {subscription_name} ({subscription_id}) has password authentication disabled (SSH key authentication enforced)." else: report.status = "FAIL" - report.status_extended = f"VM {vm.resource_name} in subscription {subscription_name} has password authentication enabled (password-based SSH allowed)." + report.status_extended = f"VM {vm.resource_name} in subscription {subscription_name} ({subscription_id}) has password authentication enabled (password-based SSH allowed)." findings.append(report) return findings diff --git a/prowler/providers/azure/services/vm/vm_scaleset_associated_with_load_balancer/vm_scaleset_associated_with_load_balancer.py b/prowler/providers/azure/services/vm/vm_scaleset_associated_with_load_balancer/vm_scaleset_associated_with_load_balancer.py index c6150f1d7f..4b893397b1 100644 --- a/prowler/providers/azure/services/vm/vm_scaleset_associated_with_load_balancer/vm_scaleset_associated_with_load_balancer.py +++ b/prowler/providers/azure/services/vm/vm_scaleset_associated_with_load_balancer/vm_scaleset_associated_with_load_balancer.py @@ -14,6 +14,7 @@ class vm_scaleset_associated_with_load_balancer(Check): def execute(self): findings = [] for subscription, scale_sets in vm_client.vm_scale_sets.items(): + subscription_name = vm_client.subscriptions.get(subscription, subscription) for scale_set in scale_sets.values(): report = Check_Report_Azure( metadata=self.metadata(), resource=scale_set @@ -28,9 +29,9 @@ class vm_scaleset_associated_with_load_balancer(Check): pool.split("/")[-1] for pool in scale_set.load_balancer_backend_pools ] - report.status_extended = f"Scale set '{scale_set.resource_name}' in subscription '{subscription}' is associated with load balancer backend pool(s): {', '.join(backend_pool_names)}." + report.status_extended = f"Scale set '{scale_set.resource_name}' in subscription '{subscription_name} ({subscription})' is associated with load balancer backend pool(s): {', '.join(backend_pool_names)}." else: report.status = "FAIL" - report.status_extended = f"Scale set '{scale_set.resource_name}' in subscription '{subscription}' is not associated with any load balancer backend pool." + report.status_extended = f"Scale set '{scale_set.resource_name}' in subscription '{subscription_name} ({subscription})' is not associated with any load balancer backend pool." findings.append(report) return findings diff --git a/prowler/providers/azure/services/vm/vm_scaleset_not_empty/vm_scaleset_not_empty.py b/prowler/providers/azure/services/vm/vm_scaleset_not_empty/vm_scaleset_not_empty.py index 4061fe4790..ac77dd01e0 100644 --- a/prowler/providers/azure/services/vm/vm_scaleset_not_empty/vm_scaleset_not_empty.py +++ b/prowler/providers/azure/services/vm/vm_scaleset_not_empty/vm_scaleset_not_empty.py @@ -14,6 +14,7 @@ class vm_scaleset_not_empty(Check): def execute(self): findings = [] for subscription, scale_sets in vm_client.vm_scale_sets.items(): + subscription_name = vm_client.subscriptions.get(subscription, subscription) for scale_set in scale_sets.values(): report = Check_Report_Azure( metadata=self.metadata(), resource=scale_set @@ -21,9 +22,9 @@ class vm_scaleset_not_empty(Check): report.subscription = subscription if not scale_set.instance_ids: report.status = "FAIL" - report.status_extended = f"Scale set '{scale_set.resource_name}' in subscription '{subscription}' is empty: no VM instances present." + report.status_extended = f"Scale set '{scale_set.resource_name}' in subscription '{subscription_name} ({subscription})' is empty: no VM instances present." else: report.status = "PASS" - report.status_extended = f"Scale set '{scale_set.resource_name}' in subscription '{subscription}' has {len(scale_set.instance_ids)} VM instances." + report.status_extended = f"Scale set '{scale_set.resource_name}' in subscription '{subscription_name} ({subscription})' has {len(scale_set.instance_ids)} VM instances." findings.append(report) return findings diff --git a/prowler/providers/azure/services/vm/vm_service.py b/prowler/providers/azure/services/vm/vm_service.py index ea63de6197..b20f4b5678 100644 --- a/prowler/providers/azure/services/vm/vm_service.py +++ b/prowler/providers/azure/services/vm/vm_service.py @@ -20,10 +20,10 @@ class VirtualMachines(AzureService): logger.info("VirtualMachines - Getting virtual machines...") virtual_machines = {} - for subscription_name, client in self.clients.items(): + for subscription_id, client in self.clients.items(): try: virtual_machines_list = client.virtual_machines.list_all() - virtual_machines.update({subscription_name: {}}) + virtual_machines.update({subscription_id: {}}) for vm in virtual_machines_list: storage_profile = getattr(vm, "storage_profile", None) @@ -98,7 +98,7 @@ class VirtualMachines(AzureService): uefi_settings=uefi_settings, ) - virtual_machines[subscription_name].update( + virtual_machines[subscription_id].update( { vm.id: VirtualMachine( resource_id=vm.id, @@ -144,7 +144,7 @@ class VirtualMachines(AzureService): ) except Exception as error: logger.error( - f"Subscription name: {subscription_name} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription_id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return virtual_machines @@ -153,10 +153,10 @@ class VirtualMachines(AzureService): logger.info("VirtualMachines - Getting disks...") disks = {} - for subscription_name, client in self.clients.items(): + for subscription_id, client in self.clients.items(): try: disks_list = client.disks.list() - disks.update({subscription_name: {}}) + disks.update({subscription_id: {}}) for disk in disks_list: vms_attached = [] @@ -164,7 +164,7 @@ class VirtualMachines(AzureService): vms_attached.append(disk.managed_by) if disk.managed_by_extended: vms_attached.extend(disk.managed_by_extended) - disks[subscription_name].update( + disks[subscription_id].update( { disk.unique_id: Disk( resource_id=disk.id, @@ -179,7 +179,7 @@ class VirtualMachines(AzureService): ) except Exception as error: logger.error( - f"Subscription name: {subscription_name} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription_id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return disks @@ -191,7 +191,7 @@ class VirtualMachines(AzureService): Returns: A nested dictionary with the following structure: { - "subscription_name": { + "subscription_id": { "vm_scale_set_id": VirtualMachineScaleSet() } } @@ -200,10 +200,10 @@ class VirtualMachines(AzureService): "VirtualMachines - Getting VM scale sets and their load balancer associations..." ) vm_scale_sets = {} - for subscription_name, client in self.clients.items(): + for subscription_id, client in self.clients.items(): try: scale_sets = client.virtual_machine_scale_sets.list_all() - vm_scale_sets[subscription_name] = {} + vm_scale_sets[subscription_id] = {} for scale_set in scale_sets: backend_pools = [] nic_configs = [] @@ -235,9 +235,9 @@ class VirtualMachines(AzureService): backend_pools.append(pool.id) # Get instance IDs using the private method instance_ids = self._get_vmss_instance_ids( - subscription_name, scale_set.id + subscription_id, scale_set.id ) - vm_scale_sets[subscription_name][scale_set.id] = ( + vm_scale_sets[subscription_id][scale_set.id] = ( VirtualMachineScaleSet( resource_id=scale_set.id, resource_name=scale_set.name, @@ -248,28 +248,28 @@ class VirtualMachines(AzureService): ) except Exception as error: logger.error( - f"Subscription name: {subscription_name} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription ID: {subscription_id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return vm_scale_sets def _get_vmss_instance_ids( - self, subscription_name: str, scale_set_id: str + self, subscription_id: str, scale_set_id: str ) -> list[str]: """ Given a subscription and scale set ID, return the list of VM instance IDs in the scale set. Args: - subscription_name: The name of the subscription. + subscription_id: The name of the subscription. scale_set_id: The ID of the scale set. Returns: A list of VM instance IDs that compose the scale set. """ logger.info( - f"VirtualMachines - Getting VM scale set instance IDs for {scale_set_id} in {subscription_name}..." + f"VirtualMachines - Getting VM scale set instance IDs for {scale_set_id} in {subscription_id}..." ) vm_instance_ids = [] - client = self.clients.get(subscription_name, None) + client = self.clients.get(subscription_id, None) try: resource_id_parts = scale_set_id.split("/") resource_group = "" diff --git a/prowler/providers/azure/services/vm/vm_sufficient_daily_backup_retention_period/vm_sufficient_daily_backup_retention_period.py b/prowler/providers/azure/services/vm/vm_sufficient_daily_backup_retention_period/vm_sufficient_daily_backup_retention_period.py index 444cfcfa3b..09017d26b5 100644 --- a/prowler/providers/azure/services/vm/vm_sufficient_daily_backup_retention_period/vm_sufficient_daily_backup_retention_period.py +++ b/prowler/providers/azure/services/vm/vm_sufficient_daily_backup_retention_period/vm_sufficient_daily_backup_retention_period.py @@ -19,6 +19,9 @@ class vm_sufficient_daily_backup_retention_period(Check): ) for subscription, vms in vm_client.virtual_machines.items(): + subscription_name = recovery_client.subscriptions.get( + subscription, subscription + ) vaults = recovery_client.vaults.get(subscription, {}) for vm in vms.values(): backup_found = False @@ -44,9 +47,9 @@ class vm_sufficient_daily_backup_retention_period(Check): report.subscription = subscription if retention_days >= min_retention_days: report.status = "PASS" - report.status_extended = f"VM {vm.resource_name} in subscription {subscription} has a daily backup retention period of {retention_days} days (minimum required: {min_retention_days})." + report.status_extended = f"VM {vm.resource_name} in subscription {subscription_name} ({subscription}) has a daily backup retention period of {retention_days} days (minimum required: {min_retention_days})." else: report.status = "FAIL" - report.status_extended = f"VM {vm.resource_name} in subscription {subscription} has insufficient daily backup retention period of {retention_days} days (minimum required: {min_retention_days})." + report.status_extended = f"VM {vm.resource_name} in subscription {subscription_name} ({subscription}) has insufficient daily backup retention period of {retention_days} days (minimum required: {min_retention_days})." findings.append(report) return findings diff --git a/prowler/providers/azure/services/vm/vm_trusted_launch_enabled/vm_trusted_launch_enabled.py b/prowler/providers/azure/services/vm/vm_trusted_launch_enabled/vm_trusted_launch_enabled.py index d4896b70ec..4a5163c2db 100644 --- a/prowler/providers/azure/services/vm/vm_trusted_launch_enabled/vm_trusted_launch_enabled.py +++ b/prowler/providers/azure/services/vm/vm_trusted_launch_enabled/vm_trusted_launch_enabled.py @@ -6,12 +6,15 @@ class vm_trusted_launch_enabled(Check): def execute(self) -> Check_Report_Azure: findings = [] - for subscription_name, vms in vm_client.virtual_machines.items(): + for subscription_id, vms in vm_client.virtual_machines.items(): + subscription_name = vm_client.subscriptions.get( + subscription_id, subscription_id + ) for vm in vms.values(): report = Check_Report_Azure(metadata=self.metadata(), resource=vm) - report.subscription = subscription_name + report.subscription = subscription_id report.status = "FAIL" - report.status_extended = f"VM {vm.resource_name} has trusted launch disabled in subscription {subscription_name}" + report.status_extended = f"VM {vm.resource_name} has trusted launch disabled in subscription {subscription_name} ({subscription_id})" if ( vm.security_profile @@ -20,7 +23,7 @@ class vm_trusted_launch_enabled(Check): and vm.security_profile.uefi_settings.v_tpm_enabled ): report.status = "PASS" - report.status_extended = f"VM {vm.resource_name} has trusted launch enabled in subscription {subscription_name}" + report.status_extended = f"VM {vm.resource_name} has trusted launch enabled in subscription {subscription_name} ({subscription_id})" findings.append(report) diff --git a/prowler/providers/cloudflare/lib/plan.py b/prowler/providers/cloudflare/lib/plan.py new file mode 100644 index 0000000000..e6fa20d77d --- /dev/null +++ b/prowler/providers/cloudflare/lib/plan.py @@ -0,0 +1,35 @@ +from typing import Optional + +# Cloudflare returns the plan name in ``zone.plan.name`` (e.g. "Free Website", +# "Pro Website", "Business Website", "Enterprise Website"). Free plans do not +# expose WAF managed rulesets at all, while paid plans expose them but the +# legacy ``waf`` zone setting can lag behind the actual deployment state. +PAID_PLAN_KEYWORDS = ("pro", "business", "enterprise") +FREE_PLAN_KEYWORDS = ("free",) + + +def _plan_matches(plan: Optional[str], keywords: tuple[str, ...]) -> bool: + if not isinstance(plan, str): + return False + plan_lower = plan.lower() + return any(keyword in plan_lower for keyword in keywords) + + +def is_paid_plan(plan: Optional[str]) -> bool: + """Return True when the Cloudflare zone plan is a paid tier.""" + return _plan_matches(plan, PAID_PLAN_KEYWORDS) + + +def is_free_plan(plan: Optional[str]) -> bool: + """Return True when the Cloudflare zone plan is the Free tier.""" + return _plan_matches(plan, FREE_PLAN_KEYWORDS) + + +def paid_plan_suffix(plan: Optional[str], message: str) -> str: + """Return an explanatory suffix only when the zone is on a paid plan.""" + return f" {message}" if is_paid_plan(plan) else "" + + +def free_plan_suffix(plan: Optional[str], message: str) -> str: + """Return an explanatory suffix only when the zone is on the Free plan.""" + return f" {message}" if is_free_plan(plan) else "" diff --git a/prowler/providers/cloudflare/services/zone/zone_waf_enabled/zone_waf_enabled.py b/prowler/providers/cloudflare/services/zone/zone_waf_enabled/zone_waf_enabled.py index 64265c3985..4d3a7eed71 100644 --- a/prowler/providers/cloudflare/services/zone/zone_waf_enabled/zone_waf_enabled.py +++ b/prowler/providers/cloudflare/services/zone/zone_waf_enabled/zone_waf_enabled.py @@ -1,6 +1,19 @@ from prowler.lib.check.models import Check, CheckReportCloudflare +from prowler.providers.cloudflare.lib.plan import ( + free_plan_suffix, + paid_plan_suffix, +) from prowler.providers.cloudflare.services.zone.zone_client import zone_client +PAID_PLAN_FALSE_POSITIVE_HINT = ( + "This may be a false positive if WAF managed rulesets are configured via " + "the Cloudflare dashboard; verify manually in Security > WAF." +) +FREE_PLAN_UNAVAILABLE_HINT = ( + "This may be expected because the Web Application Firewall is not " + "available on the Cloudflare Free plan." +) + class zone_waf_enabled(Check): """Ensure that WAF is enabled for Cloudflare zones. @@ -35,6 +48,16 @@ class zone_waf_enabled(Check): report.status_extended = f"WAF is enabled for zone {zone.name}." else: report.status = "FAIL" - report.status_extended = f"WAF is not enabled for zone {zone.name}." + # Two plan-specific hints can be appended to the FAIL message: + # - Paid plans: the legacy ``waf`` zone setting can read ``off`` + # while WAF managed rulesets are deployed via the dashboard, + # so the FAIL may be a false positive. + # - Free plans: WAF is not available at all, so the FAIL is + # expected and the suffix points that out. + report.status_extended = ( + f"WAF is not enabled for zone {zone.name}." + f"{paid_plan_suffix(zone.plan, PAID_PLAN_FALSE_POSITIVE_HINT)}" + f"{free_plan_suffix(zone.plan, FREE_PLAN_UNAVAILABLE_HINT)}" + ) findings.append(report) return findings diff --git a/prowler/providers/common/provider.py b/prowler/providers/common/provider.py index cea9f09112..348e3ee638 100644 --- a/prowler/providers/common/provider.py +++ b/prowler/providers/common/provider.py @@ -557,6 +557,32 @@ class Provider(ABC): mutelist_path=arguments.mutelist_file, fixer_config=fixer_config, ) + + elif "okta" in provider_class_name.lower(): + provider_class( + okta_org_domain=getattr(arguments, "okta_org_domain", ""), + okta_client_id=getattr(arguments, "okta_client_id", ""), + okta_private_key=getattr(arguments, "okta_private_key", ""), + okta_private_key_file=getattr( + arguments, "okta_private_key_file", "" + ), + okta_scopes=getattr(arguments, "okta_scopes", None), + config_path=arguments.config_file, + mutelist_path=arguments.mutelist_file, + fixer_config=fixer_config, + ) + elif "scaleway" in provider_class_name.lower(): + # Credentials are read from the SCW_ACCESS_KEY / + # SCW_SECRET_KEY env vars by the provider itself; there + # are no credential CLI flags to avoid leaking secrets. + provider_class( + organization_id=getattr(arguments, "organization_id", None), + project_id=getattr(arguments, "project_id", None), + region=getattr(arguments, "region", None), + config_path=arguments.config_file, + mutelist_path=arguments.mutelist_file, + fixer_config=fixer_config, + ) else: # Dynamic fallback: any external/custom provider. # Honor the from_cli_args type hint (-> Provider): if the diff --git a/tests/providers/googleworkspace/services/gmail/gmail_untrusted_link_warnings_enabled/__init__.py b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_cmek_encryption_enabled/__init__.py similarity index 100% rename from tests/providers/googleworkspace/services/gmail/gmail_untrusted_link_warnings_enabled/__init__.py rename to prowler/providers/gcp/services/cloudsql/cloudsql_instance_cmek_encryption_enabled/__init__.py diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_cmek_encryption_enabled/cloudsql_instance_cmek_encryption_enabled.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_cmek_encryption_enabled/cloudsql_instance_cmek_encryption_enabled.metadata.json new file mode 100644 index 0000000000..5ec026f9ed --- /dev/null +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_cmek_encryption_enabled/cloudsql_instance_cmek_encryption_enabled.metadata.json @@ -0,0 +1,42 @@ +{ + "Provider": "gcp", + "CheckID": "cloudsql_instance_cmek_encryption_enabled", + "CheckTitle": "Cloud SQL instance is encrypted with a customer-managed key (CMEK)", + "CheckType": [], + "ServiceName": "cloudsql", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "**Cloud SQL instances** use **customer-managed encryption keys** (`CMEK`) via Cloud KMS for at-rest encryption. The evaluation identifies instances lacking a configured **Cloud KMS key**, indicating use of default Google-managed encryption instead.", + "Risk": "Without CMEK, Google holds sole control of the encryption keys. If the organization must demonstrate key custody, meet data residency requirements, or immediately revoke access to data (e.g., upon contract termination), Google-managed keys are insufficient. This may violate ISMS-P 2.7.1 and regulatory requirements for sensitive or personal data.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/sql/docs/mysql/cmek", + "https://cloud.google.com/sql/docs/postgres/cmek", + "https://cloud.google.com/sql/docs/sqlserver/cmek", + "https://cloud.google.com/kms/docs/resource-hierarchy" + ], + "Remediation": { + "Code": { + "CLI": "gcloud sql instances create \\\n --database-version= \\\n --region= \\\n --disk-encryption-key=projects//locations//keyRings//cryptoKeys/", + "NativeIaC": "", + "Other": "CMEK must be configured at instance creation time. To migrate an existing instance:\n1. Create a new Cloud SQL instance with CMEK enabled.\n2. Export data from the existing instance.\n3. Import data into the new CMEK-enabled instance.\n4. Update application connection strings.", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"example\" {\n name = \"\"\n database_version = \"\"\n region = \"\"\n\n encryption_key_name = \"projects//locations//keyRings//cryptoKeys/\"\n\n settings {\n tier = \"db-custom-2-7680\"\n }\n}\n```" + }, + "Recommendation": { + "Text": "For instances storing personal or sensitive data, create new Cloud SQL instances with CMEK using a Cloud KMS key in the same region. Ensure the Cloud SQL service account has the roles/cloudkms.cryptoKeyEncrypterDecrypter role on the key, and enable key rotation per your policy.", + "Url": "https://hub.prowler.com/check/cloudsql_instance_cmek_encryption_enabled" + } + }, + "Categories": [ + "encryption" + ], + "DependsOn": [], + "RelatedTo": [ + "kms_key_rotation_enabled", + "kms_key_not_publicly_accessible", + "bigquery_dataset_cmk_encryption" + ], + "Notes": "CMEK cannot be enabled on an existing Cloud SQL instance; it must be set at creation time. Existing instances require data migration to a new CMEK-enabled instance." +} diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_cmek_encryption_enabled/cloudsql_instance_cmek_encryption_enabled.py b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_cmek_encryption_enabled/cloudsql_instance_cmek_encryption_enabled.py new file mode 100644 index 0000000000..8048ced453 --- /dev/null +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_cmek_encryption_enabled/cloudsql_instance_cmek_encryption_enabled.py @@ -0,0 +1,25 @@ +from prowler.lib.check.models import Check, Check_Report_GCP +from prowler.providers.gcp.services.cloudsql.cloudsql_client import cloudsql_client + + +class cloudsql_instance_cmek_encryption_enabled(Check): + def execute(self) -> Check_Report_GCP: + findings = [] + for instance in cloudsql_client.instances: + if instance.instance_type != "CLOUD_SQL_INSTANCE": + continue + report = Check_Report_GCP(metadata=self.metadata(), resource=instance) + if instance.cmek_key_name: + report.status = "PASS" + report.status_extended = ( + f"Database instance {instance.name} is encrypted with " + f"customer-managed key: {instance.cmek_key_name}." + ) + else: + report.status = "FAIL" + report.status_extended = ( + f"Database instance {instance.name} is not encrypted with a " + f"customer-managed key (CMEK); Google-managed key is in use." + ) + findings.append(report) + return findings diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_service.py b/prowler/providers/gcp/services/cloudsql/cloudsql_service.py index 2d04a4248c..137169999a 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_service.py +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_service.py @@ -1,3 +1,5 @@ +from typing import Optional + from pydantic.v1 import BaseModel from prowler.lib.logger import logger @@ -24,6 +26,8 @@ class CloudSQL(GCPService): for address in instance.get("ipAddresses", []): if address["type"] == "PRIMARY": public_ip = True + settings = instance.get("settings", {}) + ip_config = settings.get("ipConfiguration", {}) self.instances.append( Instance( name=instance["name"], @@ -31,19 +35,23 @@ class CloudSQL(GCPService): region=instance["region"], ip_addresses=instance.get("ipAddresses", []), public_ip=public_ip, - require_ssl=instance["settings"] - .get("ipConfiguration", {}) - .get("requireSsl", False), - ssl_mode=instance["settings"] - .get("ipConfiguration", {}) - .get("sslMode", "ALLOW_UNENCRYPTED_AND_ENCRYPTED"), - automated_backups=instance["settings"] - .get("backupConfiguration", {}) - .get("enabled", False), - authorized_networks=instance["settings"] - .get("ipConfiguration", {}) - .get("authorizedNetworks", []), - flags=instance["settings"].get("databaseFlags", []), + require_ssl=ip_config.get("requireSsl", False), + ssl_mode=ip_config.get( + "sslMode", "ALLOW_UNENCRYPTED_AND_ENCRYPTED" + ), + automated_backups=settings.get( + "backupConfiguration", {} + ).get("enabled", False), + authorized_networks=ip_config.get( + "authorizedNetworks", [] + ), + flags=settings.get("databaseFlags", []), + instance_type=instance.get( + "instanceType", "CLOUD_SQL_INSTANCE" + ), + cmek_key_name=instance.get( + "diskEncryptionConfiguration", {} + ).get("kmsKeyName"), project_id=project_id, ) ) @@ -68,4 +76,6 @@ class Instance(BaseModel): ssl_mode: str automated_backups: bool flags: list + instance_type: str = "CLOUD_SQL_INSTANCE" + cmek_key_name: Optional[str] = None project_id: str diff --git a/prowler/providers/gcp/services/compute/compute_service.py b/prowler/providers/gcp/services/compute/compute_service.py index 0965142766..41cce29a7b 100644 --- a/prowler/providers/gcp/services/compute/compute_service.py +++ b/prowler/providers/gcp/services/compute/compute_service.py @@ -87,9 +87,15 @@ class Compute(GCPService): .execute(num_retries=DEFAULT_RETRY_ATTEMPTS) ) for item in response["commonInstanceMetadata"].get("items", []): - if item["key"] == "enable-oslogin" and item["value"] == "TRUE": + if ( + item["key"] == "enable-oslogin" + and item["value"].lower() == "true" + ): enable_oslogin = True - if item["key"] == "enable-oslogin-2fa" and item["value"] == "TRUE": + if ( + item["key"] == "enable-oslogin-2fa" + and item["value"].lower() == "true" + ): enable_oslogin_2fa = True self.compute_projects.append( Project( diff --git a/prowler/providers/googleworkspace/services/additionalservices/__init__.py b/prowler/providers/googleworkspace/services/additionalservices/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/additionalservices/additionalservices_client.py b/prowler/providers/googleworkspace/services/additionalservices/additionalservices_client.py new file mode 100644 index 0000000000..0d256528fa --- /dev/null +++ b/prowler/providers/googleworkspace/services/additionalservices/additionalservices_client.py @@ -0,0 +1,6 @@ +from prowler.providers.common.provider import Provider +from prowler.providers.googleworkspace.services.additionalservices.additionalservices_service import ( + AdditionalServices, +) + +additionalservices_client = AdditionalServices(Provider.get_global_provider()) diff --git a/prowler/providers/googleworkspace/services/additionalservices/additionalservices_external_groups_disabled/__init__.py b/prowler/providers/googleworkspace/services/additionalservices/additionalservices_external_groups_disabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/additionalservices/additionalservices_external_groups_disabled/additionalservices_external_groups_disabled.metadata.json b/prowler/providers/googleworkspace/services/additionalservices/additionalservices_external_groups_disabled/additionalservices_external_groups_disabled.metadata.json new file mode 100644 index 0000000000..82bb368a8a --- /dev/null +++ b/prowler/providers/googleworkspace/services/additionalservices/additionalservices_external_groups_disabled/additionalservices_external_groups_disabled.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "googleworkspace", + "CheckID": "additionalservices_external_groups_disabled", + "CheckTitle": "Access to external Google Groups is off for everyone", + "CheckType": [], + "ServiceName": "additionalservices", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "collaboration", + "Description": "The Additional Google services configuration **disables access to external Google Groups** for all users. This setting controls whether users can access groups created outside the organization from their Google Workspace account.", + "Risk": "When external Google Groups access is enabled, users can access and participate in groups created **outside the organization**, potentially exposing them to **phishing, social engineering, or data leakage** through unmanaged external group communications.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://support.google.com/a/answer/181865", + "https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Additional Google services**\n3. Scroll down to **Google Groups**\n4. Set it to **OFF for everyone**\n5. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Disable access to **external Google Groups** for all users. If specific users require access to external groups, enable it by exception for those users or groups only.", + "Url": "https://hub.prowler.com/check/additionalservices_external_groups_disabled" + } + }, + "Categories": [ + "trust-boundaries" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "This check covers the 'Additional Google services > Google Groups' toggle, which is distinct from the 'Groups for Business' sharing settings covered by the groups service checks." +} diff --git a/prowler/providers/googleworkspace/services/additionalservices/additionalservices_external_groups_disabled/additionalservices_external_groups_disabled.py b/prowler/providers/googleworkspace/services/additionalservices/additionalservices_external_groups_disabled/additionalservices_external_groups_disabled.py new file mode 100644 index 0000000000..cf61220054 --- /dev/null +++ b/prowler/providers/googleworkspace/services/additionalservices/additionalservices_external_groups_disabled/additionalservices_external_groups_disabled.py @@ -0,0 +1,55 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.additionalservices.additionalservices_client import ( + additionalservices_client, +) + + +class additionalservices_external_groups_disabled(Check): + """Check that access to external Google Groups is disabled for all users. + + This check verifies that the domain-level Additional Google services policy + disables external Google Groups access, preventing users from accessing + groups created outside the organization. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if additionalservices_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=additionalservices_client.policies, + resource_id="additionalServicesPolicies", + resource_name="Additional Services Policies", + customer_id=additionalservices_client.provider.identity.customer_id, + ) + + groups_state = additionalservices_client.policies.groups_service_state + + if groups_state == "DISABLED": + report.status = "PASS" + report.status_extended = ( + f"Access to external Google Groups is disabled " + f"in domain {additionalservices_client.provider.identity.domain}." + ) + else: + report.status = "FAIL" + if groups_state is None: + report.status_extended = ( + f"Access to external Google Groups is not explicitly configured " + f"in domain {additionalservices_client.provider.identity.domain}. " + f"The default is ON for everyone. " + f"External Google Groups access should be disabled." + ) + else: + report.status_extended = ( + f"Access to external Google Groups is enabled " + f"in domain {additionalservices_client.provider.identity.domain}. " + f"External Google Groups access should be disabled." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/additionalservices/additionalservices_service.py b/prowler/providers/googleworkspace/services/additionalservices/additionalservices_service.py new file mode 100644 index 0000000000..5a41060bc8 --- /dev/null +++ b/prowler/providers/googleworkspace/services/additionalservices/additionalservices_service.py @@ -0,0 +1,92 @@ +from typing import Optional + +from pydantic import BaseModel + +from prowler.lib.logger import logger +from prowler.providers.googleworkspace.lib.service.service import GoogleWorkspaceService + + +class AdditionalServices(GoogleWorkspaceService): + """Google Workspace Additional Services for auditing domain-level service toggles. + + Uses the Cloud Identity Policy API v1 to read the service status of + additional Google services configured in the Admin Console, such as + the external Google Groups access toggle. + """ + + def __init__(self, provider): + super().__init__(provider) + self.policies = AdditionalServicesPolicies() + self.policies_fetched = False + self._fetch_additional_services_policies() + + def _fetch_additional_services_policies(self): + """Fetch Additional Services policies from the Cloud Identity Policy API v1.""" + logger.info("Additional Services - Fetching policies...") + + try: + service = self._build_service("cloudidentity", "v1") + + if not service: + logger.error("Failed to build Cloud Identity service") + return + + request = service.policies().list( + pageSize=100, + filter='setting.type.matches("groups.service_status")', + ) + fetch_succeeded = True + + while request is not None: + try: + response = request.execute() + + for policy in response.get("policies", []): + if not self._is_customer_level_policy(policy): + continue + + setting = policy.get("setting", {}) + setting_type = setting.get("type", "").removeprefix("settings/") + value = setting.get("value", {}) + + if setting_type == "groups.service_status": + self.policies.groups_service_state = value.get( + "serviceState" + ) + logger.debug( + "Additional Services - Groups service state: " + f"{self.policies.groups_service_state}" + ) + + request = service.policies().list_next(request, response) + + except Exception as error: + self._handle_api_error( + error, + "fetching Additional Services policies", + self.provider.identity.customer_id, + ) + fetch_succeeded = False + break + + self.policies_fetched = fetch_succeeded + + logger.info( + f"Additional Services policies fetched - " + f"Groups service state: {self.policies.groups_service_state}" + ) + + except Exception as error: + self._handle_api_error( + error, + "fetching Additional Services policies", + self.provider.identity.customer_id, + ) + self.policies_fetched = False + + +class AdditionalServicesPolicies(BaseModel): + """Model for domain-level Additional Google Services policy settings.""" + + # groups.service_status + groups_service_state: Optional[str] = None diff --git a/prowler/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/calendar_external_invitations_warning.py b/prowler/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/calendar_external_invitations_warning.py index da65a162ab..f2c8f58b2f 100644 --- a/prowler/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/calendar_external_invitations_warning.py +++ b/prowler/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/calendar_external_invitations_warning.py @@ -20,7 +20,10 @@ class calendar_external_invitations_warning(Check): if calendar_client.policies_fetched: report = CheckReportGoogleWorkspace( metadata=self.metadata(), - resource=calendar_client.provider.domain_resource, + resource=calendar_client.policies, + resource_id="calendarPolicies", + resource_name="Calendar Policies", + customer_id=calendar_client.provider.identity.customer_id, ) warning_enabled = calendar_client.policies.external_invitations_warning diff --git a/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/calendar_external_sharing_primary_calendar.py b/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/calendar_external_sharing_primary_calendar.py index 935b28bd02..42be5e9f0d 100644 --- a/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/calendar_external_sharing_primary_calendar.py +++ b/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/calendar_external_sharing_primary_calendar.py @@ -20,7 +20,10 @@ class calendar_external_sharing_primary_calendar(Check): if calendar_client.policies_fetched: report = CheckReportGoogleWorkspace( metadata=self.metadata(), - resource=calendar_client.provider.domain_resource, + resource=calendar_client.policies, + resource_id="calendarPolicies", + resource_name="Calendar Policies", + customer_id=calendar_client.provider.identity.customer_id, ) sharing = calendar_client.policies.primary_calendar_external_sharing diff --git a/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_secondary_calendar/calendar_external_sharing_secondary_calendar.py b/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_secondary_calendar/calendar_external_sharing_secondary_calendar.py index 6834b60ae7..5f53fd2089 100644 --- a/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_secondary_calendar/calendar_external_sharing_secondary_calendar.py +++ b/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_secondary_calendar/calendar_external_sharing_secondary_calendar.py @@ -20,7 +20,10 @@ class calendar_external_sharing_secondary_calendar(Check): if calendar_client.policies_fetched: report = CheckReportGoogleWorkspace( metadata=self.metadata(), - resource=calendar_client.provider.domain_resource, + resource=calendar_client.policies, + resource_id="calendarPolicies", + resource_name="Calendar Policies", + customer_id=calendar_client.provider.identity.customer_id, ) sharing = calendar_client.policies.secondary_calendar_external_sharing diff --git a/prowler/providers/googleworkspace/services/calendar/calendar_service.py b/prowler/providers/googleworkspace/services/calendar/calendar_service.py index f976b654c0..aa822d4218 100644 --- a/prowler/providers/googleworkspace/services/calendar/calendar_service.py +++ b/prowler/providers/googleworkspace/services/calendar/calendar_service.py @@ -30,7 +30,10 @@ class Calendar(GoogleWorkspaceService): logger.error("Failed to build Cloud Identity service") return - request = service.policies().list(pageSize=100) + request = service.policies().list( + pageSize=100, + filter='setting.type.matches("calendar.*")', + ) fetch_succeeded = True while request is not None: diff --git a/prowler/providers/googleworkspace/services/chat/__init__.py b/prowler/providers/googleworkspace/services/chat/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/chat/chat_apps_installation_disabled/__init__.py b/prowler/providers/googleworkspace/services/chat/chat_apps_installation_disabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/chat/chat_apps_installation_disabled/chat_apps_installation_disabled.metadata.json b/prowler/providers/googleworkspace/services/chat/chat_apps_installation_disabled/chat_apps_installation_disabled.metadata.json new file mode 100644 index 0000000000..54f4d6d852 --- /dev/null +++ b/prowler/providers/googleworkspace/services/chat/chat_apps_installation_disabled/chat_apps_installation_disabled.metadata.json @@ -0,0 +1,39 @@ +{ + "Provider": "googleworkspace", + "CheckID": "chat_apps_installation_disabled", + "CheckTitle": "Chat apps installation is disabled for users", + "CheckType": [], + "ServiceName": "chat", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "collaboration", + "Description": "Google Chat apps connect to external services to look up information, schedule meetings, or complete tasks. Apps are accounts created by Google, users in the organization, or third parties that can access user data including **email addresses**, **conversation content**, and **organizational information**.", + "Risk": "Unrestricted Chat app installation allows **unvetted third-party applications** to access user data including conversation content and organizational information. An attacker could distribute a malicious Chat app to **exfiltrate confidential data** or establish **persistent access** to internal communications.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://support.google.com/a/answer/6089179", + "https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Google Chat and classic Hangouts**\n3. Click **Chat apps**\n4. Under Chat apps access settings, set **Allow users to install Chat apps** to **OFF**\n5. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Disable Chat apps installation to prevent **unvetted third-party applications** from accessing organizational data through the Chat platform.", + "Url": "https://hub.prowler.com/check/chat_apps_installation_disabled" + } + }, + "Categories": [ + "trust-boundaries" + ], + "DependsOn": [], + "RelatedTo": [ + "chat_incoming_webhooks_disabled" + ], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/chat/chat_apps_installation_disabled/chat_apps_installation_disabled.py b/prowler/providers/googleworkspace/services/chat/chat_apps_installation_disabled/chat_apps_installation_disabled.py new file mode 100644 index 0000000000..c80be7e6dc --- /dev/null +++ b/prowler/providers/googleworkspace/services/chat/chat_apps_installation_disabled/chat_apps_installation_disabled.py @@ -0,0 +1,52 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.chat.chat_client import chat_client + + +class chat_apps_installation_disabled(Check): + """Check that users cannot install Chat apps. + + This check verifies that the domain-level Chat policy prevents users + from installing Chat apps, reducing the risk of data exposure through + third-party or unvetted applications. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if chat_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=chat_client.policies, + resource_id="chatPolicies", + resource_name="Chat Policies", + customer_id=chat_client.provider.identity.customer_id, + ) + + apps_enabled = chat_client.policies.enable_apps + + if apps_enabled is False: + report.status = "PASS" + report.status_extended = ( + f"Chat apps installation is disabled " + f"in domain {chat_client.provider.identity.domain}." + ) + elif apps_enabled is None: + report.status = "PASS" + report.status_extended = ( + f"Chat apps installation uses Google's secure default " + f"configuration (disabled) " + f"in domain {chat_client.provider.identity.domain}." + ) + else: + report.status = "FAIL" + report.status_extended = ( + f"Chat apps installation is enabled " + f"in domain {chat_client.provider.identity.domain}. " + f"Chat apps installation should be disabled to prevent unvetted apps." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/chat/chat_client.py b/prowler/providers/googleworkspace/services/chat/chat_client.py new file mode 100644 index 0000000000..b8a12ecdcc --- /dev/null +++ b/prowler/providers/googleworkspace/services/chat/chat_client.py @@ -0,0 +1,4 @@ +from prowler.providers.common.provider import Provider +from prowler.providers.googleworkspace.services.chat.chat_service import Chat + +chat_client = Chat(Provider.get_global_provider()) diff --git a/prowler/providers/googleworkspace/services/chat/chat_external_file_sharing_disabled/__init__.py b/prowler/providers/googleworkspace/services/chat/chat_external_file_sharing_disabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/chat/chat_external_file_sharing_disabled/chat_external_file_sharing_disabled.metadata.json b/prowler/providers/googleworkspace/services/chat/chat_external_file_sharing_disabled/chat_external_file_sharing_disabled.metadata.json new file mode 100644 index 0000000000..c2d8323994 --- /dev/null +++ b/prowler/providers/googleworkspace/services/chat/chat_external_file_sharing_disabled/chat_external_file_sharing_disabled.metadata.json @@ -0,0 +1,40 @@ +{ + "Provider": "googleworkspace", + "CheckID": "chat_external_file_sharing_disabled", + "CheckTitle": "External file sharing in Chat is set to no files", + "CheckType": [], + "ServiceName": "chat", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "collaboration", + "Description": "Google Chat **external file sharing** controls whether users can share files with people outside the organization via Chat conversations. Files often contain **confidential information**, and organizations in regulated industries need to control the flow of this information outside their boundaries.", + "Risk": "Enabled external file sharing allows users to send files containing **confidential information** to external parties through Chat. This creates a **data leakage** channel that bypasses DLP controls, particularly dangerous for organizations handling **regulated data** such as PII, PHI, or financial records.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://support.google.com/a/answer/9540647", + "https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Google Chat and classic Hangouts**\n3. Click **Chat File Sharing**\n4. Under Setting, set **External filesharing** to **No files**\n5. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Disable **external file sharing** in Chat to prevent users from sharing files with people outside the organization through Chat conversations.", + "Url": "https://hub.prowler.com/check/chat_external_file_sharing_disabled" + } + }, + "Categories": [ + "trust-boundaries" + ], + "DependsOn": [], + "RelatedTo": [ + "chat_internal_file_sharing_disabled", + "drive_sharing_allowlisted_domains" + ], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/chat/chat_external_file_sharing_disabled/chat_external_file_sharing_disabled.py b/prowler/providers/googleworkspace/services/chat/chat_external_file_sharing_disabled/chat_external_file_sharing_disabled.py new file mode 100644 index 0000000000..7c17b314e2 --- /dev/null +++ b/prowler/providers/googleworkspace/services/chat/chat_external_file_sharing_disabled/chat_external_file_sharing_disabled.py @@ -0,0 +1,52 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.chat.chat_client import chat_client + + +class chat_external_file_sharing_disabled(Check): + """Check that external file sharing in Google Chat is disabled. + + This check verifies that the domain-level Chat policy prevents users + from sharing files with people outside the organization via Chat, + protecting sensitive information from unauthorized external access. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if chat_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=chat_client.policies, + resource_id="chatPolicies", + resource_name="Chat Policies", + customer_id=chat_client.provider.identity.customer_id, + ) + + external_sharing = chat_client.policies.external_file_sharing + + if external_sharing == "NO_FILES": + report.status = "PASS" + report.status_extended = ( + f"External file sharing in Chat is disabled " + f"in domain {chat_client.provider.identity.domain}." + ) + else: + report.status = "FAIL" + if external_sharing is None: + report.status_extended = ( + f"External file sharing in Chat is not explicitly configured " + f"in domain {chat_client.provider.identity.domain}. " + f"External file sharing should be set to No files." + ) + else: + report.status_extended = ( + f"External file sharing in Chat is set to {external_sharing} " + f"in domain {chat_client.provider.identity.domain}. " + f"External file sharing should be set to No files." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/chat/chat_external_messaging_restricted/__init__.py b/prowler/providers/googleworkspace/services/chat/chat_external_messaging_restricted/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/chat/chat_external_messaging_restricted/chat_external_messaging_restricted.metadata.json b/prowler/providers/googleworkspace/services/chat/chat_external_messaging_restricted/chat_external_messaging_restricted.metadata.json new file mode 100644 index 0000000000..0c366d7689 --- /dev/null +++ b/prowler/providers/googleworkspace/services/chat/chat_external_messaging_restricted/chat_external_messaging_restricted.metadata.json @@ -0,0 +1,40 @@ +{ + "Provider": "googleworkspace", + "CheckID": "chat_external_messaging_restricted", + "CheckTitle": "External Chat messaging is restricted to allowed domains", + "CheckType": [], + "ServiceName": "chat", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "collaboration", + "Description": "Google Chat **external messaging** controls whether users can send messages to people outside the organization. If external messaging is allowed, it can optionally be restricted to only **allowlisted domains** to limit the scope of external communication.", + "Risk": "Unrestricted external messaging allows users to communicate freely with **any external party**, increasing the risk of **data exfiltration** through conversation content and **social engineering attacks** from untrusted domains targeting internal users.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://support.google.com/a/answer/9540647", + "https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Google Chat and classic Hangouts**\n3. Click **External Chat Settings**\n4. Select **Chat externally**\n5. Set **Allow users to send messages outside the organization** to **ON**\n6. Check **Only allow this for allowlisted domains**\n7. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Restrict **external Chat messaging** to **allowlisted domains** only to limit information flow to trusted parties and reduce exposure to external threats.", + "Url": "https://hub.prowler.com/check/chat_external_messaging_restricted" + } + }, + "Categories": [ + "trust-boundaries" + ], + "DependsOn": [], + "RelatedTo": [ + "chat_external_spaces_restricted", + "drive_sharing_allowlisted_domains" + ], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/chat/chat_external_messaging_restricted/chat_external_messaging_restricted.py b/prowler/providers/googleworkspace/services/chat/chat_external_messaging_restricted/chat_external_messaging_restricted.py new file mode 100644 index 0000000000..aef75403b9 --- /dev/null +++ b/prowler/providers/googleworkspace/services/chat/chat_external_messaging_restricted/chat_external_messaging_restricted.py @@ -0,0 +1,59 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.chat.chat_client import chat_client + + +class chat_external_messaging_restricted(Check): + """Check that external Chat messaging is restricted to allowed domains. + + This check verifies that external Chat messaging is either disabled + entirely or restricted to allowlisted domains only, preventing + unrestricted communication with external users. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if chat_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=chat_client.policies, + resource_id="chatPolicies", + resource_name="Chat Policies", + customer_id=chat_client.provider.identity.customer_id, + ) + + allow_external = chat_client.policies.allow_external_chat + restriction = chat_client.policies.external_chat_restriction + + if allow_external is False: + report.status = "PASS" + report.status_extended = ( + f"External Chat messaging is disabled " + f"in domain {chat_client.provider.identity.domain}." + ) + elif allow_external is None and restriction is None: + report.status = "PASS" + report.status_extended = ( + f"External Chat messaging uses Google's secure default " + f"configuration (disabled) " + f"in domain {chat_client.provider.identity.domain}." + ) + elif restriction == "TRUSTED_DOMAINS": + report.status = "PASS" + report.status_extended = ( + f"External Chat messaging is restricted to allowed domains " + f"in domain {chat_client.provider.identity.domain}." + ) + else: + report.status = "FAIL" + report.status_extended = ( + f"External Chat messaging is not restricted to allowed domains " + f"in domain {chat_client.provider.identity.domain}. " + f"External messaging should be restricted to allowed domains only." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/chat/chat_external_spaces_restricted/__init__.py b/prowler/providers/googleworkspace/services/chat/chat_external_spaces_restricted/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/chat/chat_external_spaces_restricted/chat_external_spaces_restricted.metadata.json b/prowler/providers/googleworkspace/services/chat/chat_external_spaces_restricted/chat_external_spaces_restricted.metadata.json new file mode 100644 index 0000000000..9f40730d9f --- /dev/null +++ b/prowler/providers/googleworkspace/services/chat/chat_external_spaces_restricted/chat_external_spaces_restricted.metadata.json @@ -0,0 +1,40 @@ +{ + "Provider": "googleworkspace", + "CheckID": "chat_external_spaces_restricted", + "CheckTitle": "External spaces in Chat are restricted to allowed domains", + "CheckType": [], + "ServiceName": "chat", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "collaboration", + "Description": "Google Chat **external spaces** allow users to create or join collaborative spaces that include people outside the organization. If external spaces are allowed, they can optionally be restricted to only **allowlisted domains** to limit external participation.", + "Risk": "Unrestricted external spaces allow users to add **anyone from any domain** to persistent group conversations. This increases the risk of **confidential information exposure** in shared spaces and enables **unauthorized external access** to ongoing organizational discussions.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://support.google.com/a/answer/9540647", + "https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Google Chat and classic Hangouts**\n3. Click **External Spaces**\n4. Set **Allow users to create and join spaces with people outside their organization** to **ON**\n5. Check **Only allow users to add people from allowlisted domains**\n6. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Restrict **external spaces** to **allowlisted domains** only to control which external parties can participate in organizational Chat spaces.", + "Url": "https://hub.prowler.com/check/chat_external_spaces_restricted" + } + }, + "Categories": [ + "trust-boundaries" + ], + "DependsOn": [], + "RelatedTo": [ + "chat_external_messaging_restricted", + "drive_sharing_allowlisted_domains" + ], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/chat/chat_external_spaces_restricted/chat_external_spaces_restricted.py b/prowler/providers/googleworkspace/services/chat/chat_external_spaces_restricted/chat_external_spaces_restricted.py new file mode 100644 index 0000000000..d6ad421bda --- /dev/null +++ b/prowler/providers/googleworkspace/services/chat/chat_external_spaces_restricted/chat_external_spaces_restricted.py @@ -0,0 +1,59 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.chat.chat_client import chat_client + + +class chat_external_spaces_restricted(Check): + """Check that external spaces in Google Chat are restricted. + + This check verifies that external spaces are either disabled entirely + or restricted to allowlisted domains only, preventing users from + creating or joining spaces with unrestricted external participants. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if chat_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=chat_client.policies, + resource_id="chatPolicies", + resource_name="Chat Policies", + customer_id=chat_client.provider.identity.customer_id, + ) + + spaces_enabled = chat_client.policies.external_spaces_enabled + allowlist_mode = chat_client.policies.external_spaces_domain_allowlist_mode + + if spaces_enabled is False: + report.status = "PASS" + report.status_extended = ( + f"External spaces are disabled " + f"in domain {chat_client.provider.identity.domain}." + ) + elif allowlist_mode == "TRUSTED_DOMAINS": + report.status = "PASS" + report.status_extended = ( + f"External spaces are restricted to allowed domains " + f"in domain {chat_client.provider.identity.domain}." + ) + else: + report.status = "FAIL" + if spaces_enabled is None and allowlist_mode is None: + report.status_extended = ( + f"External spaces restriction is not explicitly configured " + f"in domain {chat_client.provider.identity.domain}. " + f"External spaces should be restricted to allowed domains only." + ) + else: + report.status_extended = ( + f"External spaces are not restricted to allowed domains " + f"in domain {chat_client.provider.identity.domain}. " + f"External spaces should be restricted to allowed domains only." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/chat/chat_incoming_webhooks_disabled/__init__.py b/prowler/providers/googleworkspace/services/chat/chat_incoming_webhooks_disabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/chat/chat_incoming_webhooks_disabled/chat_incoming_webhooks_disabled.metadata.json b/prowler/providers/googleworkspace/services/chat/chat_incoming_webhooks_disabled/chat_incoming_webhooks_disabled.metadata.json new file mode 100644 index 0000000000..7dec1c83e4 --- /dev/null +++ b/prowler/providers/googleworkspace/services/chat/chat_incoming_webhooks_disabled/chat_incoming_webhooks_disabled.metadata.json @@ -0,0 +1,39 @@ +{ + "Provider": "googleworkspace", + "CheckID": "chat_incoming_webhooks_disabled", + "CheckTitle": "Incoming webhooks in Chat are disabled for users", + "CheckType": [], + "ServiceName": "chat", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "collaboration", + "Description": "**Incoming webhooks** let external applications post asynchronous messages into Google Chat spaces without being a Chat app. When enabled, users can configure webhooks and developers can call them to send content from **external applications**.", + "Risk": "Exposed webhook URLs allow **unauthorized content injection** into Chat spaces. Attackers can send **fraudulent or misleading messages** that appear to come from trusted services, creating a vector for **social engineering** and **phishing** within internal communications.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://support.google.com/a/answer/6089179", + "https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Google Chat and classic Hangouts**\n3. Click **Chat apps**\n4. Under Chat apps access settings, set **Allow users to add and use incoming webhooks** to **OFF**\n5. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Disable **incoming webhooks** to prevent unauthenticated external applications from **injecting content** into internal Chat spaces.", + "Url": "https://hub.prowler.com/check/chat_incoming_webhooks_disabled" + } + }, + "Categories": [ + "trust-boundaries" + ], + "DependsOn": [], + "RelatedTo": [ + "chat_apps_installation_disabled" + ], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/chat/chat_incoming_webhooks_disabled/chat_incoming_webhooks_disabled.py b/prowler/providers/googleworkspace/services/chat/chat_incoming_webhooks_disabled/chat_incoming_webhooks_disabled.py new file mode 100644 index 0000000000..753daf02da --- /dev/null +++ b/prowler/providers/googleworkspace/services/chat/chat_incoming_webhooks_disabled/chat_incoming_webhooks_disabled.py @@ -0,0 +1,52 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.chat.chat_client import chat_client + + +class chat_incoming_webhooks_disabled(Check): + """Check that incoming webhooks are disabled in Google Chat. + + This check verifies that the domain-level Chat policy prevents users + from adding and using incoming webhooks, reducing the risk of + unauthorized content being posted into Chat spaces. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if chat_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=chat_client.policies, + resource_id="chatPolicies", + resource_name="Chat Policies", + customer_id=chat_client.provider.identity.customer_id, + ) + + webhooks_enabled = chat_client.policies.enable_webhooks + + if webhooks_enabled is False: + report.status = "PASS" + report.status_extended = ( + f"Incoming webhooks are disabled " + f"in domain {chat_client.provider.identity.domain}." + ) + elif webhooks_enabled is None: + report.status = "PASS" + report.status_extended = ( + f"Incoming webhooks use Google's secure default " + f"configuration (disabled) " + f"in domain {chat_client.provider.identity.domain}." + ) + else: + report.status = "FAIL" + report.status_extended = ( + f"Incoming webhooks are enabled " + f"in domain {chat_client.provider.identity.domain}. " + f"Incoming webhooks should be disabled to prevent unauthorized content." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/chat/chat_internal_file_sharing_disabled/__init__.py b/prowler/providers/googleworkspace/services/chat/chat_internal_file_sharing_disabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/chat/chat_internal_file_sharing_disabled/chat_internal_file_sharing_disabled.metadata.json b/prowler/providers/googleworkspace/services/chat/chat_internal_file_sharing_disabled/chat_internal_file_sharing_disabled.metadata.json new file mode 100644 index 0000000000..af2130d741 --- /dev/null +++ b/prowler/providers/googleworkspace/services/chat/chat_internal_file_sharing_disabled/chat_internal_file_sharing_disabled.metadata.json @@ -0,0 +1,39 @@ +{ + "Provider": "googleworkspace", + "CheckID": "chat_internal_file_sharing_disabled", + "CheckTitle": "Internal file sharing in Chat is set to no files", + "CheckType": [], + "ServiceName": "chat", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "low", + "ResourceType": "NotDefined", + "ResourceGroup": "collaboration", + "Description": "Google Chat **internal file sharing** controls whether users can share files with other people inside the organization via Chat conversations. Organizations in regulated industries may need to **control and audit** all file sharing, even between internal users.", + "Risk": "Unrestricted internal file sharing in Chat allows files with **sensitive information** to be distributed freely without passing through approved channels. This undermines **data governance** and **audit trail** requirements, making it harder to track data movement within the organization.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://support.google.com/a/answer/9540647", + "https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Google Chat and classic Hangouts**\n3. Click **Chat File Sharing**\n4. Under Setting, set **Internal filesharing** to **No files**\n5. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Disable **internal file sharing** in Chat to enforce file distribution through **approved channels** with proper audit trails and governance controls.", + "Url": "https://hub.prowler.com/check/chat_internal_file_sharing_disabled" + } + }, + "Categories": [ + "trust-boundaries" + ], + "DependsOn": [], + "RelatedTo": [ + "chat_external_file_sharing_disabled" + ], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/chat/chat_internal_file_sharing_disabled/chat_internal_file_sharing_disabled.py b/prowler/providers/googleworkspace/services/chat/chat_internal_file_sharing_disabled/chat_internal_file_sharing_disabled.py new file mode 100644 index 0000000000..614750ecda --- /dev/null +++ b/prowler/providers/googleworkspace/services/chat/chat_internal_file_sharing_disabled/chat_internal_file_sharing_disabled.py @@ -0,0 +1,52 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.chat.chat_client import chat_client + + +class chat_internal_file_sharing_disabled(Check): + """Check that internal file sharing in Google Chat is disabled. + + This check verifies that the domain-level Chat policy prevents users + from sharing files internally via Chat, providing maximum control over + file distribution within the organization. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if chat_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=chat_client.policies, + resource_id="chatPolicies", + resource_name="Chat Policies", + customer_id=chat_client.provider.identity.customer_id, + ) + + internal_sharing = chat_client.policies.internal_file_sharing + + if internal_sharing == "NO_FILES": + report.status = "PASS" + report.status_extended = ( + f"Internal file sharing in Chat is disabled " + f"in domain {chat_client.provider.identity.domain}." + ) + else: + report.status = "FAIL" + if internal_sharing is None: + report.status_extended = ( + f"Internal file sharing in Chat is not explicitly configured " + f"in domain {chat_client.provider.identity.domain}. " + f"Internal file sharing should be set to No files." + ) + else: + report.status_extended = ( + f"Internal file sharing in Chat is set to {internal_sharing} " + f"in domain {chat_client.provider.identity.domain}. " + f"Internal file sharing should be set to No files." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/chat/chat_service.py b/prowler/providers/googleworkspace/services/chat/chat_service.py new file mode 100644 index 0000000000..92d4d77dfa --- /dev/null +++ b/prowler/providers/googleworkspace/services/chat/chat_service.py @@ -0,0 +1,125 @@ +from typing import Optional + +from pydantic import BaseModel + +from prowler.lib.logger import logger +from prowler.providers.googleworkspace.lib.service.service import GoogleWorkspaceService + + +class Chat(GoogleWorkspaceService): + """Google Workspace Chat service for auditing domain-level Chat policies. + + Uses the Cloud Identity Policy API v1 to read Chat file sharing, external + messaging, spaces, and apps access settings configured in the Admin Console. + """ + + def __init__(self, provider): + super().__init__(provider) + self.policies = ChatPolicies() + self.policies_fetched = False + self._fetch_chat_policies() + + def _fetch_chat_policies(self): + """Fetch Chat policies from the Cloud Identity Policy API v1.""" + logger.info("Chat - Fetching Chat policies...") + + try: + service = self._build_service("cloudidentity", "v1") + + if not service: + logger.error("Failed to build Cloud Identity service") + return + + request = service.policies().list( + pageSize=100, + filter='setting.type.matches("chat.*")', + ) + fetch_succeeded = True + + while request is not None: + try: + response = request.execute() + + for policy in response.get("policies", []): + if not self._is_customer_level_policy(policy): + continue + + setting = policy.get("setting", {}) + setting_type = setting.get("type", "").removeprefix("settings/") + logger.debug(f"Processing setting type: {setting_type}") + + value = setting.get("value", {}) + + if setting_type == "chat.chat_file_sharing": + self.policies.external_file_sharing = value.get( + "externalFileSharing" + ) + self.policies.internal_file_sharing = value.get( + "internalFileSharing" + ) + logger.debug("Chat file sharing settings fetched.") + + elif setting_type == "chat.external_chat_restriction": + self.policies.allow_external_chat = value.get( + "allowExternalChat" + ) + self.policies.external_chat_restriction = value.get( + "externalChatRestriction" + ) + logger.debug( + "Chat external chat restriction settings fetched." + ) + + elif setting_type == "chat.chat_external_spaces": + self.policies.external_spaces_enabled = value.get("enabled") + self.policies.external_spaces_domain_allowlist_mode = ( + value.get("domainAllowlistMode") + ) + logger.debug("Chat external spaces settings fetched.") + + elif setting_type == "chat.chat_apps_access": + self.policies.enable_apps = value.get("enableApps") + self.policies.enable_webhooks = value.get("enableWebhooks") + logger.debug("Chat apps access settings fetched.") + + request = service.policies().list_next(request, response) + + except Exception as error: + self._handle_api_error( + error, + "fetching Chat policies", + self.provider.identity.customer_id, + ) + fetch_succeeded = False + break + + self.policies_fetched = fetch_succeeded + logger.info("Chat policies fetched successfully.") + + except Exception as error: + self._handle_api_error( + error, + "fetching Chat policies", + self.provider.identity.customer_id, + ) + self.policies_fetched = False + + +class ChatPolicies(BaseModel): + """Model for domain-level Chat policy settings.""" + + # chat.chat_file_sharing + external_file_sharing: Optional[str] = None + internal_file_sharing: Optional[str] = None + + # chat.external_chat_restriction + allow_external_chat: Optional[bool] = None + external_chat_restriction: Optional[str] = None + + # chat.chat_external_spaces + external_spaces_enabled: Optional[bool] = None + external_spaces_domain_allowlist_mode: Optional[str] = None + + # chat.chat_apps_access + enable_apps: Optional[bool] = None + enable_webhooks: Optional[bool] = None diff --git a/prowler/providers/googleworkspace/services/directory/directory_super_admin_count/directory_super_admin_count.py b/prowler/providers/googleworkspace/services/directory/directory_super_admin_count/directory_super_admin_count.py index 73d7313a76..ee857bae24 100644 --- a/prowler/providers/googleworkspace/services/directory/directory_super_admin_count/directory_super_admin_count.py +++ b/prowler/providers/googleworkspace/services/directory/directory_super_admin_count/directory_super_admin_count.py @@ -24,6 +24,9 @@ class directory_super_admin_count(Check): report = CheckReportGoogleWorkspace( metadata=self.metadata(), resource=directory_client.provider.domain_resource, + resource_id="directoryUsers", + resource_name="Directory Users", + customer_id=directory_client.provider.identity.customer_id, ) if 2 <= admin_count <= 4: diff --git a/prowler/providers/googleworkspace/services/directory/directory_super_admin_only_admin_roles/directory_super_admin_only_admin_roles.py b/prowler/providers/googleworkspace/services/directory/directory_super_admin_only_admin_roles/directory_super_admin_only_admin_roles.py index cc2cb47e98..d267eb3140 100644 --- a/prowler/providers/googleworkspace/services/directory/directory_super_admin_only_admin_roles/directory_super_admin_only_admin_roles.py +++ b/prowler/providers/googleworkspace/services/directory/directory_super_admin_only_admin_roles/directory_super_admin_only_admin_roles.py @@ -47,6 +47,9 @@ class directory_super_admin_only_admin_roles(Check): report = CheckReportGoogleWorkspace( metadata=self.metadata(), resource=directory_client.provider.domain_resource, + resource_id="directoryUsers", + resource_name="Directory Users", + customer_id=directory_client.provider.identity.customer_id, ) report.status = "PASS" report.status_extended = ( diff --git a/prowler/providers/googleworkspace/services/drive/drive_access_checker_recipients_only/drive_access_checker_recipients_only.py b/prowler/providers/googleworkspace/services/drive/drive_access_checker_recipients_only/drive_access_checker_recipients_only.py index be4ee08654..435e9c0f7d 100644 --- a/prowler/providers/googleworkspace/services/drive/drive_access_checker_recipients_only/drive_access_checker_recipients_only.py +++ b/prowler/providers/googleworkspace/services/drive/drive_access_checker_recipients_only/drive_access_checker_recipients_only.py @@ -19,7 +19,10 @@ class drive_access_checker_recipients_only(Check): if drive_client.policies_fetched: report = CheckReportGoogleWorkspace( metadata=self.metadata(), - resource=drive_client.provider.domain_resource, + resource=drive_client.policies, + resource_id="drivePolicies", + resource_name="Drive Policies", + customer_id=drive_client.provider.identity.customer_id, ) access_checker = drive_client.policies.access_checker_suggestions diff --git a/prowler/providers/googleworkspace/services/drive/drive_desktop_access_disabled/drive_desktop_access_disabled.py b/prowler/providers/googleworkspace/services/drive/drive_desktop_access_disabled/drive_desktop_access_disabled.py index de69f180ea..189694adaf 100644 --- a/prowler/providers/googleworkspace/services/drive/drive_desktop_access_disabled/drive_desktop_access_disabled.py +++ b/prowler/providers/googleworkspace/services/drive/drive_desktop_access_disabled/drive_desktop_access_disabled.py @@ -20,7 +20,10 @@ class drive_desktop_access_disabled(Check): if drive_client.policies_fetched: report = CheckReportGoogleWorkspace( metadata=self.metadata(), - resource=drive_client.provider.domain_resource, + resource=drive_client.policies, + resource_id="drivePolicies", + resource_name="Drive Policies", + customer_id=drive_client.provider.identity.customer_id, ) allow_desktop = drive_client.policies.allow_drive_for_desktop diff --git a/prowler/providers/googleworkspace/services/drive/drive_external_sharing_warn_users/drive_external_sharing_warn_users.py b/prowler/providers/googleworkspace/services/drive/drive_external_sharing_warn_users/drive_external_sharing_warn_users.py index e82da80e44..98da6de892 100644 --- a/prowler/providers/googleworkspace/services/drive/drive_external_sharing_warn_users/drive_external_sharing_warn_users.py +++ b/prowler/providers/googleworkspace/services/drive/drive_external_sharing_warn_users/drive_external_sharing_warn_users.py @@ -18,7 +18,10 @@ class drive_external_sharing_warn_users(Check): if drive_client.policies_fetched: report = CheckReportGoogleWorkspace( metadata=self.metadata(), - resource=drive_client.provider.domain_resource, + resource=drive_client.policies, + resource_id="drivePolicies", + resource_name="Drive Policies", + customer_id=drive_client.provider.identity.customer_id, ) warning_enabled = drive_client.policies.warn_for_external_sharing diff --git a/prowler/providers/googleworkspace/services/drive/drive_internal_users_distribute_content/drive_internal_users_distribute_content.py b/prowler/providers/googleworkspace/services/drive/drive_internal_users_distribute_content/drive_internal_users_distribute_content.py index ef65d6ad49..cdf9f1c642 100644 --- a/prowler/providers/googleworkspace/services/drive/drive_internal_users_distribute_content/drive_internal_users_distribute_content.py +++ b/prowler/providers/googleworkspace/services/drive/drive_internal_users_distribute_content/drive_internal_users_distribute_content.py @@ -19,7 +19,10 @@ class drive_internal_users_distribute_content(Check): if drive_client.policies_fetched: report = CheckReportGoogleWorkspace( metadata=self.metadata(), - resource=drive_client.provider.domain_resource, + resource=drive_client.policies, + resource_id="drivePolicies", + resource_name="Drive Policies", + customer_id=drive_client.provider.identity.customer_id, ) allowed = drive_client.policies.allowed_parties_for_distributing_content diff --git a/prowler/providers/googleworkspace/services/drive/drive_publishing_files_disabled/drive_publishing_files_disabled.py b/prowler/providers/googleworkspace/services/drive/drive_publishing_files_disabled/drive_publishing_files_disabled.py index e377113a33..397f1eee68 100644 --- a/prowler/providers/googleworkspace/services/drive/drive_publishing_files_disabled/drive_publishing_files_disabled.py +++ b/prowler/providers/googleworkspace/services/drive/drive_publishing_files_disabled/drive_publishing_files_disabled.py @@ -19,7 +19,10 @@ class drive_publishing_files_disabled(Check): if drive_client.policies_fetched: report = CheckReportGoogleWorkspace( metadata=self.metadata(), - resource=drive_client.provider.domain_resource, + resource=drive_client.policies, + resource_id="drivePolicies", + resource_name="Drive Policies", + customer_id=drive_client.provider.identity.customer_id, ) allow_publishing = drive_client.policies.allow_publishing_files diff --git a/prowler/providers/googleworkspace/services/drive/drive_service.py b/prowler/providers/googleworkspace/services/drive/drive_service.py index e7d89f8473..68c4b48453 100644 --- a/prowler/providers/googleworkspace/services/drive/drive_service.py +++ b/prowler/providers/googleworkspace/services/drive/drive_service.py @@ -31,7 +31,10 @@ class Drive(GoogleWorkspaceService): logger.error("Failed to build Cloud Identity service") return - request = service.policies().list(pageSize=100) + request = service.policies().list( + pageSize=100, + filter='setting.type.matches("drive_and_docs.*")', + ) fetch_succeeded = True while request is not None: diff --git a/prowler/providers/googleworkspace/services/drive/drive_shared_drive_creation_allowed/drive_shared_drive_creation_allowed.py b/prowler/providers/googleworkspace/services/drive/drive_shared_drive_creation_allowed/drive_shared_drive_creation_allowed.py index e82b995418..e381ad8f26 100644 --- a/prowler/providers/googleworkspace/services/drive/drive_shared_drive_creation_allowed/drive_shared_drive_creation_allowed.py +++ b/prowler/providers/googleworkspace/services/drive/drive_shared_drive_creation_allowed/drive_shared_drive_creation_allowed.py @@ -20,7 +20,10 @@ class drive_shared_drive_creation_allowed(Check): if drive_client.policies_fetched: report = CheckReportGoogleWorkspace( metadata=self.metadata(), - resource=drive_client.provider.domain_resource, + resource=drive_client.policies, + resource_id="drivePolicies", + resource_name="Drive Policies", + customer_id=drive_client.provider.identity.customer_id, ) allow_creation = drive_client.policies.allow_shared_drive_creation diff --git a/prowler/providers/googleworkspace/services/drive/drive_shared_drive_disable_download_print_copy/drive_shared_drive_disable_download_print_copy.py b/prowler/providers/googleworkspace/services/drive/drive_shared_drive_disable_download_print_copy/drive_shared_drive_disable_download_print_copy.py index a065fc08d0..85972f3674 100644 --- a/prowler/providers/googleworkspace/services/drive/drive_shared_drive_disable_download_print_copy/drive_shared_drive_disable_download_print_copy.py +++ b/prowler/providers/googleworkspace/services/drive/drive_shared_drive_disable_download_print_copy/drive_shared_drive_disable_download_print_copy.py @@ -19,7 +19,10 @@ class drive_shared_drive_disable_download_print_copy(Check): if drive_client.policies_fetched: report = CheckReportGoogleWorkspace( metadata=self.metadata(), - resource=drive_client.provider.domain_resource, + resource=drive_client.policies, + resource_id="drivePolicies", + resource_name="Drive Policies", + customer_id=drive_client.provider.identity.customer_id, ) allowed = drive_client.policies.allowed_parties_for_download_print_copy diff --git a/prowler/providers/googleworkspace/services/drive/drive_shared_drive_managers_cannot_override/drive_shared_drive_managers_cannot_override.py b/prowler/providers/googleworkspace/services/drive/drive_shared_drive_managers_cannot_override/drive_shared_drive_managers_cannot_override.py index 72d01ea73b..b6f6d1b887 100644 --- a/prowler/providers/googleworkspace/services/drive/drive_shared_drive_managers_cannot_override/drive_shared_drive_managers_cannot_override.py +++ b/prowler/providers/googleworkspace/services/drive/drive_shared_drive_managers_cannot_override/drive_shared_drive_managers_cannot_override.py @@ -19,7 +19,10 @@ class drive_shared_drive_managers_cannot_override(Check): if drive_client.policies_fetched: report = CheckReportGoogleWorkspace( metadata=self.metadata(), - resource=drive_client.provider.domain_resource, + resource=drive_client.policies, + resource_id="drivePolicies", + resource_name="Drive Policies", + customer_id=drive_client.provider.identity.customer_id, ) allow_override = drive_client.policies.allow_managers_to_override_settings diff --git a/prowler/providers/googleworkspace/services/drive/drive_shared_drive_members_only_access/drive_shared_drive_members_only_access.py b/prowler/providers/googleworkspace/services/drive/drive_shared_drive_members_only_access/drive_shared_drive_members_only_access.py index 2056fdb2d2..34cde3f6c6 100644 --- a/prowler/providers/googleworkspace/services/drive/drive_shared_drive_members_only_access/drive_shared_drive_members_only_access.py +++ b/prowler/providers/googleworkspace/services/drive/drive_shared_drive_members_only_access/drive_shared_drive_members_only_access.py @@ -19,7 +19,10 @@ class drive_shared_drive_members_only_access(Check): if drive_client.policies_fetched: report = CheckReportGoogleWorkspace( metadata=self.metadata(), - resource=drive_client.provider.domain_resource, + resource=drive_client.policies, + resource_id="drivePolicies", + resource_name="Drive Policies", + customer_id=drive_client.provider.identity.customer_id, ) allow_non_member = drive_client.policies.allow_non_member_access diff --git a/prowler/providers/googleworkspace/services/drive/drive_sharing_allowlisted_domains/drive_sharing_allowlisted_domains.py b/prowler/providers/googleworkspace/services/drive/drive_sharing_allowlisted_domains/drive_sharing_allowlisted_domains.py index 7a3d77bfce..766afecda5 100644 --- a/prowler/providers/googleworkspace/services/drive/drive_sharing_allowlisted_domains/drive_sharing_allowlisted_domains.py +++ b/prowler/providers/googleworkspace/services/drive/drive_sharing_allowlisted_domains/drive_sharing_allowlisted_domains.py @@ -18,7 +18,10 @@ class drive_sharing_allowlisted_domains(Check): if drive_client.policies_fetched: report = CheckReportGoogleWorkspace( metadata=self.metadata(), - resource=drive_client.provider.domain_resource, + resource=drive_client.policies, + resource_id="drivePolicies", + resource_name="Drive Policies", + customer_id=drive_client.provider.identity.customer_id, ) mode = drive_client.policies.external_sharing_mode diff --git a/prowler/providers/googleworkspace/services/drive/drive_warn_sharing_with_allowlisted_domains/drive_warn_sharing_with_allowlisted_domains.py b/prowler/providers/googleworkspace/services/drive/drive_warn_sharing_with_allowlisted_domains/drive_warn_sharing_with_allowlisted_domains.py index 549c089881..23fd47a229 100644 --- a/prowler/providers/googleworkspace/services/drive/drive_warn_sharing_with_allowlisted_domains/drive_warn_sharing_with_allowlisted_domains.py +++ b/prowler/providers/googleworkspace/services/drive/drive_warn_sharing_with_allowlisted_domains/drive_warn_sharing_with_allowlisted_domains.py @@ -19,7 +19,10 @@ class drive_warn_sharing_with_allowlisted_domains(Check): if drive_client.policies_fetched: report = CheckReportGoogleWorkspace( metadata=self.metadata(), - resource=drive_client.provider.domain_resource, + resource=drive_client.policies, + resource_id="drivePolicies", + resource_name="Drive Policies", + customer_id=drive_client.provider.identity.customer_id, ) warn_enabled = ( diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_anomalous_attachment_protection_enabled/__init__.py b/prowler/providers/googleworkspace/services/gmail/gmail_anomalous_attachment_protection_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_anomalous_attachment_protection_enabled/gmail_anomalous_attachment_protection_enabled.metadata.json b/prowler/providers/googleworkspace/services/gmail/gmail_anomalous_attachment_protection_enabled/gmail_anomalous_attachment_protection_enabled.metadata.json new file mode 100644 index 0000000000..77a1c68b38 --- /dev/null +++ b/prowler/providers/googleworkspace/services/gmail/gmail_anomalous_attachment_protection_enabled/gmail_anomalous_attachment_protection_enabled.metadata.json @@ -0,0 +1,40 @@ +{ + "Provider": "googleworkspace", + "CheckID": "gmail_anomalous_attachment_protection_enabled", + "CheckTitle": "Protection against anomalous attachment types in emails is enabled", + "CheckType": [], + "ServiceName": "gmail", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "collaboration", + "Description": "Verifies that Gmail is configured to take a protective action (such as moving to spam, quarantining, or showing a warning) when emails contain anomalous attachment types. Unusual file types that are uncommon for the sender or organization may indicate an attempt to deliver malware through less-scrutinized formats.", + "Risk": "Without protection against anomalous attachment types, users may receive **emails with unusual file formats** that are designed to bypass standard security filters. Attackers may use **uncommon file extensions or MIME types** to deliver malware that evades signature-based detection.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://support.google.com/a/answer/7676854", + "https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Gmail**\n3. Click **Safety** > **Attachments**\n4. Check **Protect against anomalous attachment types in emails**\n5. Select the desired action (e.g., Move email to spam)\n6. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable protection against anomalous attachment types in emails and configure an appropriate action such as moving to spam or quarantining.", + "Url": "https://hub.prowler.com/check/gmail_anomalous_attachment_protection_enabled" + } + }, + "Categories": [ + "email-security" + ], + "DependsOn": [], + "RelatedTo": [ + "gmail_encrypted_attachment_protection_enabled", + "gmail_script_attachment_protection_enabled" + ], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_anomalous_attachment_protection_enabled/gmail_anomalous_attachment_protection_enabled.py b/prowler/providers/googleworkspace/services/gmail/gmail_anomalous_attachment_protection_enabled/gmail_anomalous_attachment_protection_enabled.py new file mode 100644 index 0000000000..b8895d4e21 --- /dev/null +++ b/prowler/providers/googleworkspace/services/gmail/gmail_anomalous_attachment_protection_enabled/gmail_anomalous_attachment_protection_enabled.py @@ -0,0 +1,74 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.gmail.gmail_client import gmail_client + + +class gmail_anomalous_attachment_protection_enabled(Check): + """Check that protection against anomalous attachment types in emails is enabled. + + This check verifies that Gmail is configured to take action on + emails containing unusual attachment types, helping prevent + malware delivery via uncommon file formats. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if gmail_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=gmail_client.policies, + resource_id="gmailPolicies", + resource_name="Gmail Policies", + customer_id=gmail_client.provider.identity.customer_id, + ) + + enabled = gmail_client.policies.enable_anomalous_attachment_protection + consequence = ( + gmail_client.policies.anomalous_attachment_protection_consequence + ) + + if enabled is False: + report.status = "FAIL" + report.status_extended = ( + f"Protection against anomalous attachment types in emails " + f"is disabled in domain " + f"{gmail_client.provider.identity.domain}. " + f"Enable the protection and configure a protective action." + ) + elif enabled is None: + report.status = "FAIL" + report.status_extended = ( + f"Protection against anomalous attachment types in emails " + f"is not configured and uses Google's insecure default " + f"(disabled) in domain " + f"{gmail_client.provider.identity.domain}. " + f"Enable the protection and configure a protective action." + ) + elif consequence == "NO_ACTION": + report.status = "FAIL" + report.status_extended = ( + f"Protection against anomalous attachment types in emails " + f"is set to take no action in domain " + f"{gmail_client.provider.identity.domain}. " + f"A protective action should be configured." + ) + elif consequence is None: + report.status = "PASS" + report.status_extended = ( + f"Protection against anomalous attachment types in emails " + f"is enabled in domain " + f"{gmail_client.provider.identity.domain}." + ) + else: + report.status = "PASS" + report.status_extended = ( + f"Protection against anomalous attachment types in emails " + f"is enabled with consequence '{consequence}' " + f"in domain {gmail_client.provider.identity.domain}." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_auto_forwarding_disabled/gmail_auto_forwarding_disabled.py b/prowler/providers/googleworkspace/services/gmail/gmail_auto_forwarding_disabled/gmail_auto_forwarding_disabled.py index 9930089eb7..65cd6f1fcb 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_auto_forwarding_disabled/gmail_auto_forwarding_disabled.py +++ b/prowler/providers/googleworkspace/services/gmail/gmail_auto_forwarding_disabled/gmail_auto_forwarding_disabled.py @@ -18,7 +18,10 @@ class gmail_auto_forwarding_disabled(Check): if gmail_client.policies_fetched: report = CheckReportGoogleWorkspace( metadata=self.metadata(), - resource=gmail_client.provider.domain_resource, + resource=gmail_client.policies, + resource_id="gmailPolicies", + resource_name="Gmail Policies", + customer_id=gmail_client.provider.identity.customer_id, ) forwarding_enabled = gmail_client.policies.enable_auto_forwarding diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_comprehensive_mail_storage_enabled/gmail_comprehensive_mail_storage_enabled.py b/prowler/providers/googleworkspace/services/gmail/gmail_comprehensive_mail_storage_enabled/gmail_comprehensive_mail_storage_enabled.py index dd0b61760a..704e5a391a 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_comprehensive_mail_storage_enabled/gmail_comprehensive_mail_storage_enabled.py +++ b/prowler/providers/googleworkspace/services/gmail/gmail_comprehensive_mail_storage_enabled/gmail_comprehensive_mail_storage_enabled.py @@ -18,7 +18,10 @@ class gmail_comprehensive_mail_storage_enabled(Check): if gmail_client.policies_fetched: report = CheckReportGoogleWorkspace( metadata=self.metadata(), - resource=gmail_client.provider.domain_resource, + resource=gmail_client.policies, + resource_id="gmailPolicies", + resource_name="Gmail Policies", + customer_id=gmail_client.provider.identity.customer_id, ) storage_enabled = gmail_client.policies.comprehensive_mail_storage_enabled diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_domain_spoofing_protection_enabled/__init__.py b/prowler/providers/googleworkspace/services/gmail/gmail_domain_spoofing_protection_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_domain_spoofing_protection_enabled/gmail_domain_spoofing_protection_enabled.metadata.json b/prowler/providers/googleworkspace/services/gmail/gmail_domain_spoofing_protection_enabled/gmail_domain_spoofing_protection_enabled.metadata.json new file mode 100644 index 0000000000..4ae8bfb3c0 --- /dev/null +++ b/prowler/providers/googleworkspace/services/gmail/gmail_domain_spoofing_protection_enabled/gmail_domain_spoofing_protection_enabled.metadata.json @@ -0,0 +1,42 @@ +{ + "Provider": "googleworkspace", + "CheckID": "gmail_domain_spoofing_protection_enabled", + "CheckTitle": "Protection against domain spoofing based on similar domain names is enabled", + "CheckType": [], + "ServiceName": "gmail", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "collaboration", + "Description": "Verifies that Gmail is configured to take a protective action (such as moving to spam, quarantining, or showing a warning) when emails appear to come from domain names that look similar to the organization's domain. Lookalike domains are a common phishing technique used to trick users into trusting malicious messages.", + "Risk": "Without protection against domain spoofing based on similar domain names, users may receive **phishing emails from lookalike domains** (e.g., examp1e.com instead of example.com) that appear legitimate. This enables **credential theft, malware delivery, and business email compromise** attacks.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://support.google.com/a/answer/9157861", + "https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Gmail**\n3. Click **Safety** > **Spoofing and authentication**\n4. Check **Protect against domain spoofing based on similar domain names**\n5. Select the desired action (e.g., Move email to spam)\n6. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable protection against domain spoofing based on similar domain names and configure an appropriate action such as moving to spam or quarantining.", + "Url": "https://hub.prowler.com/check/gmail_domain_spoofing_protection_enabled" + } + }, + "Categories": [ + "email-security" + ], + "DependsOn": [], + "RelatedTo": [ + "gmail_employee_name_spoofing_protection_enabled", + "gmail_inbound_domain_spoofing_protection_enabled", + "gmail_unauthenticated_email_protection_enabled", + "gmail_groups_spoofing_protection_enabled" + ], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_domain_spoofing_protection_enabled/gmail_domain_spoofing_protection_enabled.py b/prowler/providers/googleworkspace/services/gmail/gmail_domain_spoofing_protection_enabled/gmail_domain_spoofing_protection_enabled.py new file mode 100644 index 0000000000..d3a6bc94c1 --- /dev/null +++ b/prowler/providers/googleworkspace/services/gmail/gmail_domain_spoofing_protection_enabled/gmail_domain_spoofing_protection_enabled.py @@ -0,0 +1,65 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.gmail.gmail_client import gmail_client + + +class gmail_domain_spoofing_protection_enabled(Check): + """Check that protection against domain spoofing based on similar domain names is enabled. + + This check verifies that Gmail is configured to take action on + emails that appear to come from similar-looking domain names, + helping prevent phishing via domain impersonation. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if gmail_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=gmail_client.policies, + resource_id="gmailPolicies", + resource_name="Gmail Policies", + customer_id=gmail_client.provider.identity.customer_id, + ) + + enabled = gmail_client.policies.detect_domain_name_spoofing + consequence = gmail_client.policies.domain_spoofing_consequence + + if enabled is False: + report.status = "FAIL" + report.status_extended = ( + f"Protection against domain spoofing based on similar " + f"domain names is disabled in domain " + f"{gmail_client.provider.identity.domain}. " + f"Enable the protection and configure a protective action." + ) + elif consequence == "NO_ACTION": + report.status = "FAIL" + report.status_extended = ( + f"Protection against domain spoofing based on similar " + f"domain names is set to take no action in domain " + f"{gmail_client.provider.identity.domain}. " + f"A protective action should be configured." + ) + elif consequence is None: + report.status = "PASS" + report.status_extended = ( + f"Protection against domain spoofing based on similar " + f"domain names uses Google's secure default configuration " + f"(enabled) in domain " + f"{gmail_client.provider.identity.domain}." + ) + else: + report.status = "PASS" + report.status_extended = ( + f"Protection against domain spoofing based on similar " + f"domain names is enabled with consequence " + f"'{consequence}' in domain " + f"{gmail_client.provider.identity.domain}." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_employee_name_spoofing_protection_enabled/__init__.py b/prowler/providers/googleworkspace/services/gmail/gmail_employee_name_spoofing_protection_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_employee_name_spoofing_protection_enabled/gmail_employee_name_spoofing_protection_enabled.metadata.json b/prowler/providers/googleworkspace/services/gmail/gmail_employee_name_spoofing_protection_enabled/gmail_employee_name_spoofing_protection_enabled.metadata.json new file mode 100644 index 0000000000..b72137019a --- /dev/null +++ b/prowler/providers/googleworkspace/services/gmail/gmail_employee_name_spoofing_protection_enabled/gmail_employee_name_spoofing_protection_enabled.metadata.json @@ -0,0 +1,42 @@ +{ + "Provider": "googleworkspace", + "CheckID": "gmail_employee_name_spoofing_protection_enabled", + "CheckTitle": "Protection against spoofing of employee names is enabled", + "CheckType": [], + "ServiceName": "gmail", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "collaboration", + "Description": "Verifies that Gmail is configured to take a protective action (such as moving to spam, quarantining, or showing a warning) when the sender's display name matches an employee's name but the email comes from an external address. This is a common social engineering technique where attackers impersonate colleagues or executives.", + "Risk": "Without protection against employee name spoofing, users may receive **emails that appear to come from colleagues or executives** but are actually from external attackers. This enables **business email compromise (BEC)**, **wire fraud**, and **social engineering attacks** that exploit trust relationships.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://support.google.com/a/answer/9157861", + "https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Gmail**\n3. Click **Safety** > **Spoofing and authentication**\n4. Check **Protect against spoofing of employee names**\n5. Select the desired action (e.g., Move email to spam)\n6. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable protection against spoofing of employee names and configure an appropriate action such as moving to spam or quarantining.", + "Url": "https://hub.prowler.com/check/gmail_employee_name_spoofing_protection_enabled" + } + }, + "Categories": [ + "email-security" + ], + "DependsOn": [], + "RelatedTo": [ + "gmail_domain_spoofing_protection_enabled", + "gmail_inbound_domain_spoofing_protection_enabled", + "gmail_unauthenticated_email_protection_enabled", + "gmail_groups_spoofing_protection_enabled" + ], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_employee_name_spoofing_protection_enabled/gmail_employee_name_spoofing_protection_enabled.py b/prowler/providers/googleworkspace/services/gmail/gmail_employee_name_spoofing_protection_enabled/gmail_employee_name_spoofing_protection_enabled.py new file mode 100644 index 0000000000..ea6283c940 --- /dev/null +++ b/prowler/providers/googleworkspace/services/gmail/gmail_employee_name_spoofing_protection_enabled/gmail_employee_name_spoofing_protection_enabled.py @@ -0,0 +1,63 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.gmail.gmail_client import gmail_client + + +class gmail_employee_name_spoofing_protection_enabled(Check): + """Check that protection against spoofing of employee names is enabled. + + This check verifies that Gmail is configured to take action on + emails where the sender name matches an employee name but comes + from an external address, helping prevent social engineering attacks. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if gmail_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=gmail_client.policies, + resource_id="gmailPolicies", + resource_name="Gmail Policies", + customer_id=gmail_client.provider.identity.customer_id, + ) + + enabled = gmail_client.policies.detect_employee_name_spoofing + consequence = gmail_client.policies.employee_name_spoofing_consequence + + if enabled is False: + report.status = "FAIL" + report.status_extended = ( + f"Protection against spoofing of employee names is " + f"disabled in domain " + f"{gmail_client.provider.identity.domain}. " + f"Enable the protection and configure a protective action." + ) + elif consequence == "NO_ACTION": + report.status = "FAIL" + report.status_extended = ( + f"Protection against spoofing of employee names is set " + f"to take no action in domain " + f"{gmail_client.provider.identity.domain}. " + f"A protective action should be configured." + ) + elif consequence is None: + report.status = "PASS" + report.status_extended = ( + f"Protection against spoofing of employee names uses " + f"Google's secure default configuration (enabled) " + f"in domain {gmail_client.provider.identity.domain}." + ) + else: + report.status = "PASS" + report.status_extended = ( + f"Protection against spoofing of employee names is " + f"enabled with consequence '{consequence}' in domain " + f"{gmail_client.provider.identity.domain}." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_encrypted_attachment_protection_enabled/__init__.py b/prowler/providers/googleworkspace/services/gmail/gmail_encrypted_attachment_protection_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_encrypted_attachment_protection_enabled/gmail_encrypted_attachment_protection_enabled.metadata.json b/prowler/providers/googleworkspace/services/gmail/gmail_encrypted_attachment_protection_enabled/gmail_encrypted_attachment_protection_enabled.metadata.json new file mode 100644 index 0000000000..459ce1f539 --- /dev/null +++ b/prowler/providers/googleworkspace/services/gmail/gmail_encrypted_attachment_protection_enabled/gmail_encrypted_attachment_protection_enabled.metadata.json @@ -0,0 +1,40 @@ +{ + "Provider": "googleworkspace", + "CheckID": "gmail_encrypted_attachment_protection_enabled", + "CheckTitle": "Protection against encrypted attachments from untrusted senders is enabled", + "CheckType": [], + "ServiceName": "gmail", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "collaboration", + "Description": "Verifies that Gmail is configured to take a protective action (such as moving to spam, quarantining, or showing a warning) when an encrypted attachment is received from an untrusted sender. Encrypted attachments cannot be scanned for malware by security filters, making them a common vector for delivering malicious payloads.", + "Risk": "Without protection against encrypted attachments from untrusted senders, users may receive **password-protected archives containing malware** that bypass standard content scanning. Attackers commonly use encrypted attachments to evade detection and deliver **ransomware, trojans, or other malicious payloads**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://support.google.com/a/answer/7676854", + "https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Gmail**\n3. Click **Safety** > **Attachments**\n4. Check **Protect against encrypted attachments from untrusted senders**\n5. Select the desired action (e.g., Move email to spam)\n6. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable protection against encrypted attachments from untrusted senders and configure an appropriate action such as moving to spam or quarantining.", + "Url": "https://hub.prowler.com/check/gmail_encrypted_attachment_protection_enabled" + } + }, + "Categories": [ + "email-security" + ], + "DependsOn": [], + "RelatedTo": [ + "gmail_script_attachment_protection_enabled", + "gmail_anomalous_attachment_protection_enabled" + ], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_encrypted_attachment_protection_enabled/gmail_encrypted_attachment_protection_enabled.py b/prowler/providers/googleworkspace/services/gmail/gmail_encrypted_attachment_protection_enabled/gmail_encrypted_attachment_protection_enabled.py new file mode 100644 index 0000000000..30a58cb4b4 --- /dev/null +++ b/prowler/providers/googleworkspace/services/gmail/gmail_encrypted_attachment_protection_enabled/gmail_encrypted_attachment_protection_enabled.py @@ -0,0 +1,66 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.gmail.gmail_client import gmail_client + + +class gmail_encrypted_attachment_protection_enabled(Check): + """Check that protection against encrypted attachments from untrusted senders is enabled. + + This check verifies that Gmail is configured to take action on + encrypted attachments from untrusted senders, helping prevent + malware delivery via password-protected archives. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if gmail_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=gmail_client.policies, + resource_id="gmailPolicies", + resource_name="Gmail Policies", + customer_id=gmail_client.provider.identity.customer_id, + ) + + enabled = gmail_client.policies.enable_encrypted_attachment_protection + consequence = ( + gmail_client.policies.encrypted_attachment_protection_consequence + ) + + if enabled is False: + report.status = "FAIL" + report.status_extended = ( + f"Protection against encrypted attachments from untrusted " + f"senders is disabled in domain " + f"{gmail_client.provider.identity.domain}. " + f"Enable the protection and configure a protective action." + ) + elif consequence == "NO_ACTION": + report.status = "FAIL" + report.status_extended = ( + f"Protection against encrypted attachments from untrusted " + f"senders is set to take no action in domain " + f"{gmail_client.provider.identity.domain}. " + f"A protective action should be configured." + ) + elif consequence is None: + report.status = "PASS" + report.status_extended = ( + f"Protection against encrypted attachments from untrusted " + f"senders uses Google's secure default configuration " + f"(enabled) in domain " + f"{gmail_client.provider.identity.domain}." + ) + else: + report.status = "PASS" + report.status_extended = ( + f"Protection against encrypted attachments from untrusted " + f"senders is enabled with consequence '{consequence}' " + f"in domain {gmail_client.provider.identity.domain}." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_enhanced_pre_delivery_scanning_enabled/gmail_enhanced_pre_delivery_scanning_enabled.py b/prowler/providers/googleworkspace/services/gmail/gmail_enhanced_pre_delivery_scanning_enabled/gmail_enhanced_pre_delivery_scanning_enabled.py index ab27fd837f..33e5a58f9b 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_enhanced_pre_delivery_scanning_enabled/gmail_enhanced_pre_delivery_scanning_enabled.py +++ b/prowler/providers/googleworkspace/services/gmail/gmail_enhanced_pre_delivery_scanning_enabled/gmail_enhanced_pre_delivery_scanning_enabled.py @@ -18,7 +18,10 @@ class gmail_enhanced_pre_delivery_scanning_enabled(Check): if gmail_client.policies_fetched: report = CheckReportGoogleWorkspace( metadata=self.metadata(), - resource=gmail_client.provider.domain_resource, + resource=gmail_client.policies, + resource_id="gmailPolicies", + resource_name="Gmail Policies", + customer_id=gmail_client.provider.identity.customer_id, ) scanning_enabled = ( diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_external_image_scanning_enabled/gmail_external_image_scanning_enabled.py b/prowler/providers/googleworkspace/services/gmail/gmail_external_image_scanning_enabled/gmail_external_image_scanning_enabled.py index b9731fac42..c71de1459a 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_external_image_scanning_enabled/gmail_external_image_scanning_enabled.py +++ b/prowler/providers/googleworkspace/services/gmail/gmail_external_image_scanning_enabled/gmail_external_image_scanning_enabled.py @@ -18,7 +18,10 @@ class gmail_external_image_scanning_enabled(Check): if gmail_client.policies_fetched: report = CheckReportGoogleWorkspace( metadata=self.metadata(), - resource=gmail_client.provider.domain_resource, + resource=gmail_client.policies, + resource_id="gmailPolicies", + resource_name="Gmail Policies", + customer_id=gmail_client.provider.identity.customer_id, ) scanning_enabled = gmail_client.policies.enable_external_image_scanning diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_groups_spoofing_protection_enabled/__init__.py b/prowler/providers/googleworkspace/services/gmail/gmail_groups_spoofing_protection_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_groups_spoofing_protection_enabled/gmail_groups_spoofing_protection_enabled.metadata.json b/prowler/providers/googleworkspace/services/gmail/gmail_groups_spoofing_protection_enabled/gmail_groups_spoofing_protection_enabled.metadata.json new file mode 100644 index 0000000000..0c3dd64761 --- /dev/null +++ b/prowler/providers/googleworkspace/services/gmail/gmail_groups_spoofing_protection_enabled/gmail_groups_spoofing_protection_enabled.metadata.json @@ -0,0 +1,42 @@ +{ + "Provider": "googleworkspace", + "CheckID": "gmail_groups_spoofing_protection_enabled", + "CheckTitle": "Groups are protected from inbound emails spoofing your domain", + "CheckType": [], + "ServiceName": "gmail", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "collaboration", + "Description": "Verifies that Gmail is configured to take a protective action (such as moving to spam, quarantining, or showing a warning) when groups receive inbound emails that spoof the organization's domain. Google Groups are a high-value target because a single spoofed message can reach many recipients at once.", + "Risk": "Without protection of groups from domain-spoofing emails, attackers can send **spoofed messages to group mailboxes** that appear to originate from the organization. Since groups distribute to many recipients, a single spoofed email can enable **mass phishing, social engineering, or misinformation** campaigns across the organization.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://support.google.com/a/answer/9157861", + "https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Gmail**\n3. Click **Safety** > **Spoofing and authentication**\n4. Check **Protect your Groups from inbound emails spoofing your domain**\n5. Select the desired action (e.g., Move email to spam)\n6. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable protection of groups from inbound emails spoofing your domain and configure an appropriate action such as moving to spam or quarantining.", + "Url": "https://hub.prowler.com/check/gmail_groups_spoofing_protection_enabled" + } + }, + "Categories": [ + "email-security" + ], + "DependsOn": [], + "RelatedTo": [ + "gmail_domain_spoofing_protection_enabled", + "gmail_employee_name_spoofing_protection_enabled", + "gmail_inbound_domain_spoofing_protection_enabled", + "gmail_unauthenticated_email_protection_enabled" + ], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_groups_spoofing_protection_enabled/gmail_groups_spoofing_protection_enabled.py b/prowler/providers/googleworkspace/services/gmail/gmail_groups_spoofing_protection_enabled/gmail_groups_spoofing_protection_enabled.py new file mode 100644 index 0000000000..fd4238239f --- /dev/null +++ b/prowler/providers/googleworkspace/services/gmail/gmail_groups_spoofing_protection_enabled/gmail_groups_spoofing_protection_enabled.py @@ -0,0 +1,84 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.gmail.gmail_client import gmail_client + + +class gmail_groups_spoofing_protection_enabled(Check): + """Check that groups are protected from inbound emails spoofing your domain. + + This check verifies that Gmail is configured to take action on + inbound emails to groups that spoof the organization's domain, + helping prevent impersonation attacks targeting group mailboxes. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if gmail_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=gmail_client.policies, + resource_id="gmailPolicies", + resource_name="Gmail Policies", + customer_id=gmail_client.provider.identity.customer_id, + ) + + enabled = gmail_client.policies.detect_groups_spoofing + consequence = gmail_client.policies.groups_spoofing_consequence + visibility_type = gmail_client.policies.groups_spoofing_visibility_type + + if enabled is False: + report.status = "FAIL" + report.status_extended = ( + f"Protection of groups from inbound emails spoofing your " + f"domain is disabled in domain " + f"{gmail_client.provider.identity.domain}. " + f"Enable the protection and configure a protective action." + ) + elif enabled is None: + report.status = "FAIL" + report.status_extended = ( + f"Protection of groups from inbound emails spoofing your " + f"domain is not configured and uses Google's insecure " + f"default (disabled) in domain " + f"{gmail_client.provider.identity.domain}. " + f"Enable the protection and configure a protective action." + ) + elif consequence == "NO_ACTION": + report.status = "FAIL" + report.status_extended = ( + f"Protection of groups from inbound emails spoofing your " + f"domain is set to take no action in domain " + f"{gmail_client.provider.identity.domain}. " + f"A protective action should be configured." + ) + elif consequence is None: + report.status = "PASS" + scope = ( + "private groups only" + if visibility_type == "PRIVATE_GROUPS_ONLY" + else "all groups" + ) + report.status_extended = ( + f"Protection of groups from inbound emails spoofing your " + f"domain is enabled for {scope} in domain " + f"{gmail_client.provider.identity.domain}." + ) + else: + report.status = "PASS" + scope = ( + "private groups only" + if visibility_type == "PRIVATE_GROUPS_ONLY" + else "all groups" + ) + report.status_extended = ( + f"Protection of groups from inbound emails spoofing your " + f"domain is enabled for {scope} with consequence " + f"'{consequence}' in domain " + f"{gmail_client.provider.identity.domain}." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_inbound_domain_spoofing_protection_enabled/__init__.py b/prowler/providers/googleworkspace/services/gmail/gmail_inbound_domain_spoofing_protection_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_inbound_domain_spoofing_protection_enabled/gmail_inbound_domain_spoofing_protection_enabled.metadata.json b/prowler/providers/googleworkspace/services/gmail/gmail_inbound_domain_spoofing_protection_enabled/gmail_inbound_domain_spoofing_protection_enabled.metadata.json new file mode 100644 index 0000000000..9e285621c8 --- /dev/null +++ b/prowler/providers/googleworkspace/services/gmail/gmail_inbound_domain_spoofing_protection_enabled/gmail_inbound_domain_spoofing_protection_enabled.metadata.json @@ -0,0 +1,42 @@ +{ + "Provider": "googleworkspace", + "CheckID": "gmail_inbound_domain_spoofing_protection_enabled", + "CheckTitle": "Protection against inbound emails spoofing your domain is enabled", + "CheckType": [], + "ServiceName": "gmail", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "collaboration", + "Description": "Verifies that Gmail is configured to take a protective action (such as moving to spam, quarantining, or showing a warning) when inbound emails spoof the organization's own domain. This protects against attackers sending emails that appear to originate from within the organization but are actually external.", + "Risk": "Without protection against inbound domain spoofing, users may receive **emails that appear to come from their own organization** but are sent by external attackers. This enables **internal impersonation**, **phishing**, and **business email compromise** attacks that exploit trust in internal communications.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://support.google.com/a/answer/9157861", + "https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Gmail**\n3. Click **Safety** > **Spoofing and authentication**\n4. Check **Protect against inbound emails spoofing your domain**\n5. Select the desired action (e.g., Move email to spam)\n6. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable protection against inbound emails spoofing your domain and configure an appropriate action such as moving to spam or quarantining.", + "Url": "https://hub.prowler.com/check/gmail_inbound_domain_spoofing_protection_enabled" + } + }, + "Categories": [ + "email-security" + ], + "DependsOn": [], + "RelatedTo": [ + "gmail_domain_spoofing_protection_enabled", + "gmail_employee_name_spoofing_protection_enabled", + "gmail_unauthenticated_email_protection_enabled", + "gmail_groups_spoofing_protection_enabled" + ], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_inbound_domain_spoofing_protection_enabled/gmail_inbound_domain_spoofing_protection_enabled.py b/prowler/providers/googleworkspace/services/gmail/gmail_inbound_domain_spoofing_protection_enabled/gmail_inbound_domain_spoofing_protection_enabled.py new file mode 100644 index 0000000000..b9a22cadc6 --- /dev/null +++ b/prowler/providers/googleworkspace/services/gmail/gmail_inbound_domain_spoofing_protection_enabled/gmail_inbound_domain_spoofing_protection_enabled.py @@ -0,0 +1,63 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.gmail.gmail_client import gmail_client + + +class gmail_inbound_domain_spoofing_protection_enabled(Check): + """Check that protection against inbound emails spoofing your domain is enabled. + + This check verifies that Gmail is configured to take action on + inbound emails that spoof the organization's own domain, helping + prevent impersonation of internal senders. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if gmail_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=gmail_client.policies, + resource_id="gmailPolicies", + resource_name="Gmail Policies", + customer_id=gmail_client.provider.identity.customer_id, + ) + + enabled = gmail_client.policies.detect_inbound_domain_spoofing + consequence = gmail_client.policies.inbound_domain_spoofing_consequence + + if enabled is False: + report.status = "FAIL" + report.status_extended = ( + f"Protection against inbound emails spoofing your domain " + f"is disabled in domain " + f"{gmail_client.provider.identity.domain}. " + f"Enable the protection and configure a protective action." + ) + elif consequence == "NO_ACTION": + report.status = "FAIL" + report.status_extended = ( + f"Protection against inbound emails spoofing your domain " + f"is set to take no action in domain " + f"{gmail_client.provider.identity.domain}. " + f"A protective action should be configured." + ) + elif consequence is None: + report.status = "PASS" + report.status_extended = ( + f"Protection against inbound emails spoofing your domain " + f"uses Google's secure default configuration (enabled) " + f"in domain {gmail_client.provider.identity.domain}." + ) + else: + report.status = "PASS" + report.status_extended = ( + f"Protection against inbound emails spoofing your domain " + f"is enabled with consequence '{consequence}' " + f"in domain {gmail_client.provider.identity.domain}." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_mail_delegation_disabled/gmail_mail_delegation_disabled.py b/prowler/providers/googleworkspace/services/gmail/gmail_mail_delegation_disabled/gmail_mail_delegation_disabled.py index 65cf54733b..5e32e58b99 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_mail_delegation_disabled/gmail_mail_delegation_disabled.py +++ b/prowler/providers/googleworkspace/services/gmail/gmail_mail_delegation_disabled/gmail_mail_delegation_disabled.py @@ -18,7 +18,10 @@ class gmail_mail_delegation_disabled(Check): if gmail_client.policies_fetched: report = CheckReportGoogleWorkspace( metadata=self.metadata(), - resource=gmail_client.provider.domain_resource, + resource=gmail_client.policies, + resource_id="gmailPolicies", + resource_name="Gmail Policies", + customer_id=gmail_client.provider.identity.customer_id, ) delegation_enabled = gmail_client.policies.enable_mail_delegation diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_per_user_outbound_gateway_disabled/gmail_per_user_outbound_gateway_disabled.py b/prowler/providers/googleworkspace/services/gmail/gmail_per_user_outbound_gateway_disabled/gmail_per_user_outbound_gateway_disabled.py index 95c5e6996f..3f384de1d0 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_per_user_outbound_gateway_disabled/gmail_per_user_outbound_gateway_disabled.py +++ b/prowler/providers/googleworkspace/services/gmail/gmail_per_user_outbound_gateway_disabled/gmail_per_user_outbound_gateway_disabled.py @@ -18,7 +18,10 @@ class gmail_per_user_outbound_gateway_disabled(Check): if gmail_client.policies_fetched: report = CheckReportGoogleWorkspace( metadata=self.metadata(), - resource=gmail_client.provider.domain_resource, + resource=gmail_client.policies, + resource_id="gmailPolicies", + resource_name="Gmail Policies", + customer_id=gmail_client.provider.identity.customer_id, ) gateway_allowed = gmail_client.policies.allow_per_user_outbound_gateway diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_pop_imap_access_disabled/gmail_pop_imap_access_disabled.py b/prowler/providers/googleworkspace/services/gmail/gmail_pop_imap_access_disabled/gmail_pop_imap_access_disabled.py index 17f7de3658..18b5034e31 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_pop_imap_access_disabled/gmail_pop_imap_access_disabled.py +++ b/prowler/providers/googleworkspace/services/gmail/gmail_pop_imap_access_disabled/gmail_pop_imap_access_disabled.py @@ -18,7 +18,10 @@ class gmail_pop_imap_access_disabled(Check): if gmail_client.policies_fetched: report = CheckReportGoogleWorkspace( metadata=self.metadata(), - resource=gmail_client.provider.domain_resource, + resource=gmail_client.policies, + resource_id="gmailPolicies", + resource_name="Gmail Policies", + customer_id=gmail_client.provider.identity.customer_id, ) pop_enabled = gmail_client.policies.enable_pop_access diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_script_attachment_protection_enabled/__init__.py b/prowler/providers/googleworkspace/services/gmail/gmail_script_attachment_protection_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_script_attachment_protection_enabled/gmail_script_attachment_protection_enabled.metadata.json b/prowler/providers/googleworkspace/services/gmail/gmail_script_attachment_protection_enabled/gmail_script_attachment_protection_enabled.metadata.json new file mode 100644 index 0000000000..fc2a3af1b0 --- /dev/null +++ b/prowler/providers/googleworkspace/services/gmail/gmail_script_attachment_protection_enabled/gmail_script_attachment_protection_enabled.metadata.json @@ -0,0 +1,40 @@ +{ + "Provider": "googleworkspace", + "CheckID": "gmail_script_attachment_protection_enabled", + "CheckTitle": "Protection against attachments with scripts from untrusted senders is enabled", + "CheckType": [], + "ServiceName": "gmail", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "collaboration", + "Description": "Verifies that Gmail is configured to take a protective action (such as moving to spam, quarantining, or showing a warning) when an attachment containing scripts is received from an untrusted sender. Script-bearing attachments (e.g., .js, .vbs, .ps1) are a common malware delivery mechanism.", + "Risk": "Without protection against script-bearing attachments from untrusted senders, users may receive **files containing malicious scripts** that can execute harmful code when opened. Attackers commonly use script attachments to deliver **malware, backdoors, or credential stealers**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://support.google.com/a/answer/7676854", + "https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Gmail**\n3. Click **Safety** > **Attachments**\n4. Check **Protect against attachments with scripts from untrusted senders**\n5. Select the desired action (e.g., Move email to spam)\n6. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable protection against attachments with scripts from untrusted senders and configure an appropriate action such as moving to spam or quarantining.", + "Url": "https://hub.prowler.com/check/gmail_script_attachment_protection_enabled" + } + }, + "Categories": [ + "email-security" + ], + "DependsOn": [], + "RelatedTo": [ + "gmail_encrypted_attachment_protection_enabled", + "gmail_anomalous_attachment_protection_enabled" + ], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_script_attachment_protection_enabled/gmail_script_attachment_protection_enabled.py b/prowler/providers/googleworkspace/services/gmail/gmail_script_attachment_protection_enabled/gmail_script_attachment_protection_enabled.py new file mode 100644 index 0000000000..1dd2ed59d1 --- /dev/null +++ b/prowler/providers/googleworkspace/services/gmail/gmail_script_attachment_protection_enabled/gmail_script_attachment_protection_enabled.py @@ -0,0 +1,65 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.gmail.gmail_client import gmail_client + + +class gmail_script_attachment_protection_enabled(Check): + """Check that protection against attachments with scripts from untrusted senders is enabled. + + This check verifies that Gmail is configured to take action on + attachments containing scripts from untrusted senders, helping + prevent malware delivery via script-bearing files. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if gmail_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=gmail_client.policies, + resource_id="gmailPolicies", + resource_name="Gmail Policies", + customer_id=gmail_client.provider.identity.customer_id, + ) + + enabled = gmail_client.policies.enable_script_attachment_protection + consequence = gmail_client.policies.script_attachment_protection_consequence + + if enabled is False: + report.status = "FAIL" + report.status_extended = ( + f"Protection against attachments with scripts from " + f"untrusted senders is disabled in domain " + f"{gmail_client.provider.identity.domain}. " + f"Enable the protection and configure a protective action." + ) + elif consequence == "NO_ACTION": + report.status = "FAIL" + report.status_extended = ( + f"Protection against attachments with scripts from " + f"untrusted senders is set to take no action in domain " + f"{gmail_client.provider.identity.domain}. " + f"A protective action should be configured." + ) + elif consequence is None: + report.status = "PASS" + report.status_extended = ( + f"Protection against attachments with scripts from " + f"untrusted senders uses Google's secure default " + f"configuration (enabled) in domain " + f"{gmail_client.provider.identity.domain}." + ) + else: + report.status = "PASS" + report.status_extended = ( + f"Protection against attachments with scripts from " + f"untrusted senders is enabled with consequence " + f"'{consequence}' in domain " + f"{gmail_client.provider.identity.domain}." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_service.py b/prowler/providers/googleworkspace/services/gmail/gmail_service.py index eecf0c1884..ed9dc71803 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_service.py +++ b/prowler/providers/googleworkspace/services/gmail/gmail_service.py @@ -57,12 +57,21 @@ class Gmail(GoogleWorkspaceService): logger.debug("Gmail mail delegation setting fetched.") elif setting_type == "gmail.email_attachment_safety": + self.policies.enable_encrypted_attachment_protection = ( + value.get("enableEncryptedAttachmentProtection") + ) self.policies.encrypted_attachment_protection_consequence = value.get( "encryptedAttachmentProtectionConsequence" ) + self.policies.enable_script_attachment_protection = ( + value.get("enableAttachmentWithScriptsProtection") + ) self.policies.script_attachment_protection_consequence = ( value.get("scriptAttachmentProtectionConsequence") ) + self.policies.enable_anomalous_attachment_protection = ( + value.get("enableAnomalousAttachmentProtection") + ) self.policies.anomalous_attachment_protection_consequence = value.get( "anomalousAttachmentProtectionConsequence" ) @@ -83,18 +92,36 @@ class Gmail(GoogleWorkspaceService): ) elif setting_type == "gmail.spoofing_and_authentication": + self.policies.detect_domain_name_spoofing = value.get( + "detectDomainNameSpoofing" + ) self.policies.domain_spoofing_consequence = value.get( "domainSpoofingConsequence" ) + self.policies.detect_employee_name_spoofing = value.get( + "detectEmployeeNameSpoofing" + ) self.policies.employee_name_spoofing_consequence = ( value.get("employeeNameSpoofingConsequence") ) + self.policies.detect_inbound_domain_spoofing = value.get( + "detectDomainSpoofingFromUnauthenticatedSenders" + ) self.policies.inbound_domain_spoofing_consequence = ( value.get("inboundDomainSpoofingConsequence") ) + self.policies.detect_unauthenticated_emails = value.get( + "detectUnauthenticatedEmails" + ) self.policies.unauthenticated_email_consequence = value.get( "unauthenticatedEmailConsequence" ) + self.policies.detect_groups_spoofing = value.get( + "detectGroupsSpoofing" + ) + self.policies.groups_spoofing_visibility_type = value.get( + "groupsSpoofingVisibilityType" + ) self.policies.groups_spoofing_consequence = value.get( "groupsSpoofingConsequence" ) @@ -177,8 +204,11 @@ class GmailPolicies(BaseModel): enable_mail_delegation: Optional[bool] = None # gmail.email_attachment_safety + enable_encrypted_attachment_protection: Optional[bool] = None encrypted_attachment_protection_consequence: Optional[str] = None + enable_script_attachment_protection: Optional[bool] = None script_attachment_protection_consequence: Optional[str] = None + enable_anomalous_attachment_protection: Optional[bool] = None anomalous_attachment_protection_consequence: Optional[str] = None # gmail.links_and_external_images @@ -187,10 +217,16 @@ class GmailPolicies(BaseModel): enable_aggressive_warnings_on_untrusted_links: Optional[bool] = None # gmail.spoofing_and_authentication + detect_domain_name_spoofing: Optional[bool] = None domain_spoofing_consequence: Optional[str] = None + detect_employee_name_spoofing: Optional[bool] = None employee_name_spoofing_consequence: Optional[str] = None + detect_inbound_domain_spoofing: Optional[bool] = None inbound_domain_spoofing_consequence: Optional[str] = None + detect_unauthenticated_emails: Optional[bool] = None unauthenticated_email_consequence: Optional[str] = None + detect_groups_spoofing: Optional[bool] = None + groups_spoofing_visibility_type: Optional[str] = None groups_spoofing_consequence: Optional[str] = None # gmail.pop_access diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_shortener_scanning_enabled/gmail_shortener_scanning_enabled.py b/prowler/providers/googleworkspace/services/gmail/gmail_shortener_scanning_enabled/gmail_shortener_scanning_enabled.py index f9bfef5112..01411dcc04 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_shortener_scanning_enabled/gmail_shortener_scanning_enabled.py +++ b/prowler/providers/googleworkspace/services/gmail/gmail_shortener_scanning_enabled/gmail_shortener_scanning_enabled.py @@ -18,7 +18,10 @@ class gmail_shortener_scanning_enabled(Check): if gmail_client.policies_fetched: report = CheckReportGoogleWorkspace( metadata=self.metadata(), - resource=gmail_client.provider.domain_resource, + resource=gmail_client.policies, + resource_id="gmailPolicies", + resource_name="Gmail Policies", + customer_id=gmail_client.provider.identity.customer_id, ) scanning_enabled = gmail_client.policies.enable_shortener_scanning diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_unauthenticated_email_protection_enabled/__init__.py b/prowler/providers/googleworkspace/services/gmail/gmail_unauthenticated_email_protection_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_unauthenticated_email_protection_enabled/gmail_unauthenticated_email_protection_enabled.metadata.json b/prowler/providers/googleworkspace/services/gmail/gmail_unauthenticated_email_protection_enabled/gmail_unauthenticated_email_protection_enabled.metadata.json new file mode 100644 index 0000000000..6199080224 --- /dev/null +++ b/prowler/providers/googleworkspace/services/gmail/gmail_unauthenticated_email_protection_enabled/gmail_unauthenticated_email_protection_enabled.metadata.json @@ -0,0 +1,42 @@ +{ + "Provider": "googleworkspace", + "CheckID": "gmail_unauthenticated_email_protection_enabled", + "CheckTitle": "Protection against any unauthenticated emails is enabled", + "CheckType": [], + "ServiceName": "gmail", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "collaboration", + "Description": "Verifies that Gmail is configured to take a protective action (such as moving to spam, quarantining, or showing a warning) when emails are not authenticated via SPF or DKIM. Unauthenticated emails cannot be verified as originating from the claimed sender, making them more likely to be spoofed or forged.", + "Risk": "Without protection against unauthenticated emails, users may receive **spoofed or forged messages** that fail SPF and DKIM checks but are still delivered normally. This enables **phishing**, **spam**, and **impersonation attacks** that exploit the lack of sender verification.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://support.google.com/a/answer/9157861", + "https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Gmail**\n3. Click **Safety** > **Spoofing and authentication**\n4. Check **Protect against any unauthenticated emails**\n5. Select the desired action (e.g., Move email to spam)\n6. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable protection against any unauthenticated emails and configure an appropriate action such as moving to spam or quarantining.", + "Url": "https://hub.prowler.com/check/gmail_unauthenticated_email_protection_enabled" + } + }, + "Categories": [ + "email-security" + ], + "DependsOn": [], + "RelatedTo": [ + "gmail_domain_spoofing_protection_enabled", + "gmail_employee_name_spoofing_protection_enabled", + "gmail_inbound_domain_spoofing_protection_enabled", + "gmail_groups_spoofing_protection_enabled" + ], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_unauthenticated_email_protection_enabled/gmail_unauthenticated_email_protection_enabled.py b/prowler/providers/googleworkspace/services/gmail/gmail_unauthenticated_email_protection_enabled/gmail_unauthenticated_email_protection_enabled.py new file mode 100644 index 0000000000..44ff6fa24d --- /dev/null +++ b/prowler/providers/googleworkspace/services/gmail/gmail_unauthenticated_email_protection_enabled/gmail_unauthenticated_email_protection_enabled.py @@ -0,0 +1,70 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.gmail.gmail_client import gmail_client + + +class gmail_unauthenticated_email_protection_enabled(Check): + """Check that protection against any unauthenticated emails is enabled. + + This check verifies that Gmail is configured to take action on + emails that are not authenticated via SPF or DKIM, helping prevent + delivery of spoofed or forged messages. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if gmail_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=gmail_client.policies, + resource_id="gmailPolicies", + resource_name="Gmail Policies", + customer_id=gmail_client.provider.identity.customer_id, + ) + + enabled = gmail_client.policies.detect_unauthenticated_emails + consequence = gmail_client.policies.unauthenticated_email_consequence + + if enabled is False: + report.status = "FAIL" + report.status_extended = ( + f"Protection against unauthenticated emails is disabled " + f"in domain {gmail_client.provider.identity.domain}. " + f"Enable the protection and configure a protective action." + ) + elif enabled is None: + report.status = "FAIL" + report.status_extended = ( + f"Protection against unauthenticated emails is not " + f"configured and uses Google's insecure default " + f"(disabled) in domain " + f"{gmail_client.provider.identity.domain}. " + f"Enable the protection and configure a protective action." + ) + elif consequence == "NO_ACTION": + report.status = "FAIL" + report.status_extended = ( + f"Protection against unauthenticated emails is set to " + f"take no action in domain " + f"{gmail_client.provider.identity.domain}. " + f"A protective action should be configured." + ) + elif consequence is None: + report.status = "PASS" + report.status_extended = ( + f"Protection against unauthenticated emails is enabled " + f"in domain {gmail_client.provider.identity.domain}." + ) + else: + report.status = "PASS" + report.status_extended = ( + f"Protection against unauthenticated emails is enabled " + f"with consequence '{consequence}' in domain " + f"{gmail_client.provider.identity.domain}." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/gmail/gmail_untrusted_link_warnings_enabled/gmail_untrusted_link_warnings_enabled.py b/prowler/providers/googleworkspace/services/gmail/gmail_untrusted_link_warnings_enabled/gmail_untrusted_link_warnings_enabled.py index 4bf1dbdcdf..a62480de3b 100644 --- a/prowler/providers/googleworkspace/services/gmail/gmail_untrusted_link_warnings_enabled/gmail_untrusted_link_warnings_enabled.py +++ b/prowler/providers/googleworkspace/services/gmail/gmail_untrusted_link_warnings_enabled/gmail_untrusted_link_warnings_enabled.py @@ -18,7 +18,10 @@ class gmail_untrusted_link_warnings_enabled(Check): if gmail_client.policies_fetched: report = CheckReportGoogleWorkspace( metadata=self.metadata(), - resource=gmail_client.provider.domain_resource, + resource=gmail_client.policies, + resource_id="gmailPolicies", + resource_name="Gmail Policies", + customer_id=gmail_client.provider.identity.customer_id, ) warnings_enabled = ( @@ -32,11 +35,13 @@ class gmail_untrusted_link_warnings_enabled(Check): f"in domain {gmail_client.provider.identity.domain}." ) elif warnings_enabled is None: - report.status = "PASS" + report.status = "FAIL" report.status_extended = ( - f"Warning prompts for clicks on untrusted domain links uses Google's " - f"secure default configuration (enabled) " - f"in domain {gmail_client.provider.identity.domain}." + f"Warning prompts for clicks on untrusted domain links " + f"are not configured and use Google's insecure default " + f"(disabled) in domain " + f"{gmail_client.provider.identity.domain}. " + f"Untrusted link warnings should be enabled to protect users." ) else: report.status = "FAIL" diff --git a/prowler/providers/googleworkspace/services/groups/__init__.py b/prowler/providers/googleworkspace/services/groups/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/groups/groups_client.py b/prowler/providers/googleworkspace/services/groups/groups_client.py new file mode 100644 index 0000000000..0f4881b52c --- /dev/null +++ b/prowler/providers/googleworkspace/services/groups/groups_client.py @@ -0,0 +1,6 @@ +from prowler.providers.common.provider import Provider +from prowler.providers.googleworkspace.services.groups.groups_service import ( + Groups, +) + +groups_client = Groups(Provider.get_global_provider()) diff --git a/prowler/providers/googleworkspace/services/groups/groups_creation_restricted/__init__.py b/prowler/providers/googleworkspace/services/groups/groups_creation_restricted/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/groups/groups_creation_restricted/groups_creation_restricted.metadata.json b/prowler/providers/googleworkspace/services/groups/groups_creation_restricted/groups_creation_restricted.metadata.json new file mode 100644 index 0000000000..c2725de695 --- /dev/null +++ b/prowler/providers/googleworkspace/services/groups/groups_creation_restricted/groups_creation_restricted.metadata.json @@ -0,0 +1,40 @@ +{ + "Provider": "googleworkspace", + "CheckID": "groups_creation_restricted", + "CheckTitle": "Group creation is restricted to admins with no external members or incoming email", + "CheckType": [], + "ServiceName": "groups", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "collaboration", + "Description": "Google Groups for Business **creation settings** control who can create groups and whether group owners can add external members or allow incoming email from outside the organization. Restricting creation to admins and disabling external member and incoming email options limits the attack surface.", + "Risk": "Allowing any user to create groups with external members or incoming email from outside increases the risk of **unauthorized data sharing**, **spam delivery**, and **shadow IT** groups that bypass organizational controls.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://support.google.com/a/answer/10308022", + "https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Groups for Business**\n3. Click **Creating groups**\n4. Select **Only organization admins can create groups**\n5. Uncheck **Group owners can allow external members**\n6. Uncheck **Group owners can allow incoming email from outside the organization**\n7. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Restrict **group creation** to **organization admins** and disable **external member** and **incoming email** options to maintain control over group membership and communication boundaries.", + "Url": "https://hub.prowler.com/check/groups_creation_restricted" + } + }, + "Categories": [ + "trust-boundaries" + ], + "DependsOn": [], + "RelatedTo": [ + "groups_external_access_restricted", + "groups_view_conversations_restricted" + ], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/groups/groups_creation_restricted/groups_creation_restricted.py b/prowler/providers/googleworkspace/services/groups/groups_creation_restricted/groups_creation_restricted.py new file mode 100644 index 0000000000..ad9ce534ed --- /dev/null +++ b/prowler/providers/googleworkspace/services/groups/groups_creation_restricted/groups_creation_restricted.py @@ -0,0 +1,76 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.groups.groups_client import ( + groups_client, +) + + +class groups_creation_restricted(Check): + """Check that group creation is restricted to admins only with no external members or incoming email. + + This check verifies three sub-settings: + - Only organization admins can create groups (not all users) + - Group owners cannot allow external members + - Group owners cannot allow incoming email from outside the organization + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if groups_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=groups_client.policies, + resource_id="groupsPolicies", + resource_name="Groups Policies", + customer_id=groups_client.provider.identity.customer_id, + ) + + policies = groups_client.policies + domain = groups_client.provider.identity.domain + + access_level = policies.create_groups_access_level + external_members = policies.owners_can_allow_external_members + incoming_mail = policies.owners_can_allow_incoming_mail_from_public + + issues = [] + + # Check creation access level + # Default is USERS_IN_DOMAIN (insecure) — only ADMIN_ONLY is compliant + if access_level is None or access_level != "ADMIN_ONLY": + effective = access_level or "USERS_IN_DOMAIN (default)" + issues.append( + f"group creation is set to {effective} instead of ADMIN_ONLY" + ) + + # Check external members + # Default is false (secure) — only false is compliant + if external_members is True: + issues.append("group owners can allow external members") + + # Check incoming mail from outside + # Default is false (secure) — only true is non-compliant + if incoming_mail is True: + issues.append( + "group owners can allow incoming email from outside the organization" + ) + + if not issues: + report.status = "PASS" + report.status_extended = ( + f"Group creation is properly restricted in domain {domain}: " + f"admin-only creation, no external members, " + f"no incoming email from outside." + ) + else: + report.status = "FAIL" + issues_text = "; ".join(issues) + report.status_extended = ( + f"Group creation is not fully restricted " + f"in domain {domain}: {issues_text}." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/groups/groups_external_access_restricted/__init__.py b/prowler/providers/googleworkspace/services/groups/groups_external_access_restricted/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/groups/groups_external_access_restricted/groups_external_access_restricted.metadata.json b/prowler/providers/googleworkspace/services/groups/groups_external_access_restricted/groups_external_access_restricted.metadata.json new file mode 100644 index 0000000000..3656879182 --- /dev/null +++ b/prowler/providers/googleworkspace/services/groups/groups_external_access_restricted/groups_external_access_restricted.metadata.json @@ -0,0 +1,40 @@ +{ + "Provider": "googleworkspace", + "CheckID": "groups_external_access_restricted", + "CheckTitle": "Accessing groups from outside the organization is set to private", + "CheckType": [], + "ServiceName": "groups", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "collaboration", + "Description": "Google Groups for Business **external access** controls whether people outside the organization can view and search for groups. When set to private, only users within the domain can discover and access groups, while external users can still email a group if the group's own settings allow it.", + "Risk": "Allowing external access to groups exposes **group names, descriptions, and membership** to anyone outside the organization, increasing the risk of **information disclosure** and enabling external parties to identify targets for **social engineering attacks**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://support.google.com/a/answer/10308022", + "https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Groups for Business**\n3. Click **Sharing options**\n4. Set **Accessing groups from outside this organization** to **Private**\n5. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Set **external group access** to **private** to prevent external parties from viewing or searching for organizational groups.", + "Url": "https://hub.prowler.com/check/groups_external_access_restricted" + } + }, + "Categories": [ + "trust-boundaries" + ], + "DependsOn": [], + "RelatedTo": [ + "groups_creation_restricted", + "groups_view_conversations_restricted" + ], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/groups/groups_external_access_restricted/groups_external_access_restricted.py b/prowler/providers/googleworkspace/services/groups/groups_external_access_restricted/groups_external_access_restricted.py new file mode 100644 index 0000000000..4ac057b905 --- /dev/null +++ b/prowler/providers/googleworkspace/services/groups/groups_external_access_restricted/groups_external_access_restricted.py @@ -0,0 +1,54 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.groups.groups_client import ( + groups_client, +) + + +class groups_external_access_restricted(Check): + """Check that accessing groups from outside the organization is set to private. + + This check verifies that the domain-level Groups for Business policy + restricts external access so that only domain users can view groups, + preventing information exposure to external parties. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if groups_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=groups_client.policies, + resource_id="groupsPolicies", + resource_name="Groups Policies", + customer_id=groups_client.provider.identity.customer_id, + ) + + collaboration = groups_client.policies.collaboration_capability + domain = groups_client.provider.identity.domain + + if collaboration == "DOMAIN_USERS_ONLY": + report.status = "PASS" + report.status_extended = ( + f"Groups external access is set to private (domain users only) " + f"in domain {domain}." + ) + elif collaboration is None: + report.status = "PASS" + report.status_extended = ( + f"Groups external access uses Google's secure default " + f"configuration (private) in domain {domain}." + ) + else: + report.status = "FAIL" + report.status_extended = ( + f"Groups external access is set to {collaboration} " + f"in domain {domain}. " + f"External access should be set to private (domain users only)." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/groups/groups_service.py b/prowler/providers/googleworkspace/services/groups/groups_service.py new file mode 100644 index 0000000000..1ebc0508e3 --- /dev/null +++ b/prowler/providers/googleworkspace/services/groups/groups_service.py @@ -0,0 +1,118 @@ +from typing import Optional + +from pydantic import BaseModel + +from prowler.lib.logger import logger +from prowler.providers.googleworkspace.lib.service.service import GoogleWorkspaceService + + +class Groups(GoogleWorkspaceService): + """Google Workspace Groups for Business service for auditing domain-level group policies. + + Uses the Cloud Identity Policy API v1 to read group sharing, creation, + and conversation viewing settings configured in the Admin Console. + """ + + def __init__(self, provider): + super().__init__(provider) + self.policies = GroupsPolicies() + self.policies_fetched = False + self._fetch_groups_for_business_policies() + + def _fetch_groups_for_business_policies(self): + """Fetch Groups for Business policies from the Cloud Identity Policy API v1.""" + logger.info("Groups for Business - Fetching policies...") + + try: + service = self._build_service("cloudidentity", "v1") + + if not service: + logger.error("Failed to build Cloud Identity service") + return + + request = service.policies().list( + pageSize=100, + filter='setting.type.matches("groups_for_business.*")', + ) + fetch_succeeded = True + + while request is not None: + try: + response = request.execute() + + for policy in response.get("policies", []): + if not self._is_customer_level_policy(policy): + continue + + setting = policy.get("setting", {}) + setting_type = setting.get("type", "").removeprefix("settings/") + logger.debug(f"Processing setting type: {setting_type}") + + value = setting.get("value", {}) + + if setting_type == "groups_for_business.groups_sharing": + self.policies.collaboration_capability = value.get( + "collaborationCapability" + ) + self.policies.create_groups_access_level = value.get( + "createGroupsAccessLevel" + ) + self.policies.owners_can_allow_external_members = value.get( + "ownersCanAllowExternalMembers" + ) + self.policies.owners_can_allow_incoming_mail_from_public = ( + value.get("ownersCanAllowIncomingMailFromPublic") + ) + self.policies.view_topics_default_access_level = value.get( + "viewTopicsDefaultAccessLevel" + ) + self.policies.owners_can_hide_groups = value.get( + "ownersCanHideGroups" + ) + self.policies.new_groups_are_hidden = value.get( + "newGroupsAreHidden" + ) + logger.debug( + "Groups for Business sharing settings fetched." + ) + + request = service.policies().list_next(request, response) + + except Exception as error: + self._handle_api_error( + error, + "fetching Groups for Business policies", + self.provider.identity.customer_id, + ) + fetch_succeeded = False + break + + self.policies_fetched = fetch_succeeded + + logger.info( + f"Groups for Business policies fetched - " + f"Collaboration: {self.policies.collaboration_capability}, " + f"Creation: {self.policies.create_groups_access_level}, " + f"View topics: {self.policies.view_topics_default_access_level}" + ) + + except Exception as error: + self._handle_api_error( + error, + "fetching Groups for Business policies", + self.provider.identity.customer_id, + ) + self.policies_fetched = False + + +class GroupsPolicies(BaseModel): + """Model for domain-level Groups for Business policy settings.""" + + # groups_for_business.groups_sharing + collaboration_capability: Optional[str] = None + create_groups_access_level: Optional[str] = None + owners_can_allow_external_members: Optional[bool] = None + owners_can_allow_incoming_mail_from_public: Optional[bool] = None + view_topics_default_access_level: Optional[str] = None + owners_can_hide_groups: Optional[bool] = None + new_groups_are_hidden: Optional[bool] = None diff --git a/prowler/providers/googleworkspace/services/groups/groups_view_conversations_restricted/__init__.py b/prowler/providers/googleworkspace/services/groups/groups_view_conversations_restricted/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/groups/groups_view_conversations_restricted/groups_view_conversations_restricted.metadata.json b/prowler/providers/googleworkspace/services/groups/groups_view_conversations_restricted/groups_view_conversations_restricted.metadata.json new file mode 100644 index 0000000000..b90fe6041b --- /dev/null +++ b/prowler/providers/googleworkspace/services/groups/groups_view_conversations_restricted/groups_view_conversations_restricted.metadata.json @@ -0,0 +1,40 @@ +{ + "Provider": "googleworkspace", + "CheckID": "groups_view_conversations_restricted", + "CheckTitle": "Default permission to view conversations is set to all group members", + "CheckType": [], + "ServiceName": "groups", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "collaboration", + "Description": "Google Groups for Business **conversation viewing** controls who can see group conversations by default. Restricting this to group members only ensures that conversations are visible only to participants, not all users in the organization or external parties.", + "Risk": "Allowing all organization users or anyone to view group conversations can lead to **information disclosure** of sensitive discussions, internal decisions, and confidential data shared within groups.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://support.google.com/a/answer/10308022", + "https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Groups for Business**\n3. Click **Sharing options**\n4. Set **Default for permission to view conversations** to **All group members**\n5. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Set the default **permission to view conversations** to **all group members** to ensure group discussions are only visible to their participants.", + "Url": "https://hub.prowler.com/check/groups_view_conversations_restricted" + } + }, + "Categories": [ + "trust-boundaries" + ], + "DependsOn": [], + "RelatedTo": [ + "groups_external_access_restricted", + "groups_creation_restricted" + ], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/groups/groups_view_conversations_restricted/groups_view_conversations_restricted.py b/prowler/providers/googleworkspace/services/groups/groups_view_conversations_restricted/groups_view_conversations_restricted.py new file mode 100644 index 0000000000..66fb15a43b --- /dev/null +++ b/prowler/providers/googleworkspace/services/groups/groups_view_conversations_restricted/groups_view_conversations_restricted.py @@ -0,0 +1,55 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.groups.groups_client import ( + groups_client, +) + + +class groups_view_conversations_restricted(Check): + """Check that the default permission to view conversations is set to All Group Members. + + This check verifies that the domain-level Groups for Business policy + restricts conversation viewing to group members only, preventing + broader access by all organization users or anyone. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if groups_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=groups_client.policies, + resource_id="groupsPolicies", + resource_name="Groups Policies", + customer_id=groups_client.provider.identity.customer_id, + ) + + view_access = groups_client.policies.view_topics_default_access_level + domain = groups_client.provider.identity.domain + + if view_access == "GROUP_MEMBERS": + report.status = "PASS" + report.status_extended = ( + f"Default permission to view conversations is set to " + f"all group members in domain {domain}." + ) + elif view_access is None: + report.status = "FAIL" + report.status_extended = ( + f"Default permission to view conversations uses Google's default " + f"configuration (all organization users) in domain {domain}. " + f"It should be restricted to all group members only." + ) + else: + report.status = "FAIL" + report.status_extended = ( + f"Default permission to view conversations is set to " + f"{view_access} in domain {domain}. " + f"It should be restricted to all group members only." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/marketplace/__init__.py b/prowler/providers/googleworkspace/services/marketplace/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/marketplace/marketplace_apps_access_restricted/__init__.py b/prowler/providers/googleworkspace/services/marketplace/marketplace_apps_access_restricted/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/marketplace/marketplace_apps_access_restricted/marketplace_apps_access_restricted.metadata.json b/prowler/providers/googleworkspace/services/marketplace/marketplace_apps_access_restricted/marketplace_apps_access_restricted.metadata.json new file mode 100644 index 0000000000..7d982370a1 --- /dev/null +++ b/prowler/providers/googleworkspace/services/marketplace/marketplace_apps_access_restricted/marketplace_apps_access_restricted.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "googleworkspace", + "CheckID": "marketplace_apps_access_restricted", + "CheckTitle": "Users access to Google Workspace Marketplace apps is restricted", + "CheckType": [], + "ServiceName": "marketplace", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "collaboration", + "Description": "The domain-wide Google Workspace Marketplace configuration **restricts which apps users can install**. Only admin-approved apps from the Marketplace allowlist are permitted, preventing users from installing unvetted third-party applications.", + "Risk": "Allowing unrestricted Marketplace app installation exposes the organization to **unvetted third-party applications** that may request broad OAuth scopes, potentially gaining access to **sensitive organizational data** including emails, documents, and calendar events without proper security review.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://support.google.com/a/answer/6089179", + "https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace Marketplace apps**\n3. Click **Settings**\n4. Under **Manage Google Workspace Marketplace allowlist access**, select **Allow users to install and run only selected apps from the Marketplace**\n5. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Restrict **Marketplace app installation** to only **admin-approved apps** to prevent users from installing unvetted third-party applications that could access sensitive organizational data.", + "Url": "https://hub.prowler.com/check/marketplace_apps_access_restricted" + } + }, + "Categories": [ + "trust-boundaries" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/marketplace/marketplace_apps_access_restricted/marketplace_apps_access_restricted.py b/prowler/providers/googleworkspace/services/marketplace/marketplace_apps_access_restricted/marketplace_apps_access_restricted.py new file mode 100644 index 0000000000..cee1adaa71 --- /dev/null +++ b/prowler/providers/googleworkspace/services/marketplace/marketplace_apps_access_restricted/marketplace_apps_access_restricted.py @@ -0,0 +1,61 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.marketplace.marketplace_client import ( + marketplace_client, +) + + +class marketplace_apps_access_restricted(Check): + """Check that Google Workspace Marketplace app installation is restricted. + + This check verifies that the domain-level Marketplace policy restricts + which apps users can install, preventing unvetted third-party applications + from accessing organizational data. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if marketplace_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=marketplace_client.policies, + resource_id="marketplacePolicies", + resource_name="Marketplace Policies", + customer_id=marketplace_client.provider.identity.customer_id, + ) + + access_level = marketplace_client.policies.access_level + + if access_level == "ALLOW_LISTED_APPS": + report.status = "PASS" + report.status_extended = ( + f"Marketplace app installation is restricted to admin-approved apps " + f"in domain {marketplace_client.provider.identity.domain}." + ) + elif access_level == "ALLOW_NONE": + report.status = "PASS" + report.status_extended = ( + f"Marketplace app installation is fully blocked " + f"in domain {marketplace_client.provider.identity.domain}." + ) + else: + report.status = "FAIL" + if access_level is None: + report.status_extended = ( + f"Marketplace app access is not explicitly configured " + f"in domain {marketplace_client.provider.identity.domain}. " + f"The default allows all apps. " + f"App installation should be restricted to approved apps only." + ) + else: + report.status_extended = ( + f"Marketplace allows users to install any app " + f"in domain {marketplace_client.provider.identity.domain}. " + f"App installation should be restricted to approved apps only." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/marketplace/marketplace_client.py b/prowler/providers/googleworkspace/services/marketplace/marketplace_client.py new file mode 100644 index 0000000000..ccc6cdf208 --- /dev/null +++ b/prowler/providers/googleworkspace/services/marketplace/marketplace_client.py @@ -0,0 +1,6 @@ +from prowler.providers.common.provider import Provider +from prowler.providers.googleworkspace.services.marketplace.marketplace_service import ( + Marketplace, +) + +marketplace_client = Marketplace(Provider.get_global_provider()) diff --git a/prowler/providers/googleworkspace/services/marketplace/marketplace_service.py b/prowler/providers/googleworkspace/services/marketplace/marketplace_service.py new file mode 100644 index 0000000000..4c117a4468 --- /dev/null +++ b/prowler/providers/googleworkspace/services/marketplace/marketplace_service.py @@ -0,0 +1,89 @@ +from typing import Optional + +from pydantic import BaseModel + +from prowler.lib.logger import logger +from prowler.providers.googleworkspace.lib.service.service import GoogleWorkspaceService + + +class Marketplace(GoogleWorkspaceService): + """Google Workspace Marketplace service for auditing domain-level Marketplace policies. + + Uses the Cloud Identity Policy API v1 to read the Marketplace app access + settings configured in the Admin Console. + """ + + def __init__(self, provider): + super().__init__(provider) + self.policies = MarketplacePolicies() + self.policies_fetched = False + self._fetch_marketplace_policies() + + def _fetch_marketplace_policies(self): + """Fetch Marketplace policies from the Cloud Identity Policy API v1.""" + logger.info("Marketplace - Fetching marketplace policies...") + + try: + service = self._build_service("cloudidentity", "v1") + + if not service: + logger.error("Failed to build Cloud Identity service") + return + + request = service.policies().list( + pageSize=100, + filter='setting.type.matches("workspace_marketplace.*")', + ) + fetch_succeeded = True + + while request is not None: + try: + response = request.execute() + + for policy in response.get("policies", []): + if not self._is_customer_level_policy(policy): + continue + + setting = policy.get("setting", {}) + setting_type = setting.get("type", "").removeprefix("settings/") + value = setting.get("value", {}) + + if setting_type == "workspace_marketplace.apps_access_options": + self.policies.access_level = value.get("accessLevel") + logger.debug( + "Marketplace access level: " + f"{self.policies.access_level}" + ) + + request = service.policies().list_next(request, response) + + except Exception as error: + self._handle_api_error( + error, + "fetching Marketplace policies", + self.provider.identity.customer_id, + ) + fetch_succeeded = False + break + + self.policies_fetched = fetch_succeeded + + logger.info( + f"Marketplace policies fetched - " + f"Access level: {self.policies.access_level}" + ) + + except Exception as error: + self._handle_api_error( + error, + "fetching Marketplace policies", + self.provider.identity.customer_id, + ) + self.policies_fetched = False + + +class MarketplacePolicies(BaseModel): + """Model for domain-level Marketplace policy settings.""" + + # workspace_marketplace.apps_access_options + access_level: Optional[str] = None diff --git a/prowler/providers/googleworkspace/services/sites/__init__.py b/prowler/providers/googleworkspace/services/sites/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/sites/sites_client.py b/prowler/providers/googleworkspace/services/sites/sites_client.py new file mode 100644 index 0000000000..d57e537c5d --- /dev/null +++ b/prowler/providers/googleworkspace/services/sites/sites_client.py @@ -0,0 +1,6 @@ +from prowler.providers.common.provider import Provider +from prowler.providers.googleworkspace.services.sites.sites_service import ( + Sites, +) + +sites_client = Sites(Provider.get_global_provider()) diff --git a/prowler/providers/googleworkspace/services/sites/sites_service.py b/prowler/providers/googleworkspace/services/sites/sites_service.py new file mode 100644 index 0000000000..456c2aca65 --- /dev/null +++ b/prowler/providers/googleworkspace/services/sites/sites_service.py @@ -0,0 +1,88 @@ +from typing import Optional + +from pydantic import BaseModel + +from prowler.lib.logger import logger +from prowler.providers.googleworkspace.lib.service.service import GoogleWorkspaceService + + +class Sites(GoogleWorkspaceService): + """Google Workspace Sites service for auditing domain-level Sites policies. + + Uses the Cloud Identity Policy API v1 to read the Sites service status + configured in the Admin Console. + """ + + def __init__(self, provider): + super().__init__(provider) + self.policies = SitesPolicies() + self.policies_fetched = False + self._fetch_sites_policies() + + def _fetch_sites_policies(self): + """Fetch Sites policies from the Cloud Identity Policy API v1.""" + logger.info("Sites - Fetching sites policies...") + + try: + service = self._build_service("cloudidentity", "v1") + + if not service: + logger.error("Failed to build Cloud Identity service") + return + + request = service.policies().list( + pageSize=100, + filter='setting.type.matches("sites.*")', + ) + fetch_succeeded = True + + while request is not None: + try: + response = request.execute() + + for policy in response.get("policies", []): + if not self._is_customer_level_policy(policy): + continue + + setting = policy.get("setting", {}) + setting_type = setting.get("type", "").removeprefix("settings/") + value = setting.get("value", {}) + + if setting_type == "sites.service_status": + self.policies.service_state = value.get("serviceState") + logger.debug( + "Sites service state: " f"{self.policies.service_state}" + ) + + request = service.policies().list_next(request, response) + + except Exception as error: + self._handle_api_error( + error, + "fetching Sites policies", + self.provider.identity.customer_id, + ) + fetch_succeeded = False + break + + self.policies_fetched = fetch_succeeded + + logger.info( + f"Sites policies fetched - " + f"Service state: {self.policies.service_state}" + ) + + except Exception as error: + self._handle_api_error( + error, + "fetching Sites policies", + self.provider.identity.customer_id, + ) + self.policies_fetched = False + + +class SitesPolicies(BaseModel): + """Model for domain-level Sites policy settings.""" + + # sites.service_status + service_state: Optional[str] = None diff --git a/prowler/providers/googleworkspace/services/sites/sites_service_disabled/__init__.py b/prowler/providers/googleworkspace/services/sites/sites_service_disabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/sites/sites_service_disabled/sites_service_disabled.metadata.json b/prowler/providers/googleworkspace/services/sites/sites_service_disabled/sites_service_disabled.metadata.json new file mode 100644 index 0000000000..6422fa974b --- /dev/null +++ b/prowler/providers/googleworkspace/services/sites/sites_service_disabled/sites_service_disabled.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "googleworkspace", + "CheckID": "sites_service_disabled", + "CheckTitle": "Service status for Google Sites is set to off", + "CheckType": [], + "ServiceName": "sites", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "collaboration", + "Description": "The domain-wide Google Sites configuration has the service **disabled for all users**. Google Sites allows users to create internal and external websites, which may expose sensitive information or create unmanaged content outside the organization's control.", + "Risk": "When Google Sites is enabled, users can create websites that may **inadvertently expose internal information** to external parties. These sites can be difficult to track and manage, creating potential **data leakage vectors** outside the organization's standard content management controls.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://support.google.com/a/answer/182442", + "https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Sites**\n3. Click **Service status**\n4. Select **OFF for everyone**\n5. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Disable **Google Sites** for all users to reduce the organization's attack surface. If specific users require access, enable it by exception for those users or groups only.", + "Url": "https://hub.prowler.com/check/sites_service_disabled" + } + }, + "Categories": [ + "trust-boundaries" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/sites/sites_service_disabled/sites_service_disabled.py b/prowler/providers/googleworkspace/services/sites/sites_service_disabled/sites_service_disabled.py new file mode 100644 index 0000000000..2acd84fb7c --- /dev/null +++ b/prowler/providers/googleworkspace/services/sites/sites_service_disabled/sites_service_disabled.py @@ -0,0 +1,52 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.sites.sites_client import sites_client + + +class sites_service_disabled(Check): + """Check that the Google Sites service is disabled for all users. + + This check verifies that the domain-level policy disables the Google Sites + service, reducing the organization's attack surface by preventing users + from creating internal or external websites. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if sites_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=sites_client.policies, + resource_id="sitesPolicies", + resource_name="Sites Policies", + customer_id=sites_client.provider.identity.customer_id, + ) + + service_state = sites_client.policies.service_state + + if service_state == "DISABLED": + report.status = "PASS" + report.status_extended = ( + f"Google Sites service is disabled " + f"in domain {sites_client.provider.identity.domain}." + ) + else: + report.status = "FAIL" + if service_state is None: + report.status_extended = ( + f"Google Sites service is not explicitly configured " + f"in domain {sites_client.provider.identity.domain}. " + f"The default is ON for everyone. Google Sites should be disabled." + ) + else: + report.status_extended = ( + f"Google Sites service is enabled " + f"in domain {sites_client.provider.identity.domain}. " + f"Google Sites should be disabled." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/iac/iac_provider.py b/prowler/providers/iac/iac_provider.py index 5b3d898fb3..b91fdf070e 100644 --- a/prowler/providers/iac/iac_provider.py +++ b/prowler/providers/iac/iac_provider.py @@ -18,6 +18,10 @@ from prowler.config.config import ( from prowler.lib.check.models import CheckReportIAC from prowler.lib.logger import logger from prowler.lib.utils.utils import print_boxes +from prowler.lib.utils.vulnerability_references import ( + build_finding_reference_url, + resolve_vulnerability_reference_urls, +) from prowler.providers.common.models import Audit_Metadata, Connection from prowler.providers.common.provider import Provider @@ -189,14 +193,28 @@ class IacProvider(Provider): finding_id = finding["VulnerabilityID"] finding_description = finding["Description"] finding_status = finding.get("Status", "FAIL") + recommendation_url, additional_urls = ( + resolve_vulnerability_reference_urls( + vulnerability_id=finding_id, + references=finding.get("References"), + primary_url=finding.get("PrimaryURL", ""), + ) + ) + if not recommendation_url: + recommendation_url = build_finding_reference_url(finding_id) + additional_urls = [recommendation_url] elif "RuleID" in finding: finding_id = finding["RuleID"] finding_description = finding["Title"] finding_status = finding.get("Status", "FAIL") + recommendation_url = build_finding_reference_url(finding_id) + additional_urls = [recommendation_url] else: finding_id = finding["ID"] finding_description = finding["Description"] finding_status = finding["Status"] + recommendation_url = build_finding_reference_url(finding_id) + additional_urls = [recommendation_url] metadata_dict = { "Provider": "iac", @@ -210,7 +228,7 @@ class IacProvider(Provider): "ResourceType": "iac", "Description": finding_description, "Risk": "This provider has not defined a risk for this check.", - "RelatedUrl": finding.get("PrimaryURL", ""), + "RelatedUrl": "", "Remediation": { "Code": { "NativeIaC": "", @@ -220,11 +238,11 @@ class IacProvider(Provider): }, "Recommendation": { "Text": finding.get("Resolution", ""), - "Url": finding.get("PrimaryURL", ""), + "Url": recommendation_url, }, }, "Categories": [], - "AdditionalURLs": [], + "AdditionalURLs": additional_urls, "DependsOn": [], "RelatedTo": [], "Notes": "", diff --git a/prowler/providers/image/image_provider.py b/prowler/providers/image/image_provider.py index 07d02c04c9..b142240867 100644 --- a/prowler/providers/image/image_provider.py +++ b/prowler/providers/image/image_provider.py @@ -18,6 +18,9 @@ from prowler.config.config import ( from prowler.lib.check.models import CheckReportImage from prowler.lib.logger import logger from prowler.lib.utils.utils import print_boxes +from prowler.lib.utils.vulnerability_references import ( + resolve_vulnerability_reference_urls, +) from prowler.providers.common.models import Audit_Metadata, Connection from prowler.providers.common.provider import Provider from prowler.providers.image.exceptions.exceptions import ( @@ -395,6 +398,8 @@ class ImageProvider(Provider): """ try: # Determine finding ID and category based on type + recommendation_url = "" + additional_urls: list[str] = [] if "VulnerabilityID" in finding: finding_id = finding["VulnerabilityID"] finding_description = finding.get( @@ -402,17 +407,30 @@ class ImageProvider(Provider): ) finding_status = "FAIL" finding_categories = ["vulnerabilities"] + recommendation_url, additional_urls = ( + resolve_vulnerability_reference_urls( + vulnerability_id=finding_id, + references=finding.get("References"), + primary_url=finding.get("PrimaryURL", ""), + ) + ) elif "RuleID" in finding: # Secret finding finding_id = finding["RuleID"] finding_description = finding.get("Title", "Secret detected") finding_status = "FAIL" finding_categories = ["secrets"] + additional_urls = ( + [url] if (url := finding.get("PrimaryURL", "")) else [] + ) else: finding_id = finding.get("ID", "UNKNOWN") finding_description = finding.get("Description", "") finding_status = finding.get("Status", "FAIL") finding_categories = [] + additional_urls = ( + [url] if (url := finding.get("PrimaryURL", "")) else [] + ) # Build remediation text for vulnerabilities remediation_text = "" @@ -451,13 +469,11 @@ class ImageProvider(Provider): }, "Recommendation": { "Text": remediation_text, - "Url": "", + "Url": recommendation_url, }, }, "Categories": finding_categories, - "AdditionalURLs": ( - [url] if (url := finding.get("PrimaryURL", "")) else [] - ), + "AdditionalURLs": additional_urls, "DependsOn": [], "RelatedTo": [], "Notes": "", diff --git a/prowler/providers/m365/lib/powershell/m365_powershell.py b/prowler/providers/m365/lib/powershell/m365_powershell.py index 9cc7207f20..1bc8aa4075 100644 --- a/prowler/providers/m365/lib/powershell/m365_powershell.py +++ b/prowler/providers/m365/lib/powershell/m365_powershell.py @@ -104,8 +104,11 @@ class M365PowerShell(PowerShellSession): authentication information. Note: - The credentials are sanitized to prevent command injection and - stored securely in the PowerShell session. + ``client_id`` and ``tenant_id`` are sanitized via ``sanitize()`` since + they are UUIDs. ``client_secret`` is assigned with a single-quoted + string (escaping any embedded single quote as ``''``) following + PowerShell best practices for literal values, so its content is taken + verbatim with no variable expansion or subexpression evaluation. """ # Certificate Auth if credentials.certificate_content and credentials.client_id: @@ -135,9 +138,16 @@ class M365PowerShell(PowerShellSession): else: # Application Auth - self.execute(f'$clientID = "{credentials.client_id}"') - self.execute(f'$clientSecret = "{credentials.client_secret}"') - self.execute(f'$tenantID = "{credentials.tenant_id}"') + sanitized_client_id = self.sanitize(credentials.client_id) + sanitized_tenant_id = self.sanitize(credentials.tenant_id) + self.execute(f"$clientID = '{sanitized_client_id}'") + # Single-quoted strings are the PowerShell convention for literals: + # the content is taken verbatim with no variable expansion. Escape any + # embedded single quote as '' and do not sanitize() so the value is + # preserved exactly. + sanitized_secret = (credentials.client_secret or "").replace("'", "''") + self.execute(f"$clientSecret = '{sanitized_secret}'") + self.execute(f"$tenantID = '{sanitized_tenant_id}'") self.execute( '$graphtokenBody = @{ Grant_Type = "client_credentials"; Scope = "https://graph.microsoft.com/.default"; Client_Id = $clientID; Client_Secret = $clientSecret }' ) @@ -196,7 +206,7 @@ class M365PowerShell(PowerShellSession): """Test Exchange Online API connection and raise exception if it fails.""" try: self.execute( - '$SecureSecret = ConvertTo-SecureString "$clientSecret" -AsPlainText -Force' + "$SecureSecret = ConvertTo-SecureString $clientSecret -AsPlainText -Force" ) self.execute( '$exchangeToken = Get-MsalToken -clientID "$clientID" -tenantID "$tenantID" -clientSecret $SecureSecret -Scopes "https://outlook.office365.com/.default"' diff --git a/prowler/providers/m365/m365_provider.py b/prowler/providers/m365/m365_provider.py index 0e3a6c2b26..0454d85f3d 100644 --- a/prowler/providers/m365/m365_provider.py +++ b/prowler/providers/m365/m365_provider.py @@ -1073,7 +1073,7 @@ class M365Provider(Provider): organization_info = await client.organization.get() identity.tenant_id = organization_info.value[0].id - asyncio.get_event_loop().run_until_complete(get_m365_identity(identity)) + asyncio.run(get_m365_identity(identity)) return identity @staticmethod @@ -1261,9 +1261,7 @@ class M365Provider(Provider): result = await client.domains.get() return result.value - result = asyncio.get_event_loop().run_until_complete( - verify_certificate() - ) + result = asyncio.run(verify_certificate()) if not result: raise M365NotValidCertificateContentError( file=os.path.basename(__file__), @@ -1284,9 +1282,7 @@ class M365Provider(Provider): result = await client.domains.get() return result.value - result = asyncio.get_event_loop().run_until_complete( - verify_certificate() - ) + result = asyncio.run(verify_certificate()) if not result: raise M365NotValidCertificatePathError( file=os.path.basename(__file__), diff --git a/prowler/providers/m365/services/entra/entra_app_registration_client_secret_unused/__init__.py b/prowler/providers/m365/services/entra/entra_app_registration_client_secret_unused/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/entra/entra_app_registration_client_secret_unused/entra_app_registration_client_secret_unused.metadata.json b/prowler/providers/m365/services/entra/entra_app_registration_client_secret_unused/entra_app_registration_client_secret_unused.metadata.json new file mode 100644 index 0000000000..56e2b0ec5f --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_app_registration_client_secret_unused/entra_app_registration_client_secret_unused.metadata.json @@ -0,0 +1,42 @@ +{ + "Provider": "m365", + "CheckID": "entra_app_registration_client_secret_unused", + "CheckTitle": "Application registrations should not use client secrets", + "CheckType": [], + "ServiceName": "entra", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "IAM", + "Description": "Microsoft Entra **application registrations** should not have any **secret credentials** configured. This check audits the current state of every application registration and reports those that hold one or more **secrets**. Both **expired** and **active** secrets are reported, since expired entries are credential clutter that should be cleaned up.", + "Risk": "**Secrets** are bearer credentials that are easy to leak (committed to repositories, copied into CI variables, shared via chat). Once leaked, a **secret** can be used from anywhere on the internet until it is rotated. Both **expired** and **active** secrets increase the attack surface and represent credential clutter.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity-platform/certificate-credentials", + "https://learn.microsoft.com/en-us/graph/api/resources/passwordcredential?view=graph-rest-1.0", + "https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation", + "https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Navigate to Microsoft Entra admin center (https://entra.microsoft.com)\n2. Go to Applications > App registrations\n3. Select the flagged application\n4. Go to Certificates & secrets\n5. Delete all client secrets under the 'Client secrets' tab\n6. Add a certificate or configure federated identity credentials instead", + "Terraform": "" + }, + "Recommendation": { + "Text": "Remove every **secret** from the affected **application registrations** so they no longer rely on long-lived shared secrets. Enable the **default app management policy** to block new secrets from being added.", + "Url": "https://hub.prowler.com/check/entra_app_registration_client_secret_unused" + } + }, + "Categories": [ + "identity-access", + "e3" + ], + "DependsOn": [], + "RelatedTo": [ + "entra_default_app_management_policy_enabled" + ], + "Notes": "This check audits the current state of password credentials on app registrations. The related check entra_default_app_management_policy_enabled audits the tenant-wide policy that prevents new secrets from being added. Both expired and active password credentials trigger a FAIL." +} diff --git a/prowler/providers/m365/services/entra/entra_app_registration_client_secret_unused/entra_app_registration_client_secret_unused.py b/prowler/providers/m365/services/entra/entra_app_registration_client_secret_unused/entra_app_registration_client_secret_unused.py new file mode 100644 index 0000000000..a759eda75a --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_app_registration_client_secret_unused/entra_app_registration_client_secret_unused.py @@ -0,0 +1,58 @@ +from prowler.lib.check.models import Check, CheckReportM365 +from prowler.providers.m365.services.entra.entra_client import entra_client + + +class entra_app_registration_client_secret_unused(Check): + """ + Ensure that application registrations do not use password credentials (client secrets). + + This check evaluates application registrations in Microsoft Entra ID to identify + those with password credentials (client secrets). Applications should authenticate + using certificates, federated identity credentials, or managed identities instead. + Both expired and active password credentials are flagged, since expired entries are + credential clutter that should be cleaned up. + + - PASS: The application has no password credentials. + - FAIL: The application has one or more password credentials that should be removed. + """ + + def execute(self) -> list[CheckReportM365]: + findings = [] + + for app_id, app in entra_client.app_registrations.items(): + report = CheckReportM365( + metadata=self.metadata(), + resource=app, + resource_name=app.name or app.app_id, + resource_id=app_id, + ) + + num_secrets = len(app.password_credentials) + if num_secrets > 0: + report.status = "FAIL" + secret_details = [] + for cred in app.password_credentials: + detail = cred.display_name or cred.key_id + if cred.end_date_time: + detail += f" (expires: {cred.end_date_time})" + secret_details.append(detail) + + if num_secrets > 5: + displayed = ", ".join(secret_details[:5]) + displayed += f" (and {num_secrets - 5} more)" + else: + displayed = ", ".join(secret_details) + + report.status_extended = ( + f"App registration {app.name} has {num_secrets} " + f"password credential(s) (client secrets): {displayed}." + ) + else: + report.status = "PASS" + report.status_extended = ( + f"App registration {app.name} does not use password credentials." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/m365/services/entra/entra_break_glass_account_fido2_security_key_registered/entra_break_glass_account_fido2_security_key_registered.py b/prowler/providers/m365/services/entra/entra_break_glass_account_fido2_security_key_registered/entra_break_glass_account_fido2_security_key_registered.py index de973e9ffa..e90eed0023 100644 --- a/prowler/providers/m365/services/entra/entra_break_glass_account_fido2_security_key_registered/entra_break_glass_account_fido2_security_key_registered.py +++ b/prowler/providers/m365/services/entra/entra_break_glass_account_fido2_security_key_registered/entra_break_glass_account_fido2_security_key_registered.py @@ -85,6 +85,15 @@ class entra_break_glass_account_fido2_security_key_registered(Check): resource_id=user.id, ) + if entra_client.user_registration_details_error: + report.status = "FAIL" + report.status_extended = ( + f"Cannot verify FIDO2 security key registration for break glass account {user.name}: " + f"{entra_client.user_registration_details_error}." + ) + findings.append(report) + continue + auth_methods = set(user.authentication_methods) has_fido2 = "fido2SecurityKey" in auth_methods has_passkey_device_bound = "passKeyDeviceBound" in auth_methods diff --git a/prowler/providers/m365/services/entra/entra_emergency_access_exclusion/entra_emergency_access_exclusion.metadata.json b/prowler/providers/m365/services/entra/entra_emergency_access_exclusion/entra_emergency_access_exclusion.metadata.json index cd7efd39e3..7d594a4975 100644 --- a/prowler/providers/m365/services/entra/entra_emergency_access_exclusion/entra_emergency_access_exclusion.metadata.json +++ b/prowler/providers/m365/services/entra/entra_emergency_access_exclusion/entra_emergency_access_exclusion.metadata.json @@ -9,8 +9,8 @@ "Severity": "high", "ResourceType": "NotDefined", "ResourceGroup": "IAM", - "Description": "Microsoft Entra **Conditional Access** is verified to have at least one **emergency access** (break glass) account or group excluded from all policies. Emergency access accounts provide a fallback mechanism when normal administrative access is blocked due to misconfigured policies.", - "Risk": "Without emergency access accounts excluded from Conditional Access policies, a misconfiguration could lock out all administrators from the tenant. This creates a **critical availability risk** where legitimate administrators cannot access or remediate issues in the environment.", + "Description": "Microsoft Entra **Conditional Access** is verified to have at least one **emergency access** (break glass) account or group excluded from every enabled Conditional Access policy with a **Block** grant control. Emergency access accounts provide a fallback mechanism when normal administrative access is blocked due to misconfigured blocking policies.", + "Risk": "Without emergency access accounts excluded from every blocking Conditional Access policy, a misconfigured Block policy can lock out all administrators from the tenant. This creates a **critical availability risk** where legitimate administrators cannot access or remediate issues in the environment.", "RelatedUrl": "", "AdditionalURLs": [ "https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/security-emergency-access", @@ -20,11 +20,11 @@ "Code": { "CLI": "", "NativeIaC": "", - "Other": "1. Create dedicated emergency access accounts or a security group in Microsoft Entra admin center.\n2. Navigate to Protection > Conditional Access > Policies.\n3. For each Conditional Access policy, add the emergency access account or group to the exclusion list under Users > Exclude.\n4. Ensure the emergency accounts are protected with strong credentials and limited usage.", + "Other": "1. Create dedicated emergency access accounts or a security group in Microsoft Entra admin center.\n2. Navigate to Protection > Conditional Access > Policies.\n3. For every Conditional Access policy whose grant control is **Block**, add the emergency access account or group to the exclusion list under Users > Exclude.\n4. Ensure the emergency accounts are protected with strong credentials and limited usage.", "Terraform": "" }, "Recommendation": { - "Text": "Create and maintain at least two emergency access accounts that are excluded from all Conditional Access policies. Store credentials securely offline, monitor usage, and test access regularly. Follow **least privilege** principles for these accounts while ensuring they can recover tenant access when needed.", + "Text": "Create and maintain at least two emergency access accounts that are excluded from every Conditional Access policy with a **Block** grant control so they can never be denied access by a misconfiguration. Store credentials securely offline, monitor usage, and test access regularly. Follow **least privilege** principles for these accounts while ensuring they can recover tenant access when needed.", "Url": "https://hub.prowler.com/check/entra_emergency_access_exclusion" } }, diff --git a/prowler/providers/m365/services/entra/entra_emergency_access_exclusion/entra_emergency_access_exclusion.py b/prowler/providers/m365/services/entra/entra_emergency_access_exclusion/entra_emergency_access_exclusion.py index 45f9bda05d..28ad2a3378 100644 --- a/prowler/providers/m365/services/entra/entra_emergency_access_exclusion/entra_emergency_access_exclusion.py +++ b/prowler/providers/m365/services/entra/entra_emergency_access_exclusion/entra_emergency_access_exclusion.py @@ -3,87 +3,38 @@ from collections import Counter from prowler.lib.check.models import Check, CheckReportM365 from prowler.providers.m365.services.entra.entra_client import entra_client from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessGrantControl, ConditionalAccessPolicyState, ) class entra_emergency_access_exclusion(Check): - """Check if at least one emergency access account or group is excluded from all Conditional Access policies. + """Check that at least one emergency access account or group is excluded + from every enabled Conditional Access policy with a `Block` grant control. - This check ensures that the tenant has at least one emergency/break glass account - or account exclusion group that is excluded from all Conditional Access policies. - This prevents accidental lockout scenarios where misconfigured CA policies could - block all administrative access to the tenant. + Emergency access (break glass) accounts are, by definition, accounts that + cannot be blocked by Conditional Access. Membership of an account in the + exclusion list of every enabled blocking policy is therefore the necessary + condition for it to act as a true emergency account: if any enabled + blocking policy applies to it, a misconfiguration of that policy can lock + out the tenant. - - PASS: At least one user or group is excluded from all enabled Conditional Access policies, - or there are no enabled policies. - - FAIL: No user or group is excluded from all enabled Conditional Access policies. + - PASS: At least one user or group is excluded from every enabled + Conditional Access policy with a `Block` grant control, or no + enabled blocking Conditional Access policy exists. + - FAIL: One or more enabled blocking Conditional Access policies exist and + no user or group is excluded from all of them. """ def execute(self) -> list[CheckReportM365]: - """Execute the check for emergency access account exclusions. + """Execute the check for emergency access account exclusions from + blocking Conditional Access policies. Returns: list[CheckReportM365]: A list containing the result of the check. """ findings = [] - # Get all enabled CA policies (excluding disabled ones) - enabled_policies = [ - policy - for policy in entra_client.conditional_access_policies.values() - if policy.state != ConditionalAccessPolicyState.DISABLED - ] - - # If there are no enabled policies, there's nothing to exclude from - if not enabled_policies: - report = CheckReportM365( - metadata=self.metadata(), - resource={}, - resource_name="Conditional Access Policies", - resource_id="conditionalAccessPolicies", - ) - report.status = "PASS" - report.status_extended = "No enabled Conditional Access policies found. Emergency access exclusions are not required." - findings.append(report) - return findings - - total_policy_count = len(enabled_policies) - - # Count how many policies exclude each user - excluded_users_counter = Counter() - for policy in enabled_policies: - user_conditions = policy.conditions.user_conditions - if user_conditions: - for user_id in user_conditions.excluded_users: - excluded_users_counter[user_id] += 1 - - # Count how many policies exclude each group - excluded_groups_counter = Counter() - for policy in enabled_policies: - user_conditions = policy.conditions.user_conditions - if user_conditions: - for group_id in user_conditions.excluded_groups: - excluded_groups_counter[group_id] += 1 - - # Find users excluded from ALL policies - users_excluded_from_all = [ - user_id - for user_id, count in excluded_users_counter.items() - if count == total_policy_count - ] - - # Find groups excluded from ALL policies - groups_excluded_from_all = [ - group_id - for group_id, count in excluded_groups_counter.items() - if count == total_policy_count - ] - - has_emergency_exclusion = bool( - users_excluded_from_all or groups_excluded_from_all - ) - report = CheckReportM365( metadata=self.metadata(), resource={}, @@ -91,27 +42,67 @@ class entra_emergency_access_exclusion(Check): resource_id="conditionalAccessPolicies", ) - if has_emergency_exclusion: - report.status = "PASS" - exclusion_details = [] - if users_excluded_from_all: - user_names = [] - for user_id in users_excluded_from_all: - user = entra_client.users.get(user_id) - user_names.append(user.name if user else user_id) - exclusion_details.append(f"user(s): {', '.join(user_names)}") - if groups_excluded_from_all: - group_names = [] - groups_by_id = {g.id: g for g in entra_client.groups} - for group_id in groups_excluded_from_all: - group = groups_by_id.get(group_id) - group_names.append(group.name if group else group_id) - exclusion_details.append(f"group(s): {', '.join(group_names)}") - report.status_extended = f"Emergency access {' and '.join(exclusion_details)} excluded from all {total_policy_count} enabled Conditional Access policies." - else: - report.status = "FAIL" - report.status_extended = f"No user or group is excluded as emergency access from all {total_policy_count} enabled Conditional Access policies." + blocking_policies = [ + policy + for policy in entra_client.conditional_access_policies.values() + if policy.state != ConditionalAccessPolicyState.DISABLED + and ConditionalAccessGrantControl.BLOCK + in policy.grant_controls.built_in_controls + ] + if not blocking_policies: + report.status = "PASS" + report.status_extended = "No enabled Conditional Access policies with a Block grant control found. Emergency access exclusions are not required." + findings.append(report) + return findings + + total_blocking_count = len(blocking_policies) + + excluded_users_counter = Counter() + excluded_groups_counter = Counter() + for policy in blocking_policies: + user_conditions = policy.conditions.user_conditions + if not user_conditions: + continue + for user_id in user_conditions.excluded_users: + excluded_users_counter[user_id] += 1 + for group_id in user_conditions.excluded_groups: + excluded_groups_counter[group_id] += 1 + + emergency_user_ids = [ + user_id + for user_id, count in excluded_users_counter.items() + if count == total_blocking_count + ] + emergency_group_ids = [ + group_id + for group_id, count in excluded_groups_counter.items() + if count == total_blocking_count + ] + + if not (emergency_user_ids or emergency_group_ids): + report.status = "FAIL" + report.status_extended = f"No user or group is excluded as emergency access from all {total_blocking_count} enabled Conditional Access policies with a Block grant control." + findings.append(report) + return findings + + exclusion_details = [] + if emergency_user_ids: + user_names = [] + for uid in emergency_user_ids: + user = entra_client.users.get(uid) + user_names.append(user.name if user else uid) + exclusion_details.append(f"user(s): {', '.join(user_names)}") + if emergency_group_ids: + groups_by_id = {g.id: g for g in entra_client.groups} + group_names = [] + for gid in emergency_group_ids: + group = groups_by_id.get(gid) + group_names.append(group.name if group else gid) + exclusion_details.append(f"group(s): {', '.join(group_names)}") + + report.status = "PASS" + report.status_extended = f"Emergency access {' and '.join(exclusion_details)} excluded from all {total_blocking_count} enabled Conditional Access policies with a Block grant control." findings.append(report) return findings diff --git a/prowler/providers/m365/services/entra/entra_service.py b/prowler/providers/m365/services/entra/entra_service.py index a6b8451387..a09ee32da0 100644 --- a/prowler/providers/m365/services/entra/entra_service.py +++ b/prowler/providers/m365/services/entra/entra_service.py @@ -1,14 +1,17 @@ import asyncio import json from asyncio import gather +from datetime import datetime, timezone from enum import Enum -from typing import Dict, List, Optional +from typing import Any, Dict, List, Optional, Tuple from uuid import UUID +from kiota_abstractions.base_request_configuration import RequestConfiguration from msgraph.generated.models.o_data_errors.o_data_error import ODataError from msgraph.generated.security.microsoft_graph_security_run_hunting_query.run_hunting_query_post_request_body import ( RunHuntingQueryPostRequestBody, ) +from msgraph.generated.users.users_request_builder import UsersRequestBuilder from pydantic.v1 import BaseModel, validator from prowler.lib.logger import logger @@ -36,6 +39,7 @@ class Entra(M365Service): user_accounts_status (dict): Dictionary of user account statuses. oauth_apps (dict): Dictionary of OAuth applications from Defender XDR. authentication_method_configurations (dict): Dictionary of authentication method configurations. + service_principals (dict): Dictionary of service principals with credentials and role assignments. """ def __init__(self, provider: M365Provider): @@ -71,6 +75,8 @@ class Entra(M365Service): ) self.tenant_domain = provider.identity.tenant_domain + self.tenant_id = getattr(provider.identity, "tenant_id", None) + self.user_registration_details_error: Optional[str] = None attributes = loop.run_until_complete( gather( self._get_authorization_policy(), @@ -83,6 +89,8 @@ class Entra(M365Service): self._get_oauth_apps(), self._get_directory_sync_settings(), self._get_authentication_method_configurations(), + self._get_service_principals(), + self._get_app_registrations(), ) ) @@ -98,6 +106,8 @@ class Entra(M365Service): self.authentication_method_configurations: Dict[ str, AuthenticationMethodConfiguration ] = attributes[9] + self.service_principals: Dict[str, "ServicePrincipal"] = attributes[10] + self.app_registrations: Dict[str, "AppRegistration"] = attributes[11] self.user_accounts_status = {} if created_loop: @@ -806,7 +816,29 @@ class Entra(M365Service): logger.info("Entra - Getting users...") users = {} try: - users_response = await self.client.users.get() + # Microsoft Graph's /users endpoint omits accountEnabled, userType and + # onPremisesSyncEnabled from the default property set, so we must request + # them explicitly via $select. Without this, disabled guest users surface + # as account_enabled=True (Pydantic default) and user_type=None, which + # bypasses the guest/disabled filters in checks like + # entra_users_mfa_capable (CIS 5.2.3.4). See issue #10921. + query_parameters = ( + UsersRequestBuilder.UsersRequestBuilderGetQueryParameters( + select=[ + "id", + "displayName", + "userType", + "accountEnabled", + "onPremisesSyncEnabled", + ], + ) + ) + request_configuration = RequestConfiguration( + query_parameters=query_parameters, + ) + users_response = await self.client.users.get( + request_configuration=request_configuration, + ) directory_roles = await self.client.directory_roles.get() async def fetch_role_members(directory_role): @@ -825,11 +857,26 @@ class Entra(M365Service): for member in members: user_roles_map.setdefault(member.id, []).append(role_template_id) - registration_details = await self._get_user_registration_details() + registration_details, self.user_registration_details_error = ( + await self._get_user_registration_details() + ) while users_response: for user in getattr(users_response, "value", []) or []: reg_info = registration_details.get(user.id, {}) + # Prefer Microsoft Graph as the source of truth for + # accountEnabled: it covers every directory user including + # guests, whereas EXO's Get-User only returns mail-enabled + # accounts and silently drops disabled guests. Fall back to + # the EXO PowerShell value only when Graph does not return a + # value (e.g. older tenants or permission-restricted reads). + graph_account_enabled = getattr(user, "account_enabled", None) + if graph_account_enabled is None: + account_enabled = not self.user_accounts_status.get( + user.id, {} + ).get("AccountDisabled", False) + else: + account_enabled = bool(graph_account_enabled) users[user.id] = User( id=user.id, name=user.display_name, @@ -838,9 +885,7 @@ class Entra(M365Service): ), directory_roles_ids=user_roles_map.get(user.id, []), is_mfa_capable=reg_info.get("is_mfa_capable", False), - account_enabled=not self.user_accounts_status.get( - user.id, {} - ).get("AccountDisabled", False), + account_enabled=account_enabled, authentication_methods=reg_info.get( "authentication_methods", [] ), @@ -857,18 +902,24 @@ class Entra(M365Service): ) return users - async def _get_user_registration_details(self): + async def _get_user_registration_details( + self, + ) -> Tuple[Dict[str, Dict[str, Any]], Optional[str]]: """Retrieve user authentication method registration details. Fetches registration details from the Microsoft Graph API, including MFA capability and the specific authentication methods each user has registered. Returns: - dict: A dictionary mapping user IDs to their registration details, - where each value is a dict with 'is_mfa_capable' (bool) and - 'authentication_methods' (list of str). + A tuple containing: + - A dictionary mapping user IDs to their registration details, + where each value is a dict with 'is_mfa_capable' (bool) and + 'authentication_methods' (list of str), or an empty dict if + retrieval fails. + - An error message string if there was an access error, None otherwise. """ registration_details = {} + error_message = None try: registration_builder = ( self.client.reports.authentication_methods.user_registration_details @@ -893,16 +944,25 @@ class Entra(M365Service): next_link ).get() - except Exception as error: - if ( - error.__class__.__name__ == "ODataError" - and error.__dict__.get("response_status_code", None) == 403 - ): + except ODataError as error: + error_code = getattr(error.error, "code", None) if error.error else None + if error_code == "Authorization_RequestDenied": + error_message = "Insufficient privileges to read user registration details. Required permission: AuditLog.Read.All" + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error_message}" + ) + else: logger.error( f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) + error_message = str(error) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + error_message = f"Failed to retrieve user registration details: {error}" - return registration_details + return registration_details, error_message async def _get_oauth_apps(self) -> Optional[Dict[str, "OAuthApp"]]: """ @@ -1055,6 +1115,207 @@ OAuthAppInfo ) return authentication_method_configurations + async def _get_service_principals(self): + """Retrieve service principals owned by the audited tenant. + + Fetches all service principals from Microsoft Graph and keeps only the + ones whose ``appOwnerOrganizationId`` matches the audited tenant. Skips + Microsoft first-party service principals and multi-tenant ISV apps + consented from other publishers: their credentials live in the + publisher's tenant, not this one, so they are out of scope for any + check that evaluates secret hygiene or role assignments managed by the + customer. + + Returns: + Dict[str, ServicePrincipal]: Customer-owned service principals + keyed by service principal ID. + """ + logger.info("Entra - Getting service principals...") + service_principals = {} + tenant_id_normalized = str(self.tenant_id).lower() if self.tenant_id else None + try: + sp_response = await self.client.service_principals.get() + + # Build a map of service principal IDs to their data + while sp_response: + for sp in getattr(sp_response, "value", []) or []: + raw_owner = getattr(sp, "app_owner_organization_id", None) + app_owner_org_id = str(raw_owner).lower() if raw_owner else None + if ( + tenant_id_normalized + and app_owner_org_id != tenant_id_normalized + ): + # Skip Microsoft first-party SPs and consented + # multi-tenant ISV apps; the customer cannot manage + # their credentials. + continue + + password_credentials = [] + for cred in getattr(sp, "password_credentials", []) or []: + password_credentials.append( + PasswordCredential( + key_id=str(getattr(cred, "key_id", "")), + display_name=getattr(cred, "display_name", None), + end_date_time=getattr(cred, "end_date_time", None), + ) + ) + + key_credentials = [] + for cred in getattr(sp, "key_credentials", []) or []: + key_credentials.append( + KeyCredential( + key_id=str(getattr(cred, "key_id", "")), + display_name=getattr(cred, "display_name", None), + ) + ) + + service_principals[sp.id] = ServicePrincipal( + id=sp.id, + name=getattr(sp, "display_name", "") or "", + app_id=getattr(sp, "app_id", "") or "", + app_owner_organization_id=app_owner_org_id, + password_credentials=password_credentials, + key_credentials=key_credentials, + ) + + next_link = getattr(sp_response, "odata_next_link", None) + if not next_link: + break + sp_response = await self.client.service_principals.with_url( + next_link + ).get() + + # Fold in credentials registered on the parent Application objects. + # Microsoft Graph stores secrets and certificates added through + # "Certificates & secrets" on /applications, not on the service + # principal itself, so /servicePrincipals.passwordCredentials is + # almost always empty for normal app registrations. Joining via + # appId is required for the check to see those credentials. + # + # Index service principals by app_id once so the join below is + # O(N+M) instead of scanning all SPs for every Application page. + service_principals_by_app_id = { + sp.app_id: sp for sp in service_principals.values() if sp.app_id + } + app_response = await self.client.applications.get() + while app_response: + for app in getattr(app_response, "value", []) or []: + app_id = getattr(app, "app_id", None) + if not app_id: + continue + target_sp = service_principals_by_app_id.get(app_id) + if target_sp is None: + continue + + for cred in getattr(app, "password_credentials", []) or []: + target_sp.password_credentials.append( + PasswordCredential( + key_id=str(getattr(cred, "key_id", "")), + display_name=getattr(cred, "display_name", None), + end_date_time=getattr(cred, "end_date_time", None), + ) + ) + for cred in getattr(app, "key_credentials", []) or []: + target_sp.key_credentials.append( + KeyCredential( + key_id=str(getattr(cred, "key_id", "")), + display_name=getattr(cred, "display_name", None), + ) + ) + + next_link = getattr(app_response, "odata_next_link", None) + if not next_link: + break + app_response = await self.client.applications.with_url(next_link).get() + + # Identify permanent Tier 0 directory role assignments via the unified + # role management endpoint. ``directoryRoles/{id}/members`` mixes + # permanent direct assignments with PIM-activated temporary ones, so + # using it would mark just-in-time elevations as "permanent" and emit + # false positives. ``roleManagement/directory/roleAssignments`` + # exposes only the durable, statically-assigned principals, which is + # exactly what the Tier 0 check needs. + role_assignments_response = ( + await self.client.role_management.directory.role_assignments.get() + ) + while role_assignments_response: + for assignment in getattr(role_assignments_response, "value", []) or []: + principal_id = getattr(assignment, "principal_id", None) + role_definition_id = getattr(assignment, "role_definition_id", None) + if ( + principal_id in service_principals + and role_definition_id in TIER_0_ROLE_TEMPLATE_IDS + ): + service_principals[ + principal_id + ].directory_role_template_ids.append(role_definition_id) + + next_link = getattr(role_assignments_response, "odata_next_link", None) + if not next_link: + break + role_assignments_response = await self.client.role_management.directory.role_assignments.with_url( + next_link + ).get() + + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return service_principals + + async def _get_app_registrations(self) -> Dict[str, "AppRegistration"]: + """Retrieve application registrations from Microsoft Entra. + + Fetches every application object and its password credentials (client + secrets) across all pages. Customer-owned applications should + authenticate using certificates, federated identity credentials, or + managed identities, so any entry in ``passwordCredentials`` is reported + by the related check. + + Returns: + Dict[str, AppRegistration]: Application registrations keyed by the + application object ID. + """ + logger.info("Entra - Getting app registrations...") + app_registrations: Dict[str, AppRegistration] = {} + try: + app_response = await self.client.applications.get() + while app_response: + for app in getattr(app_response, "value", []) or []: + app_id = getattr(app, "app_id", None) + object_id = getattr(app, "id", None) + if not app_id or not object_id: + continue + + password_credentials = [] + for cred in getattr(app, "password_credentials", []) or []: + password_credentials.append( + PasswordCredential( + key_id=str(getattr(cred, "key_id", "")), + display_name=getattr(cred, "display_name", None), + start_date_time=getattr(cred, "start_date_time", None), + end_date_time=getattr(cred, "end_date_time", None), + ) + ) + + app_registrations[object_id] = AppRegistration( + id=object_id, + app_id=app_id, + name=getattr(app, "display_name", "") or "", + password_credentials=password_credentials, + ) + + next_link = getattr(app_response, "odata_next_link", None) + if not next_link: + break + app_response = await self.client.applications.with_url(next_link).get() + + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return app_registrations + class ConditionalAccessPolicyState(Enum): ENABLED = "enabled" @@ -1486,3 +1747,114 @@ class OAuthApp(BaseModel): is_admin_consented: bool = False last_used_time: Optional[str] = None app_origin: str = "" + + +class PasswordCredential(BaseModel): + """Model representing a password credential (client secret) on a service principal. + + Attributes: + key_id: The unique identifier of the credential. + display_name: The optional display name of the credential. + start_date_time: The time at which the credential becomes valid. + ``None`` when the API does not report it. + end_date_time: The expiration time of the credential. ``None`` indicates + the secret has no recorded expiry and is treated as active. + """ + + key_id: str + display_name: Optional[str] = None + start_date_time: Optional[datetime] = None + end_date_time: Optional[datetime] = None + + def is_active(self, now: Optional[datetime] = None) -> bool: + """Return ``True`` when the credential has not expired. + + A credential with no ``end_date_time`` is assumed to be active, matching + the behavior of the Microsoft Graph API when the field is omitted. + """ + if self.end_date_time is None: + return True + reference = now or datetime.now(timezone.utc) + return self.end_date_time > reference + + +class KeyCredential(BaseModel): + """Model representing a key credential (certificate) on a service principal. + + Attributes: + key_id: The unique identifier of the credential. + display_name: The optional display name of the credential. + """ + + key_id: str + display_name: Optional[str] = None + + +# Control Plane (Tier 0) role template IDs. +# +# Roles included grant tenant-wide control over identity, authentication, or the +# directory itself, so a credential compromise on any of them is equivalent to a +# tenant takeover. References: +# https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/privileged-roles-permissions +# https://learn.microsoft.com/en-us/security/privileged-access-workstations/privileged-access-access-model +TIER_0_ROLE_TEMPLATE_IDS = { + "62e90394-69f5-4237-9190-012177145e10", # Global Administrator + "e8611ab8-c189-46e8-94e1-60213ab1f814", # Privileged Role Administrator + "7be44c8a-adaf-4e2a-84d6-ab2649e08a13", # Privileged Authentication Administrator + "9b895d92-2cd3-44c7-9d02-a6ac2d5ea5c3", # Application Administrator + "158c047a-c907-4556-b7ef-446551a6b5f7", # Cloud Application Administrator + "c4e39bd9-1100-46d3-8c65-fb160da0071f", # Authentication Administrator + "0526716b-113d-4c15-b2c8-68e3c22b9f80", # Authentication Policy Administrator + "b1be1c3e-b65d-4f19-8427-f6fa0d97feb9", # Conditional Access Administrator + "8329153b-31d0-4727-b945-745eb3bc5f31", # Domain Name Administrator + "be2f45a1-457d-42af-a067-6ec1fa63bc45", # External Identity Provider Administrator + "8ac3fc64-6eca-42ea-9e69-59f4c7b60eb2", # Hybrid Identity Administrator + "194ae4cb-b126-40b2-bd5b-6091b380977d", # Security Administrator + "fe930be7-5e62-47db-91af-98c3a49a38b1", # User Administrator + "d29b2b05-8046-44ba-8758-1e26182fcf32", # Directory Synchronization Accounts + "e00e864a-17c5-4a4b-9c06-f5b95a8d5bd8", # Partner Tier2 Support +} + + +class ServicePrincipal(BaseModel): + """Model representing a Microsoft Entra ID service principal. + + Attributes: + id: The service principal's unique identifier. + name: The service principal's display name. + app_id: The application ID associated with the service principal. + app_owner_organization_id: Tenant ID of the application's publisher. + For customer-owned apps this matches the audited tenant; the + service-layer fetch uses this to filter out Microsoft first-party + and third-party multi-tenant service principals that the customer + cannot manage credentials for. + password_credentials: List of password credentials (client secrets). + key_credentials: List of key credentials (certificates). + directory_role_template_ids: List of directory role template IDs permanently + assigned to this service principal. + """ + + id: str + name: str + app_id: str = "" + app_owner_organization_id: Optional[str] = None + password_credentials: List[PasswordCredential] = [] + key_credentials: List[KeyCredential] = [] + directory_role_template_ids: List[str] = [] + + +class AppRegistration(BaseModel): + """Model representing a Microsoft Entra ID application registration. + + Attributes: + id: The application object's unique identifier. + app_id: The application (client) ID. + name: The application's display name. + password_credentials: List of password credentials (client secrets) + registered on the application. + """ + + id: str + app_id: str = "" + name: str = "" + password_credentials: List[PasswordCredential] = [] diff --git a/prowler/providers/m365/services/entra/entra_service_principal_no_secrets_for_permanent_tier0_roles/__init__.py b/prowler/providers/m365/services/entra/entra_service_principal_no_secrets_for_permanent_tier0_roles/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/entra/entra_service_principal_no_secrets_for_permanent_tier0_roles/entra_service_principal_no_secrets_for_permanent_tier0_roles.metadata.json b/prowler/providers/m365/services/entra/entra_service_principal_no_secrets_for_permanent_tier0_roles/entra_service_principal_no_secrets_for_permanent_tier0_roles.metadata.json new file mode 100644 index 0000000000..477017c458 --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_service_principal_no_secrets_for_permanent_tier0_roles/entra_service_principal_no_secrets_for_permanent_tier0_roles.metadata.json @@ -0,0 +1,38 @@ +{ + "Provider": "m365", + "CheckID": "entra_service_principal_no_secrets_for_permanent_tier0_roles", + "CheckTitle": "Secure credential management prevents client secret usage for service principals with permanent Tier 0 roles", + "CheckType": [], + "ServiceName": "entra", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "critical", + "ResourceType": "NotDefined", + "ResourceGroup": "IAM", + "Description": "Microsoft Entra **service principals** with permanent assignments to **Control Plane (Tier 0)** directory roles are evaluated for the use of **client secrets** (password credentials) instead of more secure authentication methods such as certificates or managed identities.", + "Risk": "A service principal authenticating with a **client secret** while holding a **Tier 0** role creates a high-impact credential theft path. Leaked or brute-forced secrets grant immediate control-plane access, enabling tenant-wide privilege escalation, security control bypass, data exfiltration, and persistent backdoor creation—impacting **confidentiality**, **integrity**, and **availability**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/security-planning#prefer-certificate-credentials", + "https://learn.microsoft.com/en-us/entra/architecture/security-operations-applications#application-credentials" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the Microsoft Entra admin center (https://entra.microsoft.com)\n2. Go to Identity > Applications > App registrations > select the application\n3. Under Certificates & secrets, remove all client secrets\n4. Under Certificates & secrets > Certificates, upload a certificate for authentication\n5. Alternatively, migrate to a managed identity where possible", + "Terraform": "" + }, + "Recommendation": { + "Text": "Replace **client secrets** with **certificates** or **managed identities** for service principals holding Control Plane roles. Apply **least privilege** by removing unnecessary Tier 0 assignments. Use **Privileged Identity Management (PIM)** for just-in-time eligible assignments instead of permanent ones. Rotate credentials regularly and monitor sign-in logs for anomalies.", + "Url": "https://hub.prowler.com/check/entra_service_principal_no_secrets_for_permanent_tier0_roles" + } + }, + "Categories": [ + "identity-access", + "secrets" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Tier 0 roles evaluated follow the Microsoft Entra Control Plane classification, including Global Administrator, Privileged Role Administrator, and Privileged Authentication Administrator." +} diff --git a/prowler/providers/m365/services/entra/entra_service_principal_no_secrets_for_permanent_tier0_roles/entra_service_principal_no_secrets_for_permanent_tier0_roles.py b/prowler/providers/m365/services/entra/entra_service_principal_no_secrets_for_permanent_tier0_roles/entra_service_principal_no_secrets_for_permanent_tier0_roles.py new file mode 100644 index 0000000000..4d5955456c --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_service_principal_no_secrets_for_permanent_tier0_roles/entra_service_principal_no_secrets_for_permanent_tier0_roles.py @@ -0,0 +1,81 @@ +"""Check for service principals using client secrets with permanent Tier 0 role assignments.""" + +from typing import List + +from prowler.lib.check.models import Check, CheckReportM365 +from prowler.providers.m365.services.entra.entra_client import entra_client +from prowler.providers.m365.services.entra.entra_service import TIER_0_ROLE_TEMPLATE_IDS + + +class entra_service_principal_no_secrets_for_permanent_tier0_roles(Check): + """ + Service principal with permanent Control Plane role does not use client secrets. + + This check evaluates whether service principals that hold permanent assignments + to Tier 0 (Control Plane) directory roles authenticate using client secrets + instead of more secure alternatives such as certificates or managed identities. + + - PASS: The service principal does not use client secrets or does not hold a + permanent Tier 0 directory role assignment. + - FAIL: The service principal uses client secrets and has a permanent assignment + to at least one Tier 0 directory role. + """ + + def execute(self) -> List[CheckReportM365]: + """Execute the service principal secret management check. + + Iterates over service principals and identifies those that combine password + credentials (client secrets) with permanent Tier 0 directory role assignments. + + Returns: + A list of reports containing the result of the check for each service principal. + """ + findings = [] + + for sp in entra_client.service_principals.values(): + report = CheckReportM365( + metadata=self.metadata(), + resource=sp, + resource_name=sp.name, + resource_id=sp.id, + ) + + active_secrets = [ + credential + for credential in sp.password_credentials + if credential.is_active() + ] + has_secrets = len(active_secrets) > 0 + tier0_roles = [ + role_id + for role_id in sp.directory_role_template_ids + if role_id in TIER_0_ROLE_TEMPLATE_IDS + ] + + if has_secrets and tier0_roles: + report.status = "FAIL" + report.status_extended = ( + f"Service principal '{sp.name}' uses client secrets and has " + f"permanent assignment to {len(tier0_roles)} Control Plane " + f"(Tier 0) directory role(s)." + ) + else: + report.status = "PASS" + if not has_secrets and not tier0_roles: + report.status_extended = ( + f"Service principal '{sp.name}' does not use client secrets " + f"and has no Tier 0 directory role assignments." + ) + elif not has_secrets: + report.status_extended = ( + f"Service principal '{sp.name}' does not use client secrets." + ) + else: + report.status_extended = ( + f"Service principal '{sp.name}' has no permanent Tier 0 " + f"directory role assignments." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/m365/services/entra/entra_users_mfa_capable/entra_users_mfa_capable.py b/prowler/providers/m365/services/entra/entra_users_mfa_capable/entra_users_mfa_capable.py index 58a38d14d5..9485676d30 100644 --- a/prowler/providers/m365/services/entra/entra_users_mfa_capable/entra_users_mfa_capable.py +++ b/prowler/providers/m365/services/entra/entra_users_mfa_capable/entra_users_mfa_capable.py @@ -13,6 +13,10 @@ class entra_users_mfa_capable(Check): ("Ensure all member users are 'MFA capable'"). Guest users and disabled accounts are excluded from the evaluation. + + - PASS: The member user is MFA capable. + - FAIL: The member user is not MFA capable, or MFA capability cannot be + verified due to insufficient permissions to read user registration details. """ def execute(self) -> List[CheckReportM365]: @@ -42,7 +46,13 @@ class entra_users_mfa_capable(Check): resource_id=user.id, ) - if not user.is_mfa_capable: + if entra_client.user_registration_details_error: + report.status = "FAIL" + report.status_extended = ( + f"Cannot verify MFA capability for user {user.name}: " + f"{entra_client.user_registration_details_error}." + ) + elif not user.is_mfa_capable: report.status = "FAIL" report.status_extended = f"User {user.name} is not MFA capable." else: diff --git a/prowler/providers/okta/__init__.py b/prowler/providers/okta/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/okta/exceptions/__init__.py b/prowler/providers/okta/exceptions/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/okta/exceptions/exceptions.py b/prowler/providers/okta/exceptions/exceptions.py new file mode 100644 index 0000000000..04dd71bef0 --- /dev/null +++ b/prowler/providers/okta/exceptions/exceptions.py @@ -0,0 +1,123 @@ +from prowler.exceptions.exceptions import ProwlerException + + +# Exceptions codes from 14000 to 14999 are reserved for Okta exceptions +class OktaBaseException(ProwlerException): + """Base class for Okta Errors.""" + + OKTA_ERROR_CODES = { + (14000, "OktaEnvironmentVariableError"): { + "message": "Okta environment variable error", + "remediation": "Check the Okta environment variables and ensure they are properly set.", + }, + (14001, "OktaSetUpSessionError"): { + "message": "Error setting up Okta session", + "remediation": "Check the OAuth credentials (org URL, client ID, private key, scopes) and ensure they are properly configured.", + }, + (14002, "OktaSetUpIdentityError"): { + "message": "Okta identity setup error due to bad credentials", + "remediation": "Check the OAuth credentials and confirm the service app has been granted the required read scopes.", + }, + (14003, "OktaInvalidCredentialsError"): { + "message": "Okta credentials are not valid", + "remediation": "Check the client ID and private key for the Okta service app.", + }, + (14004, "OktaInvalidOrgDomainError"): { + "message": "Okta organization domain is not valid", + "remediation": "Provide an Okta-managed domain such as .okta.com (or .oktapreview.com / .okta-emea.com / .okta-gov.com / .okta.mil / .okta-miltest.com / .trex-govcloud.com), with no scheme and no trailing slash.", + }, + (14005, "OktaPrivateKeyFileError"): { + "message": "Okta private key file could not be read", + "remediation": "Check the file path and permissions, and ensure the file contains a PEM-encoded RSA key or a JWK JSON document.", + }, + (14006, "OktaInsufficientPermissionsError"): { + "message": "Okta service app is missing required scopes", + "remediation": "Have a Super Admin grant the required *.read scopes to the service app and assign the Read-Only Administrator role.", + }, + (14007, "OktaInvalidProviderIdError"): { + "message": "The provided provider_id does not match the credentials org domain", + "remediation": "Check the provider_id (Okta org domain) and ensure it matches the org the service app credentials were issued for.", + }, + } + + def __init__(self, code, file=None, original_exception=None, message=None): + provider = "Okta" + error_info = self.OKTA_ERROR_CODES.get((code, self.__class__.__name__)) + if error_info is None: + error_info = { + "message": message or "Unknown Okta error.", + "remediation": "Check the Okta API documentation for more details.", + } + elif message: + error_info = error_info.copy() + error_info["message"] = message + super().__init__( + code=code, + source=provider, + file=file, + original_exception=original_exception, + error_info=error_info, + ) + + +class OktaCredentialsError(OktaBaseException): + """Base class for Okta credentials errors.""" + + def __init__(self, code, file=None, original_exception=None, message=None): + super().__init__(code, file, original_exception, message) + + +class OktaEnvironmentVariableError(OktaCredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 14000, file=file, original_exception=original_exception, message=message + ) + + +class OktaSetUpSessionError(OktaCredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 14001, file=file, original_exception=original_exception, message=message + ) + + +class OktaSetUpIdentityError(OktaCredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 14002, file=file, original_exception=original_exception, message=message + ) + + +class OktaInvalidCredentialsError(OktaCredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 14003, file=file, original_exception=original_exception, message=message + ) + + +class OktaInvalidOrgDomainError(OktaCredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 14004, file=file, original_exception=original_exception, message=message + ) + + +class OktaPrivateKeyFileError(OktaCredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 14005, file=file, original_exception=original_exception, message=message + ) + + +class OktaInsufficientPermissionsError(OktaCredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 14006, file=file, original_exception=original_exception, message=message + ) + + +class OktaInvalidProviderIdError(OktaCredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 14007, file=file, original_exception=original_exception, message=message + ) diff --git a/prowler/providers/okta/lib/__init__.py b/prowler/providers/okta/lib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/okta/lib/arguments/__init__.py b/prowler/providers/okta/lib/arguments/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/okta/lib/arguments/arguments.py b/prowler/providers/okta/lib/arguments/arguments.py new file mode 100644 index 0000000000..287f786b0f --- /dev/null +++ b/prowler/providers/okta/lib/arguments/arguments.py @@ -0,0 +1,43 @@ +def init_parser(self): + """Init the Okta Provider CLI parser. + + The Okta provider authenticates with OAuth 2.0 (private-key JWT). The + private key is intentionally not exposed as a CLI flag — secrets must + be supplied via the `OKTA_PRIVATE_KEY` or `OKTA_PRIVATE_KEY_FILE` + environment variable. Non-secret values (org URL, client ID, scopes) + are flag-configurable. + """ + okta_parser = self.subparsers.add_parser( + "okta", parents=[self.common_providers_parser], help="Okta Provider" + ) + okta_auth_subparser = okta_parser.add_argument_group("Authentication") + okta_auth_subparser.add_argument( + "--okta-org-domain", + nargs="?", + help=( + "Okta organization domain (e.g. acme.okta.com). Must be an " + "Okta-managed domain (.okta.com / .oktapreview.com / " + ".okta-emea.com / .okta-gov.com / .okta.mil / " + ".okta-miltest.com / .trex-govcloud.com), without scheme or path." + ), + default=None, + metavar="OKTA_ORG_DOMAIN", + ) + okta_auth_subparser.add_argument( + "--okta-client-id", + nargs="?", + help="Okta service app Client ID for OAuth 2.0 (private-key JWT)", + default=None, + metavar="OKTA_CLIENT_ID", + ) + okta_auth_subparser.add_argument( + "--okta-scopes", + nargs="+", + help=( + "OAuth scopes to request, space-separated " + "(e.g. okta.policies.read okta.brands.read okta.apps.read). " + "Defaults to the read scopes required by the bundled checks." + ), + default=None, + metavar="OKTA_SCOPES", + ) diff --git a/prowler/providers/okta/lib/mutelist/__init__.py b/prowler/providers/okta/lib/mutelist/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/okta/lib/mutelist/mutelist.py b/prowler/providers/okta/lib/mutelist/mutelist.py new file mode 100644 index 0000000000..26afeb30fc --- /dev/null +++ b/prowler/providers/okta/lib/mutelist/mutelist.py @@ -0,0 +1,14 @@ +from prowler.lib.check.models import CheckReportOkta +from prowler.lib.mutelist.mutelist import Mutelist +from prowler.lib.outputs.utils import unroll_dict, unroll_tags + + +class OktaMutelist(Mutelist): + def is_finding_muted(self, finding: CheckReportOkta, org_domain: str) -> bool: + return self.is_muted( + org_domain, + finding.check_metadata.CheckID, + "*", + finding.resource_name, + unroll_dict(unroll_tags(finding.resource_tags)), + ) diff --git a/prowler/providers/okta/lib/service/__init__.py b/prowler/providers/okta/lib/service/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/okta/lib/service/service.py b/prowler/providers/okta/lib/service/service.py new file mode 100644 index 0000000000..baaabcb219 --- /dev/null +++ b/prowler/providers/okta/lib/service/service.py @@ -0,0 +1,34 @@ +import asyncio +from typing import TYPE_CHECKING + +from okta.client import Client as OktaSDKClient + +from prowler.providers.okta.models import OktaSession + +if TYPE_CHECKING: + from prowler.providers.okta.okta_provider import OktaProvider + + +class OktaService: + """Base class for Okta service implementations. + + Wraps the async okta-sdk-python `Client` so that subclasses can stay + synchronous like the other Prowler providers. The SDK auto-refreshes + the OAuth access token; nothing to manage here. + """ + + def __init__(self, service: str, provider: "OktaProvider"): + self.provider = provider + self.service = service + self.client = self.__set_client__(provider.session) + self.audit_config = provider.audit_config + self.fixer_config = provider.fixer_config + + @staticmethod + def __set_client__(session: OktaSession) -> OktaSDKClient: + return OktaSDKClient(session.to_sdk_config()) + + @staticmethod + def _run(coro): + """Run an okta-sdk-python coroutine from synchronous code.""" + return asyncio.run(coro) diff --git a/prowler/providers/okta/models.py b/prowler/providers/okta/models.py new file mode 100644 index 0000000000..b3f5c1f1cc --- /dev/null +++ b/prowler/providers/okta/models.py @@ -0,0 +1,54 @@ +from pydantic import BaseModel + +from prowler.config.config import output_file_timestamp +from prowler.providers.common.models import ProviderOutputOptions + + +class OktaSession(BaseModel): + org_domain: str + client_id: str + scopes: list[str] + private_key: str + + def to_sdk_config(self) -> dict: + # Shared by the credential probe (OktaProvider.setup_identity) and + # the service-level client (OktaService.__set_client__). Keeping the + # builder in one place stops the two SDK config dicts from drifting. + # The Okta SDK expects a fully-qualified `orgUrl`; we build it from + # the validated domain so user input stays scheme-free. + # DPoP proofs are sent on every token request — required by tenants + # with "Demonstrating Proof of Possession" enabled on the service + # app (or org-wide), harmless on tenants that don't. + return { + "orgUrl": f"https://{self.org_domain}", + "authorizationMode": "PrivateKey", + "clientId": self.client_id, + "scopes": self.scopes, + "privateKey": self.private_key, + "dpopEnabled": True, + } + + +class OktaIdentityInfo(BaseModel): + org_domain: str + client_id: str + # Scopes actually granted in the access token (`scp` claim). Used by + # services to distinguish "no data" from "no permission" so checks can + # surface the missing scope rather than a misleading FAIL. Empty when + # decoding the token was not possible — callers must treat empty as + # "unknown" and fall back to attempting the API call. + granted_scopes: list[str] = [] + + +class OktaOutputOptions(ProviderOutputOptions): + def __init__(self, arguments, bulk_checks_metadata, identity): + super().__init__(arguments, bulk_checks_metadata) + if ( + not hasattr(arguments, "output_filename") + or arguments.output_filename is None + ): + self.output_filename = ( + f"prowler-output-{identity.org_domain}-{output_file_timestamp}" + ) + else: + self.output_filename = arguments.output_filename diff --git a/prowler/providers/okta/okta_provider.py b/prowler/providers/okta/okta_provider.py new file mode 100644 index 0000000000..0a7b0b8e9d --- /dev/null +++ b/prowler/providers/okta/okta_provider.py @@ -0,0 +1,455 @@ +import asyncio +import base64 +import json +import os +import re +from os import environ +from typing import Optional, Union + +from colorama import Fore, Style +from okta.client import Client as OktaSDKClient + +from prowler.config.config import ( + default_config_file_path, + get_default_mute_file_path, + load_and_validate_config_file, +) +from prowler.lib.logger import logger +from prowler.lib.mutelist.mutelist import Mutelist +from prowler.lib.utils.utils import print_boxes +from prowler.providers.common.models import Audit_Metadata, Connection +from prowler.providers.common.provider import Provider +from prowler.providers.okta.exceptions.exceptions import ( + OktaEnvironmentVariableError, + OktaInsufficientPermissionsError, + OktaInvalidCredentialsError, + OktaInvalidOrgDomainError, + OktaInvalidProviderIdError, + OktaPrivateKeyFileError, + OktaSetUpIdentityError, + OktaSetUpSessionError, +) +from prowler.providers.okta.lib.mutelist.mutelist import OktaMutelist +from prowler.providers.okta.models import OktaIdentityInfo, OktaSession + +DEFAULT_SCOPES = ["okta.policies.read", "okta.brands.read", "okta.apps.read"] +# Accept only Okta-managed domains. Custom (vanity) domains are rejected on +# purpose — they're a recurring source of typos and silent misconfig and +# Prowler's audience overwhelmingly uses Okta-managed hosts. The TLDs below +# match the set the Okta SDK whitelists in `okta.config.config_validator`, +# which includes the commercial, preview, EMEA and US gov/mil environments. +# If a customer with a custom domain shows up, lift this guard behind an +# explicit opt-in. +ORG_DOMAIN_RE = re.compile( + r"^[a-z0-9][a-z0-9-]*\.(" + r"okta\.com|oktapreview\.com|okta-emea\.com|" + r"okta-gov\.com|okta\.mil|okta-miltest\.com|trex-govcloud\.com" + r")$" +) + + +class OktaProvider(Provider): + """Okta Provider class. + + Authenticates against an Okta organization using OAuth 2.0 with a + private-key JWT (Client Credentials grant). The SDK requests and + refreshes the access token internally. + + Attributes: + _type (str): The type of the provider. + _auth_method (str): The authentication method used by the provider. + _session (OktaSession): The session object for the provider. + _identity (OktaIdentityInfo): The identity information for the provider. + _audit_config (dict): The audit configuration for the provider. + _fixer_config (dict): The fixer configuration for the provider. + _mutelist (Mutelist): The mutelist for the provider. + audit_metadata (Audit_Metadata): The audit metadata for the provider. + """ + + _type: str = "okta" + _auth_method: str = None + _session: OktaSession + _identity: OktaIdentityInfo + _audit_config: dict + _fixer_config: dict + _mutelist: Mutelist + audit_metadata: Audit_Metadata + + def __init__( + self, + okta_org_domain: str = "", + okta_client_id: str = "", + okta_private_key: str = "", + okta_private_key_file: str = "", + okta_scopes: Optional[Union[str, list[str]]] = None, + config_path: str = None, + config_content: dict = None, + fixer_config: dict = {}, + mutelist_path: str = None, + mutelist_content: dict = None, + ): + """Okta Provider constructor.""" + logger.info("Instantiating Okta Provider...") + + OktaProvider.validate_arguments( + okta_org_domain=okta_org_domain, + okta_client_id=okta_client_id, + okta_private_key=okta_private_key, + okta_private_key_file=okta_private_key_file, + ) + self._session = OktaProvider.setup_session( + org_domain=okta_org_domain, + client_id=okta_client_id, + private_key=okta_private_key, + private_key_file=okta_private_key_file, + scopes=okta_scopes, + ) + self._identity = OktaProvider.setup_identity(self._session) + self._auth_method = "OAuth 2.0 (private-key JWT)" + + if config_content: + self._audit_config = config_content + else: + if not config_path: + config_path = default_config_file_path + self._audit_config = load_and_validate_config_file(self._type, config_path) + self._fixer_config = fixer_config + + if mutelist_content: + self._mutelist = OktaMutelist(mutelist_content=mutelist_content) + else: + if not mutelist_path: + mutelist_path = get_default_mute_file_path(self.type) + self._mutelist = OktaMutelist(mutelist_path=mutelist_path) + + Provider.set_global_provider(self) + + @property + def auth_method(self): + return self._auth_method + + @property + def session(self): + return self._session + + @property + def identity(self): + return self._identity + + @property + def type(self): + return self._type + + @property + def audit_config(self): + return self._audit_config + + @property + def fixer_config(self): + return self._fixer_config + + @property + def mutelist(self) -> OktaMutelist: + return self._mutelist + + @staticmethod + def validate_arguments( + okta_org_domain: str = "", + okta_client_id: str = "", + okta_private_key: str = "", + okta_private_key_file: str = "", + ): + """Validate that all required OAuth credentials are provided. + + Falls back to the matching `OKTA_*` environment variables when a CLI + argument is not supplied. The private key may be supplied as raw + content (preferred for API/UI integrations) or as a file path. + Raises a single combined error if any required value is missing. + """ + org_domain = okta_org_domain or environ.get("OKTA_ORG_DOMAIN", "") + client_id = okta_client_id or environ.get("OKTA_CLIENT_ID", "") + private_key = okta_private_key or environ.get("OKTA_PRIVATE_KEY", "") + private_key_file = okta_private_key_file or environ.get( + "OKTA_PRIVATE_KEY_FILE", "" + ) + + missing = [] + if not org_domain: + missing.append("--okta-org-domain / OKTA_ORG_DOMAIN") + if not client_id: + missing.append("--okta-client-id / OKTA_CLIENT_ID") + if not private_key and not private_key_file: + missing.append("OKTA_PRIVATE_KEY (or OKTA_PRIVATE_KEY_FILE)") + if missing: + raise OktaEnvironmentVariableError( + file=os.path.basename(__file__), + message=( + "Okta provider requires all OAuth credentials. Missing: " + + ", ".join(missing) + ), + ) + + @staticmethod + def setup_session( + org_domain: str = "", + client_id: str = "", + private_key: str = "", + private_key_file: str = "", + scopes: Optional[Union[str, list[str]]] = None, + ) -> OktaSession: + """Build an OktaSession from CLI args, falling back to environment variables. + + Accepts the private key as raw content (`private_key` / + `OKTA_PRIVATE_KEY`) or as a file path (`private_key_file` / + `OKTA_PRIVATE_KEY_FILE`). Content takes precedence when both are + supplied — this matches the GitHub provider pattern and keeps the + API/UI integrations from having to write keys to disk. + """ + try: + org_domain = org_domain or environ.get("OKTA_ORG_DOMAIN", "") + client_id = client_id or environ.get("OKTA_CLIENT_ID", "") + private_key = private_key or environ.get("OKTA_PRIVATE_KEY", "") + private_key_file = private_key_file or environ.get( + "OKTA_PRIVATE_KEY_FILE", "" + ) + if not scopes: + scopes = environ.get("OKTA_SCOPES", "") + + org_domain = org_domain.strip().lower() + if not ORG_DOMAIN_RE.match(org_domain): + raise OktaInvalidOrgDomainError( + file=os.path.basename(__file__), + message=( + f"Invalid Okta org domain: '{org_domain}'. Expected " + "an Okta-managed domain such as .okta.com " + "(or .oktapreview.com / .okta-emea.com / " + ".okta-gov.com / .okta.mil / .okta-miltest.com / " + ".trex-govcloud.com), with no scheme and no path." + ), + ) + + if private_key: + private_key = private_key.strip() + else: + try: + with open(private_key_file, "r") as fh: + private_key = fh.read().strip() + except OSError as error: + raise OktaPrivateKeyFileError( + file=os.path.basename(__file__), + original_exception=error, + message=f"Could not read private key file '{private_key_file}': {error}", + ) + if not private_key: + raise OktaPrivateKeyFileError( + file=os.path.basename(__file__), + message=( + f"Private key file '{private_key_file}' is empty." + if private_key_file + else "Private key content is empty." + ), + ) + + # Accept either a CSV string (from env var / legacy callers) or + # a list[str] (from programmatic callers and the CLI's nargs="+"). + # List elements may themselves contain commas (e.g. "a,b") and + # are flattened to support mixed input. + if isinstance(scopes, str): + raw_items = scopes.split(",") + elif isinstance(scopes, list): + raw_items = [item for s in scopes for item in str(s).split(",")] + else: + raw_items = [] + scope_list = [s.strip() for s in raw_items if s and s.strip()] + if not scope_list: + scope_list = list(DEFAULT_SCOPES) + + return OktaSession( + org_domain=org_domain, + client_id=client_id, + scopes=scope_list, + private_key=private_key, + ) + + except (OktaInvalidOrgDomainError, OktaPrivateKeyFileError): + raise + except Exception as error: + logger.critical( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + raise OktaSetUpSessionError(original_exception=error) + + @staticmethod + def setup_identity(session: OktaSession) -> OktaIdentityInfo: + """Synthesize identity from the session and verify credentials. + + Service apps don't represent a human user, so the identity is the + org URL plus the service-app client ID. We still hit the cheapest + scope-covered endpoint (`list_policies` with limit=1) to fail loud + when credentials, scopes, or the granted admin role are wrong. + + After the probe succeeds, the access token's `scp` claim is + decoded and exposed on the identity. Services compare it against + their required scope so checks can emit "missing scope X" rather + than a misleading "no resources returned" finding. + """ + + async def _probe(): + client = OktaSDKClient(session.to_sdk_config()) + result = await client.list_policies(type="OKTA_SIGN_ON", limit="1") + access_token = None + # The OAuth helper caches the token on `_access_token` after + # the first authenticated call. Reach through `_request_executor` + # — a documented internal but a moving target across SDK + # versions, so any failure here degrades silently to empty + # granted_scopes (services then fall back to attempting calls). + try: + oauth = getattr(client._request_executor, "_oauth", None) + if oauth is not None: + access_token = getattr(oauth, "_access_token", None) + except Exception: + access_token = None + return result, access_token + + try: + result, access_token = asyncio.run(_probe()) + # SDK returns (items, resp, err) on the normal path and (items, err) + # only on early request-creation errors. The error is always last. + err = result[-1] + if err is not None: + err_text = str(err).lower() + # Distinguish scope/role failures from generic credential + # failures — different remediation paths in the docs. + permission_signals = ( + "invalid_scope", + "forbidden", + "not authorized", + "permission", + # Okta emits HTTP 400 `consent_required` when none of the + # requested scopes are consented on the service app — + # semantically a permission gap, not a credential one. + "consent_required", + "not allowed", + ) + if any(signal in err_text for signal in permission_signals): + raise OktaInsufficientPermissionsError( + file=os.path.basename(__file__), + message=( + "Okta rejected the credential probe with a " + f"permission-related error: {err}" + ), + ) + raise OktaInvalidCredentialsError( + file=os.path.basename(__file__), + message=f"Failed to authenticate against Okta: {err}", + ) + return OktaIdentityInfo( + org_domain=session.org_domain, + client_id=session.client_id, + granted_scopes=OktaProvider._decode_token_scopes(access_token), + ) + except (OktaInvalidCredentialsError, OktaInsufficientPermissionsError): + raise + except Exception as error: + logger.critical( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + raise OktaSetUpIdentityError(original_exception=error) + + @staticmethod + def _decode_token_scopes(access_token: Optional[str]) -> list[str]: + """Return the `scp` claim from a JWT access token, or `[]` on failure. + + No signature verification: the token came from Okta over TLS via + the SDK's OAuth handshake, so the only thing we extract is the + scope claim. Any decode error returns an empty list — callers + must treat empty as "unknown" rather than "no scopes granted". + """ + if not access_token: + return [] + try: + parts = access_token.split(".") + if len(parts) < 2: + return [] + payload_b64 = parts[1] + # Base64url pad to a multiple of 4 — JWT segments are + # unpadded per RFC 7515. + padding = "=" * (-len(payload_b64) % 4) + payload_bytes = base64.urlsafe_b64decode(payload_b64 + padding) + payload = json.loads(payload_bytes) + scp = payload.get("scp") + if isinstance(scp, list): + return [str(s) for s in scp if s] + if isinstance(scp, str): + return [s for s in scp.split(" ") if s] + return [] + except Exception as error: + logger.warning( + f"Could not decode Okta access token scopes: " + f"{error.__class__.__name__}: {error}" + ) + return [] + + def print_credentials(self): + report_lines = [ + f"Okta Domain: {Fore.YELLOW}{self.identity.org_domain}{Style.RESET_ALL}", + f"Okta Client ID: {Fore.YELLOW}{self.identity.client_id}{Style.RESET_ALL}", + f"Authentication Method: {Fore.YELLOW}{self.auth_method}{Style.RESET_ALL}", + ] + report_title = ( + f"{Style.BRIGHT}Using the Okta credentials below:{Style.RESET_ALL}" + ) + print_boxes(report_lines, report_title) + + @staticmethod + def test_connection( + okta_org_domain: str = "", + okta_client_id: str = "", + okta_private_key: str = "", + okta_private_key_file: str = "", + okta_scopes: Optional[Union[str, list[str]]] = None, + raise_on_exception: bool = True, + provider_id: str = None, + ) -> Connection: + """Test the connection to Okta with the provided OAuth credentials. + + Args: + provider_id: The provider ID (Okta org domain). When supplied, the + authenticated org domain must match it — guards against the + stored provider UID drifting from the org the credentials were + actually issued for. Compared case-insensitively, matching the + normalization applied during session setup. + """ + try: + OktaProvider.validate_arguments( + okta_org_domain=okta_org_domain, + okta_client_id=okta_client_id, + okta_private_key=okta_private_key, + okta_private_key_file=okta_private_key_file, + ) + session = OktaProvider.setup_session( + org_domain=okta_org_domain, + client_id=okta_client_id, + private_key=okta_private_key, + private_key_file=okta_private_key_file, + scopes=okta_scopes, + ) + identity = OktaProvider.setup_identity(session) + + if provider_id and provider_id.strip().lower() != identity.org_domain: + raise OktaInvalidProviderIdError( + file=os.path.basename(__file__), + message=( + f"The provider ID '{provider_id}' does not match the " + f"authenticated Okta org domain '{identity.org_domain}'." + ), + ) + + return Connection(is_connected=True) + except Exception as error: + logger.critical( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + if raise_on_exception: + raise error + return Connection(error=error) diff --git a/prowler/providers/okta/services/__init__.py b/prowler/providers/okta/services/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/okta/services/application/__init__.py b/prowler/providers/okta/services/application/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/okta/services/application/application_admin_console_mfa_required/__init__.py b/prowler/providers/okta/services/application/application_admin_console_mfa_required/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/okta/services/application/application_admin_console_mfa_required/application_admin_console_mfa_required.metadata.json b/prowler/providers/okta/services/application/application_admin_console_mfa_required/application_admin_console_mfa_required.metadata.json new file mode 100644 index 0000000000..dbe02f23f8 --- /dev/null +++ b/prowler/providers/okta/services/application/application_admin_console_mfa_required/application_admin_console_mfa_required.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "okta", + "CheckID": "application_admin_console_mfa_required", + "CheckTitle": "Okta Admin Console authentication policy enforces multifactor authentication", + "CheckType": [], + "ServiceName": "application", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "NotDefined", + "ResourceGroup": "governance", + "Description": "The **Authentication Policy** bound to the **Okta Admin Console** app must require MFA. On its top active rule, *User must authenticate with* must be set to `Password / IdP + Another factor` or `Any 2 factor types` (`factorMode=2FA` in the API).", + "Risk": "Single-factor access to the Okta control plane is the highest-impact identity risk in the tenant.\n\n- **Credential compromise** is enough to take over every administrator account\n- **Lateral movement** into every downstream SaaS that trusts Okta SSO\n- **Privileged configuration changes** with no second-factor barrier", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://help.okta.com/oie/en-us/content/topics/identity-engine/policies/about-app-sign-on-policies.htm", + "https://developer.okta.com/docs/api/openapi/okta-management/management/tag/Policy/" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the **Okta Admin Console** as a *Super Admin*.\n2. Navigate to **Security** > **Authentication Policies**.\n3. Open the **Okta Admin Console** policy.\n4. Edit the top active rule.\n5. Set *User must authenticate with* to `Password / IdP + Another factor` or `Any 2 factor types`.\n6. Save the rule.", + "Terraform": "" + }, + "Recommendation": { + "Text": "Require MFA on the top active rule of the Okta Admin Console authentication policy. Set *User must authenticate with* to `Password / IdP + Another factor` or `Any 2 factor types`.", + "Url": "https://hub.prowler.com/check/application_admin_console_mfa_required" + } + }, + "Categories": [ + "identity-access" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Aligns with DISA STIG V-273193 / OKTA-APP-000560." +} diff --git a/prowler/providers/okta/services/application/application_admin_console_mfa_required/application_admin_console_mfa_required.py b/prowler/providers/okta/services/application/application_admin_console_mfa_required/application_admin_console_mfa_required.py new file mode 100644 index 0000000000..bf21a1a766 --- /dev/null +++ b/prowler/providers/okta/services/application/application_admin_console_mfa_required/application_admin_console_mfa_required.py @@ -0,0 +1,89 @@ +from prowler.lib.check.models import Check, CheckReportOkta +from prowler.providers.okta.services.application.application_client import ( + application_client, +) +from prowler.providers.okta.services.application.application_service import ( + ADMIN_CONSOLE_APP_NAME, +) +from prowler.providers.okta.services.application.lib.application_helpers import ( + app_label, + app_not_found_finding, + missing_app_scope_finding, + policy_missing_finding, + rule_label, + top_active_rule, +) + +ADMIN_CONSOLE_LABEL_HINT = "Okta Admin Console" + + +class application_admin_console_mfa_required(Check): + """STIG V-273193 / OKTA-APP-000560. + + The Authentication Policy bound to the Okta Admin Console app must + require multifactor authentication on its top rule: `User must + authenticate with` set to `Password / IdP + Another factor` or + `Any 2 factor types`. + + The underlying SDK exposes this as `AssuranceMethod.factor_mode` + with values `1FA` / `2FA`. + """ + + def execute(self) -> list[CheckReportOkta]: + findings: list[CheckReportOkta] = [] + org_domain = application_client.provider.identity.org_domain + + for scope_key in ("built_in_apps", "access_policies"): + missing_scope = application_client.missing_scope.get(scope_key) + if missing_scope: + findings.append( + missing_app_scope_finding( + self.metadata(), + org_domain, + missing_scope, + ADMIN_CONSOLE_LABEL_HINT, + ) + ) + return findings + + app = application_client.built_in_apps.get(ADMIN_CONSOLE_APP_NAME) + if app is None: + findings.append( + app_not_found_finding( + self.metadata(), org_domain, ADMIN_CONSOLE_LABEL_HINT + ) + ) + return findings + + if app.access_policy_id is None or app.access_policy is None: + findings.append(policy_missing_finding(self.metadata(), org_domain, app)) + return findings + + report = CheckReportOkta( + metadata=self.metadata(), resource=app, org_domain=org_domain + ) + rule = top_active_rule(app) + if rule is None: + report.status = "FAIL" + report.status_extended = ( + f"{app_label(app)} has no active rules on its Authentication " + "Policy. The top rule must set `User must authenticate with` to " + "`Password / IdP + Another factor` or `Any 2 factor types`." + ) + elif rule.factor_mode == "2FA": + report.status = "PASS" + report.status_extended = ( + f"Top active {rule_label(rule)} on {app_label(app)} enforces " + "multifactor authentication (`factorMode=2FA`)." + ) + else: + report.status = "FAIL" + report.status_extended = ( + f"Top active {rule_label(rule)} on {app_label(app)} does not " + f"enforce multifactor authentication " + f"(`factorMode={rule.factor_mode or 'unset'}`). " + "Set `User must authenticate with` to `Password / IdP + Another " + "factor` or `Any 2 factor types`." + ) + findings.append(report) + return findings diff --git a/prowler/providers/okta/services/application/application_admin_console_phishing_resistant_authentication/__init__.py b/prowler/providers/okta/services/application/application_admin_console_phishing_resistant_authentication/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/okta/services/application/application_admin_console_phishing_resistant_authentication/application_admin_console_phishing_resistant_authentication.metadata.json b/prowler/providers/okta/services/application/application_admin_console_phishing_resistant_authentication/application_admin_console_phishing_resistant_authentication.metadata.json new file mode 100644 index 0000000000..0c86fdd974 --- /dev/null +++ b/prowler/providers/okta/services/application/application_admin_console_phishing_resistant_authentication/application_admin_console_phishing_resistant_authentication.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "okta", + "CheckID": "application_admin_console_phishing_resistant_authentication", + "CheckTitle": "Okta Admin Console authentication policy enforces phishing-resistant factors", + "CheckType": [], + "ServiceName": "application", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "governance", + "Description": "The **Authentication Policy** bound to the **Okta Admin Console** app must restrict possession factors to phishing-resistant authenticators (FIDO2/WebAuthn, PIV/CAC, Okta FastPass with biometrics). On the top active rule, *Possession factor constraints are: Phishing resistant* must be checked (`possession.phishingResistant=REQUIRED`).", + "Risk": "Phishable possession factors (SMS, voice, standard push, OTP delivered via reverse-proxy AiTM) leave the most privileged surface of the IdP exposed.\n\n- **Credential phishing** against administrators succeeds despite MFA\n- **Adversary-in-the-Middle attacks** capture session tokens through fake login pages\n- **Account takeover** of the tenant control plane", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://help.okta.com/oie/en-us/content/topics/identity-engine/authenticators/phishing-resistant-auth.htm", + "https://developer.okta.com/docs/api/openapi/okta-management/management/tag/Policy/" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the **Okta Admin Console** as a *Super Admin*.\n2. Navigate to **Security** > **Authentication Policies**.\n3. Open the **Okta Admin Console** policy.\n4. Edit the top active rule.\n5. Under *Possession factor constraints are*, check **Phishing resistant**.\n6. Save the rule.", + "Terraform": "" + }, + "Recommendation": { + "Text": "Require phishing-resistant possession factors on the top active rule of the Okta Admin Console authentication policy.", + "Url": "https://hub.prowler.com/check/application_admin_console_phishing_resistant_authentication" + } + }, + "Categories": [ + "identity-access" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Aligns with DISA STIG V-273191 / OKTA-APP-000190." +} diff --git a/prowler/providers/okta/services/application/application_admin_console_phishing_resistant_authentication/application_admin_console_phishing_resistant_authentication.py b/prowler/providers/okta/services/application/application_admin_console_phishing_resistant_authentication/application_admin_console_phishing_resistant_authentication.py new file mode 100644 index 0000000000..237ffe27bb --- /dev/null +++ b/prowler/providers/okta/services/application/application_admin_console_phishing_resistant_authentication/application_admin_console_phishing_resistant_authentication.py @@ -0,0 +1,88 @@ +from prowler.lib.check.models import Check, CheckReportOkta +from prowler.providers.okta.services.application.application_client import ( + application_client, +) +from prowler.providers.okta.services.application.application_service import ( + ADMIN_CONSOLE_APP_NAME, +) +from prowler.providers.okta.services.application.lib.application_helpers import ( + app_label, + app_not_found_finding, + missing_app_scope_finding, + policy_missing_finding, + rule_label, + top_active_rule, +) + +ADMIN_CONSOLE_LABEL_HINT = "Okta Admin Console" + + +class application_admin_console_phishing_resistant_authentication(Check): + """STIG V-273191 / OKTA-APP-000190. + + The Authentication Policy bound to the Okta Admin Console app must + restrict possession factors to phishing-resistant authenticators. + The underlying SDK exposes `phishingResistant` on each + `PossessionConstraint`; at least one constraint object on the top + rule must set `phishingResistant=REQUIRED` (constraints are OR-ed + by Okta semantics). + """ + + def execute(self) -> list[CheckReportOkta]: + findings: list[CheckReportOkta] = [] + org_domain = application_client.provider.identity.org_domain + + for scope_key in ("built_in_apps", "access_policies"): + missing_scope = application_client.missing_scope.get(scope_key) + if missing_scope: + findings.append( + missing_app_scope_finding( + self.metadata(), + org_domain, + missing_scope, + ADMIN_CONSOLE_LABEL_HINT, + ) + ) + return findings + + app = application_client.built_in_apps.get(ADMIN_CONSOLE_APP_NAME) + if app is None: + findings.append( + app_not_found_finding( + self.metadata(), org_domain, ADMIN_CONSOLE_LABEL_HINT + ) + ) + return findings + + if app.access_policy_id is None or app.access_policy is None: + findings.append(policy_missing_finding(self.metadata(), org_domain, app)) + return findings + + report = CheckReportOkta( + metadata=self.metadata(), resource=app, org_domain=org_domain + ) + rule = top_active_rule(app) + if rule is None: + report.status = "FAIL" + report.status_extended = ( + f"{app_label(app)} has no active rules on its Authentication " + "Policy. The top rule must mark " + "`Possession factor constraints are: Phishing resistant`." + ) + elif rule.possession_phishing_resistant_required: + report.status = "PASS" + report.status_extended = ( + f"Top active {rule_label(rule)} on {app_label(app)} enforces " + "phishing-resistant possession factors " + "(`possession.phishingResistant=REQUIRED`)." + ) + else: + report.status = "FAIL" + report.status_extended = ( + f"Top active {rule_label(rule)} on {app_label(app)} does not " + "enforce phishing-resistant possession factors. Enable " + "`Possession factor constraints are: Phishing resistant` " + "on the rule." + ) + findings.append(report) + return findings diff --git a/prowler/providers/okta/services/application/application_admin_console_session_idle_timeout_15min/__init__.py b/prowler/providers/okta/services/application/application_admin_console_session_idle_timeout_15min/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/okta/services/application/application_admin_console_session_idle_timeout_15min/application_admin_console_session_idle_timeout_15min.metadata.json b/prowler/providers/okta/services/application/application_admin_console_session_idle_timeout_15min/application_admin_console_session_idle_timeout_15min.metadata.json new file mode 100644 index 0000000000..fda8cd7816 --- /dev/null +++ b/prowler/providers/okta/services/application/application_admin_console_session_idle_timeout_15min/application_admin_console_session_idle_timeout_15min.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "okta", + "CheckID": "application_admin_console_session_idle_timeout_15min", + "CheckTitle": "Okta Admin Console app session idle timeout is 15 minutes or less", + "CheckType": [], + "ServiceName": "application", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "governance", + "Description": "The integrated **Okta Admin Console** app must close idle privileged sessions. *Maximum app session idle time* on the **Sign On** tab must be `15` minutes or less.\n\nThreshold override: `okta_admin_console_idle_timeout_max_minutes`.", + "Risk": "An unattended administrator workstation leaves the Okta control plane open for session hijacking.\n\n- **Privileged session takeover** by anyone with physical or remote access to the workstation\n- **Tenant-wide configuration changes** under the absent administrator's identity\n- **Bypassed reauthentication** for the most sensitive surface of the IdP", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://developer.okta.com/docs/guides/configure-signon-policy/main/", + "https://developer.okta.com/docs/api/openapi/okta-management/management/tag/OktaApplicationSettings/" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the **Okta Admin Console** as a *Super Admin*.\n2. Navigate to **Applications** > **Applications** > **Okta Admin Console**.\n3. Open the **Sign On** tab.\n4. Under **Okta Admin Console session**, set *Maximum app session idle time* to `15` minutes or less.\n5. Save the changes.", + "Terraform": "" + }, + "Recommendation": { + "Text": "Set the *Maximum app session idle time* of the Okta Admin Console first-party app to `15` minutes or less so privileged administrator sessions terminate on inactivity.", + "Url": "https://hub.prowler.com/check/application_admin_console_session_idle_timeout_15min" + } + }, + "Categories": [ + "identity-access" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Aligns with DISA STIG V-273187 / OKTA-APP-000025." +} diff --git a/prowler/providers/okta/services/application/application_admin_console_session_idle_timeout_15min/application_admin_console_session_idle_timeout_15min.py b/prowler/providers/okta/services/application/application_admin_console_session_idle_timeout_15min/application_admin_console_session_idle_timeout_15min.py new file mode 100644 index 0000000000..6103360f07 --- /dev/null +++ b/prowler/providers/okta/services/application/application_admin_console_session_idle_timeout_15min/application_admin_console_session_idle_timeout_15min.py @@ -0,0 +1,89 @@ +from prowler.lib.check.models import Check, CheckReportOkta +from prowler.providers.okta.services.application.application_client import ( + application_client, +) +from prowler.providers.okta.services.application.application_service import ( + AdminConsoleAppSettings, +) +from prowler.providers.okta.services.application.lib.application_helpers import ( + missing_admin_console_settings_scope_finding, +) + +DEFAULT_THRESHOLD_MINUTES = 15 + + +class application_admin_console_session_idle_timeout_15min(Check): + """STIG V-273187 / OKTA-APP-000025. + + The Okta Admin Console first-party app must set its + `Maximum app session idle time` to 15 minutes (or less) so privileged + administrator sessions terminate on inactivity. Threshold override: + `okta_admin_console_idle_timeout_max_minutes` in the audit config. + """ + + def execute(self) -> list[CheckReportOkta]: + findings: list[CheckReportOkta] = [] + audit_config = application_client.audit_config or {} + threshold = audit_config.get( + "okta_admin_console_idle_timeout_max_minutes", + DEFAULT_THRESHOLD_MINUTES, + ) + org_domain = application_client.provider.identity.org_domain + + missing_scope = application_client.missing_scope.get( + "admin_console_app_settings" + ) + if missing_scope: + findings.append( + missing_admin_console_settings_scope_finding( + self.metadata(), org_domain, missing_scope + ) + ) + return findings + + settings = application_client.admin_console_app_settings + if settings is None: + placeholder = AdminConsoleAppSettings() + report = CheckReportOkta( + metadata=self.metadata(), resource=placeholder, org_domain=org_domain + ) + report.status = "MANUAL" + report.status_extended = ( + "Could not retrieve the Okta Admin Console first-party app " + "settings. Okta restricts `GET /api/v1/first-party-app-settings/" + "admin-console` to the Super Administrator role; every other " + "role — including Read-Only Administrator — receives " + "`403 E0000006`. Assign Super Administrator to the service " + f"app to evaluate this check. The `Maximum app session idle " + f"time` must be set to {threshold} minutes or less." + ) + findings.append(report) + return findings + + report = CheckReportOkta( + metadata=self.metadata(), resource=settings, org_domain=org_domain + ) + idle = settings.session_idle_timeout_minutes + if idle is None: + report.status = "FAIL" + report.status_extended = ( + "The Okta Admin Console first-party app does not define a " + "`Maximum app session idle time`. This value must be " + f"{threshold} minutes or less." + ) + elif idle <= threshold: + report.status = "PASS" + report.status_extended = ( + "The Okta Admin Console first-party app sets the maximum " + f"app session idle time to {idle} minutes, meeting the " + f"configured threshold of {threshold} minutes." + ) + else: + report.status = "FAIL" + report.status_extended = ( + "The Okta Admin Console first-party app sets the maximum " + f"app session idle time to {idle} minutes, exceeding the " + f"configured threshold of {threshold} minutes." + ) + findings.append(report) + return findings diff --git a/prowler/providers/okta/services/application/application_authentication_policy_network_zone_enforced/__init__.py b/prowler/providers/okta/services/application/application_authentication_policy_network_zone_enforced/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/okta/services/application/application_authentication_policy_network_zone_enforced/application_authentication_policy_network_zone_enforced.metadata.json b/prowler/providers/okta/services/application/application_authentication_policy_network_zone_enforced/application_authentication_policy_network_zone_enforced.metadata.json new file mode 100644 index 0000000000..fd9234e0a6 --- /dev/null +++ b/prowler/providers/okta/services/application/application_authentication_policy_network_zone_enforced/application_authentication_policy_network_zone_enforced.metadata.json @@ -0,0 +1,38 @@ +{ + "Provider": "okta", + "CheckID": "application_authentication_policy_network_zone_enforced", + "CheckTitle": "Okta application authentication policies enforce Network Zones", + "CheckType": [], + "ServiceName": "application", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "governance", + "Description": "Every active Okta application must be bound to an **Authentication Policy** that uses **Network Zones**. Each active non-default rule must map *User's IP* to `In zone` or `Not in zone`, and the active built-in *Catch-all Rule* must set *Access is* to `Denied`.", + "Risk": "Applications without network-aware authentication rules can be reached from unauthorized locations and bypass location-based access controls.\n\n- **Unauthorized access paths** from unmanaged or blocked networks\n- **Inconsistent information-flow enforcement** across SSO applications\n- **Residual access** when the fallback rule still allows traffic after policy misses", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://help.okta.com/oie/en-us/content/topics/identity-engine/policies/about-app-sign-on-policies.htm", + "https://developer.okta.com/docs/api/openapi/okta-management/management/tag/Application/", + "https://developer.okta.com/docs/api/openapi/okta-management/management/tag/Policy/" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the **Okta Admin Console** as a *Super Admin*.\n2. Navigate to **Security** > **Networks** and define the allow-list and deny-list zones required by policy.\n3. For each active application, open **Applications** > **Applications** > *Application* > **Sign On**.\n4. In **User Authentication**, bind the appropriate **Authentication Policy** and open **View Policy Details**.\n5. For each active non-default rule, set *User's IP* to `In zone` or `Not in zone` and select the correct **Network Zone**.\n6. Edit the built-in **Catch-all Rule** and set *Access is* to `Denied`.\n7. Save the policy.", + "Terraform": "" + }, + "Recommendation": { + "Text": "Require every active application Authentication Policy to use Network Zones on each active non-default rule and to deny access on the Catch-all Rule.", + "Url": "https://hub.prowler.com/check/application_authentication_policy_network_zone_enforced" + } + }, + "Categories": [ + "identity-access" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Aligns with DISA STIG V-279693 / OKTA-APP-003244." +} diff --git a/prowler/providers/okta/services/application/application_authentication_policy_network_zone_enforced/application_authentication_policy_network_zone_enforced.py b/prowler/providers/okta/services/application/application_authentication_policy_network_zone_enforced/application_authentication_policy_network_zone_enforced.py new file mode 100644 index 0000000000..5e6e2c9c76 --- /dev/null +++ b/prowler/providers/okta/services/application/application_authentication_policy_network_zone_enforced/application_authentication_policy_network_zone_enforced.py @@ -0,0 +1,151 @@ +from prowler.lib.check.models import Check, CheckReportOkta +from prowler.providers.okta.services.application.application_client import ( + application_client, +) +from prowler.providers.okta.services.application.application_service import ( + AuthenticationPolicyRule, + OktaBuiltInApp, +) +from prowler.providers.okta.services.application.lib.application_helpers import ( + active_apps, + app_label, + missing_integrated_apps_scope_finding, + no_active_apps_finding, + rule_has_network_zone, + rule_label, +) + + +class application_authentication_policy_network_zone_enforced(Check): + """STIG V-279693 / OKTA-APP-003244. + + Every active Okta application must be bound to an Authentication + Policy that uses Network Zones. Each active non-default rule must map + `User's IP` to an allow/deny zone, and the active Catch-all Rule + must deny access. + """ + + def execute(self) -> list[CheckReportOkta]: + findings: list[CheckReportOkta] = [] + org_domain = application_client.provider.identity.org_domain + + for scope_key in ("integrated_apps", "access_policies"): + missing_scope = application_client.missing_scope.get(scope_key) + if missing_scope: + findings.append( + missing_integrated_apps_scope_finding( + self.metadata(), + org_domain, + missing_scope, + ) + ) + return findings + + apps = active_apps(application_client.integrated_apps) + if not apps: + findings.append(no_active_apps_finding(self.metadata(), org_domain)) + return findings + + for app in apps: + report = CheckReportOkta( + metadata=self.metadata(), + resource=app, + org_domain=org_domain, + resource_name=app.label or app.name, + resource_id=app.id, + ) + status, status_extended = _evaluate_app(app) + report.status = status + report.status_extended = status_extended + findings.append(report) + return findings + + +def _active_rules(app: OktaBuiltInApp) -> list[AuthenticationPolicyRule]: + if app.access_policy is None: + return [] + return sorted( + [ + rule + for rule in app.access_policy.rules + if not rule.status or rule.status.upper() == "ACTIVE" + ], + key=lambda rule: ( + rule.priority if rule.priority is not None else float("inf"), + rule.name, + ), + ) + + +def _evaluate_app(app: OktaBuiltInApp) -> tuple[str, str]: + label = app_label(app) + if app.access_policy_id is None or app.access_policy is None: + return ( + "FAIL", + f"{label} has no Authentication Policy bound to it. " + "Bind an Access Policy in Security > Authentication Policies.", + ) + + active_rules = _active_rules(app) + if not active_rules: + return ( + "FAIL", + f"{label} has no active rules on its Authentication Policy. " + "Every active non-default rule must enforce a Network Zone " + "condition, and the Catch-all Rule must set `Access is: Denied`.", + ) + + nondefault_rules = [ + rule + for rule in active_rules + if not rule.is_default and rule.name != "Catch-all Rule" + ] + if not nondefault_rules: + return ( + "FAIL", + f"{label} has no active non-default rules on its Authentication " + "Policy. Define at least one non-default rule that maps `User's " + "IP` to a Network Zone, and use the Catch-all Rule only as the " + "final deny path.", + ) + + missing_zone_rules = [ + rule.name for rule in nondefault_rules if not rule_has_network_zone(rule) + ] + if missing_zone_rules: + quoted_rules = ", ".join(f"'{rule_name}'" for rule_name in missing_zone_rules) + return ( + "FAIL", + f"{label} has active non-default rule(s) without Network Zones: " + f"{quoted_rules}. Configure `User's IP` to `In zone` or `Not in zone` " + "for every active non-default rule.", + ) + + catch_all_rule = next( + ( + rule + for rule in active_rules + if rule.is_default or rule.name == "Catch-all Rule" + ), + None, + ) + if catch_all_rule is None: + return ( + "FAIL", + f"{label} has no active Catch-all Rule. The Catch-all Rule must " + "deny access after the zoned non-default rules.", + ) + + if catch_all_rule.access != "DENY": + return ( + "FAIL", + f"Active {rule_label(catch_all_rule)} on {label} does not set " + f"`Access is` to `DENY` (`access={catch_all_rule.access or 'unset'}`). " + "Set the Catch-all Rule to deny access.", + ) + + return ( + "PASS", + f"{label} applies Network Zones on every active non-default rule and " + f"its active {rule_label(catch_all_rule)} denies access.", + ) diff --git a/prowler/providers/okta/services/application/application_client.py b/prowler/providers/okta/services/application/application_client.py new file mode 100644 index 0000000000..63658ea3c6 --- /dev/null +++ b/prowler/providers/okta/services/application/application_client.py @@ -0,0 +1,4 @@ +from prowler.providers.common.provider import Provider +from prowler.providers.okta.services.application.application_service import Application + +application_client = Application(Provider.get_global_provider()) diff --git a/prowler/providers/okta/services/application/application_dashboard_mfa_required/__init__.py b/prowler/providers/okta/services/application/application_dashboard_mfa_required/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/okta/services/application/application_dashboard_mfa_required/application_dashboard_mfa_required.metadata.json b/prowler/providers/okta/services/application/application_dashboard_mfa_required/application_dashboard_mfa_required.metadata.json new file mode 100644 index 0000000000..1211eaec62 --- /dev/null +++ b/prowler/providers/okta/services/application/application_dashboard_mfa_required/application_dashboard_mfa_required.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "okta", + "CheckID": "application_dashboard_mfa_required", + "CheckTitle": "Okta Dashboard authentication policy enforces multifactor authentication", + "CheckType": [], + "ServiceName": "application", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "NotDefined", + "ResourceGroup": "governance", + "Description": "The **Authentication Policy** bound to the **Okta Dashboard** app must require MFA for end users. On its top active rule, *User must authenticate with* must be set to `Password / IdP + Another factor` or `Any 2 factor types` (`factorMode=2FA` in the API).", + "Risk": "Single-factor access to the Okta Dashboard lets an attacker pivot from one compromised password into every downstream SSO app.\n\n- **Credential stuffing** and password reuse attacks succeed in one step\n- **Lateral movement** into every SaaS the user has access to via Okta SSO\n- **Weakened identity assurance** for every user signing in to the end-user portal", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://help.okta.com/oie/en-us/content/topics/identity-engine/policies/about-app-sign-on-policies.htm", + "https://developer.okta.com/docs/api/openapi/okta-management/management/tag/Policy/" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the **Okta Admin Console** as a *Super Admin*.\n2. Navigate to **Security** > **Authentication Policies**.\n3. Open the **Okta Dashboard** policy.\n4. Edit the top active rule.\n5. Set *User must authenticate with* to `Password / IdP + Another factor` or `Any 2 factor types`.\n6. Save the rule.", + "Terraform": "" + }, + "Recommendation": { + "Text": "Require MFA on the top active rule of the Okta Dashboard authentication policy. Set *User must authenticate with* to `Password / IdP + Another factor` or `Any 2 factor types`.", + "Url": "https://hub.prowler.com/check/application_dashboard_mfa_required" + } + }, + "Categories": [ + "identity-access" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Aligns with DISA STIG V-273194 / OKTA-APP-000570." +} diff --git a/prowler/providers/okta/services/application/application_dashboard_mfa_required/application_dashboard_mfa_required.py b/prowler/providers/okta/services/application/application_dashboard_mfa_required/application_dashboard_mfa_required.py new file mode 100644 index 0000000000..48c04083fb --- /dev/null +++ b/prowler/providers/okta/services/application/application_dashboard_mfa_required/application_dashboard_mfa_required.py @@ -0,0 +1,85 @@ +from prowler.lib.check.models import Check, CheckReportOkta +from prowler.providers.okta.services.application.application_client import ( + application_client, +) +from prowler.providers.okta.services.application.application_service import ( + DASHBOARD_APP_NAME, +) +from prowler.providers.okta.services.application.lib.application_helpers import ( + app_label, + app_not_found_finding, + missing_app_scope_finding, + policy_missing_finding, + rule_label, + top_active_rule, +) + +DASHBOARD_LABEL_HINT = "Okta Dashboard" + + +class application_dashboard_mfa_required(Check): + """STIG V-273194 / OKTA-APP-000570. + + The Authentication Policy bound to the Okta Dashboard app must + require multifactor authentication on its top rule for + non-privileged users: `User must authenticate with` set to + `Password / IdP + Another factor` or `Any 2 factor types` + (`AssuranceMethod.factor_mode == "2FA"`). + """ + + def execute(self) -> list[CheckReportOkta]: + findings: list[CheckReportOkta] = [] + org_domain = application_client.provider.identity.org_domain + + for scope_key in ("built_in_apps", "access_policies"): + missing_scope = application_client.missing_scope.get(scope_key) + if missing_scope: + findings.append( + missing_app_scope_finding( + self.metadata(), + org_domain, + missing_scope, + DASHBOARD_LABEL_HINT, + ) + ) + return findings + + app = application_client.built_in_apps.get(DASHBOARD_APP_NAME) + if app is None: + findings.append( + app_not_found_finding(self.metadata(), org_domain, DASHBOARD_LABEL_HINT) + ) + return findings + + if app.access_policy_id is None or app.access_policy is None: + findings.append(policy_missing_finding(self.metadata(), org_domain, app)) + return findings + + report = CheckReportOkta( + metadata=self.metadata(), resource=app, org_domain=org_domain + ) + rule = top_active_rule(app) + if rule is None: + report.status = "FAIL" + report.status_extended = ( + f"{app_label(app)} has no active rules on its Authentication " + "Policy. The top rule must set `User must authenticate with` to " + "`Password / IdP + Another factor` or `Any 2 factor types`." + ) + elif rule.factor_mode == "2FA": + report.status = "PASS" + report.status_extended = ( + f"Top active {rule_label(rule)} on {app_label(app)} enforces " + "multifactor authentication (`factorMode=2FA`)." + ) + else: + report.status = "FAIL" + report.status_extended = ( + f"Top active {rule_label(rule)} on {app_label(app)} does not " + f"enforce multifactor authentication " + f"(`factorMode={rule.factor_mode or 'unset'}`). " + "Set `User must authenticate with` to `Password / IdP + Another " + "factor` or `Any 2 factor types`." + ) + findings.append(report) + return findings diff --git a/prowler/providers/okta/services/application/application_dashboard_phishing_resistant_authentication/__init__.py b/prowler/providers/okta/services/application/application_dashboard_phishing_resistant_authentication/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/okta/services/application/application_dashboard_phishing_resistant_authentication/application_dashboard_phishing_resistant_authentication.metadata.json b/prowler/providers/okta/services/application/application_dashboard_phishing_resistant_authentication/application_dashboard_phishing_resistant_authentication.metadata.json new file mode 100644 index 0000000000..1c8996280b --- /dev/null +++ b/prowler/providers/okta/services/application/application_dashboard_phishing_resistant_authentication/application_dashboard_phishing_resistant_authentication.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "okta", + "CheckID": "application_dashboard_phishing_resistant_authentication", + "CheckTitle": "Okta Dashboard authentication policy enforces phishing-resistant factors", + "CheckType": [], + "ServiceName": "application", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "governance", + "Description": "The **Authentication Policy** bound to the **Okta Dashboard** app must restrict possession factors to phishing-resistant authenticators. On the top active rule, *Possession factor constraints are: Phishing resistant* must be checked (`possession.phishingResistant=REQUIRED`).", + "Risk": "Phishable possession factors leave end-user SSO sessions exposed to credential phishing and AiTM proxies.\n\n- **Credential phishing** against end users succeeds despite MFA\n- **Session token theft** through reverse-proxy AiTM attacks (Evilginx-class tooling)\n- **Compromise of every downstream SSO app** the user has access to", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://help.okta.com/oie/en-us/content/topics/identity-engine/authenticators/phishing-resistant-auth.htm", + "https://developer.okta.com/docs/api/openapi/okta-management/management/tag/Policy/" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the **Okta Admin Console** as a *Super Admin*.\n2. Navigate to **Security** > **Authentication Policies**.\n3. Open the **Okta Dashboard** policy.\n4. Edit the top active rule.\n5. Under *Possession factor constraints are*, check **Phishing resistant**.\n6. Save the rule.", + "Terraform": "" + }, + "Recommendation": { + "Text": "Require phishing-resistant possession factors on the top active rule of the Okta Dashboard authentication policy.", + "Url": "https://hub.prowler.com/check/application_dashboard_phishing_resistant_authentication" + } + }, + "Categories": [ + "identity-access" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Aligns with DISA STIG V-273190 / OKTA-APP-000180." +} diff --git a/prowler/providers/okta/services/application/application_dashboard_phishing_resistant_authentication/application_dashboard_phishing_resistant_authentication.py b/prowler/providers/okta/services/application/application_dashboard_phishing_resistant_authentication/application_dashboard_phishing_resistant_authentication.py new file mode 100644 index 0000000000..11fa310da2 --- /dev/null +++ b/prowler/providers/okta/services/application/application_dashboard_phishing_resistant_authentication/application_dashboard_phishing_resistant_authentication.py @@ -0,0 +1,84 @@ +from prowler.lib.check.models import Check, CheckReportOkta +from prowler.providers.okta.services.application.application_client import ( + application_client, +) +from prowler.providers.okta.services.application.application_service import ( + DASHBOARD_APP_NAME, +) +from prowler.providers.okta.services.application.lib.application_helpers import ( + app_label, + app_not_found_finding, + missing_app_scope_finding, + policy_missing_finding, + rule_label, + top_active_rule, +) + +DASHBOARD_LABEL_HINT = "Okta Dashboard" + + +class application_dashboard_phishing_resistant_authentication(Check): + """STIG V-273190 / OKTA-APP-000180. + + The Authentication Policy bound to the Okta Dashboard app must + restrict possession factors to phishing-resistant authenticators on + its top active rule + (`possession.phishingResistant=REQUIRED`). + """ + + def execute(self) -> list[CheckReportOkta]: + findings: list[CheckReportOkta] = [] + org_domain = application_client.provider.identity.org_domain + + for scope_key in ("built_in_apps", "access_policies"): + missing_scope = application_client.missing_scope.get(scope_key) + if missing_scope: + findings.append( + missing_app_scope_finding( + self.metadata(), + org_domain, + missing_scope, + DASHBOARD_LABEL_HINT, + ) + ) + return findings + + app = application_client.built_in_apps.get(DASHBOARD_APP_NAME) + if app is None: + findings.append( + app_not_found_finding(self.metadata(), org_domain, DASHBOARD_LABEL_HINT) + ) + return findings + + if app.access_policy_id is None or app.access_policy is None: + findings.append(policy_missing_finding(self.metadata(), org_domain, app)) + return findings + + report = CheckReportOkta( + metadata=self.metadata(), resource=app, org_domain=org_domain + ) + rule = top_active_rule(app) + if rule is None: + report.status = "FAIL" + report.status_extended = ( + f"{app_label(app)} has no active rules on its Authentication " + "Policy. The top rule must mark " + "`Possession factor constraints are: Phishing resistant`." + ) + elif rule.possession_phishing_resistant_required: + report.status = "PASS" + report.status_extended = ( + f"Top active {rule_label(rule)} on {app_label(app)} enforces " + "phishing-resistant possession factors " + "(`possession.phishingResistant=REQUIRED`)." + ) + else: + report.status = "FAIL" + report.status_extended = ( + f"Top active {rule_label(rule)} on {app_label(app)} does not " + "enforce phishing-resistant possession factors. Enable " + "`Possession factor constraints are: Phishing resistant` " + "on the rule." + ) + findings.append(report) + return findings diff --git a/prowler/providers/okta/services/application/application_service.py b/prowler/providers/okta/services/application/application_service.py new file mode 100644 index 0000000000..de2fe0a658 --- /dev/null +++ b/prowler/providers/okta/services/application/application_service.py @@ -0,0 +1,592 @@ +import json +from typing import Optional +from urllib.parse import parse_qs, urlparse + +from pydantic import BaseModel, ValidationError + +from prowler.lib.logger import logger +from prowler.providers.okta.lib.service.service import OktaService + +# These three keys are Okta-platform constants, not tenant-configurable: +# +# - `saasure` / `okta_enduser` are the `name` fields of the OIN catalog +# templates for the Okta Admin Console and Okta Dashboard built-in apps. +# The Okta SDK's `OINApplication.name` is documented as "the key name for +# the OIN app definition" — tied to the platform-level template, not +# editable by customers. The user-facing field is `label`, which we read +# only for display purposes in finding text. +# - `admin-console` is the Okta-defined URL key for +# `/api/v1/first-party-app-settings/{appName}`; per the SDK's own +# `get_first_party_app_settings` docstring it is the only value Okta +# currently supports on that endpoint. +# +# If Okta introduces a new first-party app or renames one of these at the +# platform level, both the constants and the check coverage need updating +# together. +ADMIN_CONSOLE_APP_NAME = "saasure" +DASHBOARD_APP_NAME = "okta_enduser" +ADMIN_CONSOLE_FIRST_PARTY_APP_KEY = "admin-console" + + +def _next_after_cursor(resp) -> Optional[str]: + """Extract the `after` cursor from a `Link: ...; rel="next"` header. + + Returns None when there is no next page. Header format follows RFC 5988 + and Okta's pagination guide. Mirrors the helper in `signon_service` — + duplicated rather than shared until a third Okta service appears. + """ + if resp is None: + return None + headers = getattr(resp, "headers", None) or {} + link = headers.get("link") or headers.get("Link") or "" + if not link: + return None + for part in link.split(","): + if 'rel="next"' not in part: + continue + url_segment = part.split(";", 1)[0].strip().lstrip("<").rstrip(">") + cursor = parse_qs(urlparse(url_segment).query).get("after", [None])[0] + if cursor: + return cursor + return None + + +REQUIRED_SCOPES: dict[str, str] = { + "admin_console_app_settings": "okta.apps.read", + "built_in_apps": "okta.apps.read", + "integrated_apps": "okta.apps.read", + "access_policies": "okta.policies.read", +} + + +class Application(OktaService): + """Fetches Okta first-party apps and their bound Authentication Policies. + + Populates: + - `self.admin_console_app_settings` — first-party Admin Console session + knobs (`sessionIdleTimeoutMinutes`, `sessionMaxLifetimeMinutes`). + - `self.built_in_apps` — keyed by canonical `name` (`saasure`, + `okta_enduser`). Each entry carries the resolved Authentication + Policy (Access Policy) and its rules. + - `self.integrated_apps` — lazily populated and keyed by application id. + Used by the per-application network-zone STIG to evaluate every + active app returned by `/api/v1/apps`. + + Required OAuth scopes (`REQUIRED_SCOPES`) are compared against the + access token's granted scopes (`provider.identity.granted_scopes`). + When a scope is known to be missing, the corresponding fetch is + skipped and recorded in `self.missing_scope` so each check can emit + an explicit MANUAL finding instead of a misleading + "no resources returned". Empty granted_scopes means "unknown" — the + service attempts the fetch and lets the SDK fail loudly. + """ + + def __init__(self, provider): + super().__init__(__class__.__name__, provider) + granted = set(getattr(provider.identity, "granted_scopes", None) or []) + self.missing_scope: dict[str, Optional[str]] = { + resource: (scope if granted and scope not in granted else None) + for resource, scope in REQUIRED_SCOPES.items() + } + + self.admin_console_app_settings: Optional[AdminConsoleAppSettings] = ( + None + if self.missing_scope["admin_console_app_settings"] + else self._get_admin_console_app_settings() + ) + + # Apps and policies share the same SDK round-trips, so fetch them + # together. When either scope is missing we still attempt the + # other, but `built_in_apps` is only populated when both are + # available — checks then look at `missing_scope` to report which + # one is at fault. + if self.missing_scope["built_in_apps"] or self.missing_scope["access_policies"]: + self.built_in_apps: dict[str, OktaBuiltInApp] = {} + else: + self.built_in_apps = self._list_built_in_apps_with_policies() + self._integrated_apps: Optional[dict[str, OktaBuiltInApp]] = None + + @property + def integrated_apps(self) -> dict[str, "OktaBuiltInApp"]: + """List every Okta-integrated app with its Authentication Policy. + + This is fetched lazily because only the V-279693 check needs the + full app inventory; the bundled Admin Console / Dashboard checks + only need the two built-in apps. + """ + if self._integrated_apps is None: + if ( + self.missing_scope["integrated_apps"] + or self.missing_scope["access_policies"] + ): + self._integrated_apps = {} + else: + self._integrated_apps = self._list_integrated_apps_with_policies() + return self._integrated_apps + + def _get_admin_console_app_settings(self) -> Optional["AdminConsoleAppSettings"]: + logger.info("Application - Fetching first-party Admin Console settings...") + try: + return self._run(self._fetch_admin_console_app_settings()) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return None + + async def _fetch_admin_console_app_settings( + self, + ) -> Optional["AdminConsoleAppSettings"]: + result = await self.client.get_first_party_app_settings( + ADMIN_CONSOLE_FIRST_PARTY_APP_KEY + ) + err = result[-1] + if err is not None: + # 404 means the org is on Classic engine or the endpoint isn't + # available — fall through to None and checks emit MANUAL. + logger.error(f"Error fetching first-party Admin Console settings: {err}") + return None + data = result[0] + if data is None: + return None + return AdminConsoleAppSettings( + session_idle_timeout_minutes=getattr( + data, "session_idle_timeout_minutes", None + ), + session_max_lifetime_minutes=getattr( + data, "session_max_lifetime_minutes", None + ), + ) + + def _list_built_in_apps_with_policies(self) -> dict: + logger.info("Application - Listing Okta built-in apps and policies...") + try: + return self._run(self._fetch_built_in_apps_and_policies()) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return {} + + def _list_integrated_apps_with_policies(self) -> dict: + logger.info("Application - Listing integrated Okta apps and policies...") + try: + return self._run(self._fetch_integrated_apps_and_policies()) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return {} + + async def _fetch_built_in_apps_and_policies(self) -> dict: + # Per-app try/except: one app's SDK failure (e.g. ValidationError + # while deserializing its policy rules) must not erase findings + # for the other. + result: dict[str, OktaBuiltInApp] = {} + for app_name in (ADMIN_CONSOLE_APP_NAME, DASHBOARD_APP_NAME): + try: + built_in_app = await self._fetch_built_in_app(app_name) + except Exception as error: + logger.error( + f"Error fetching built-in app {app_name}: " + f"{error.__class__.__name__}: {error}" + ) + continue + if built_in_app is None: + continue + if built_in_app.access_policy_id: + try: + built_in_app.access_policy = await self._fetch_access_policy( + built_in_app.access_policy_id + ) + except Exception as error: + logger.error( + f"Error fetching access policy " + f"{built_in_app.access_policy_id} for {app_name}: " + f"{error.__class__.__name__}: {error}" + ) + built_in_app.access_policy = None + result[app_name] = built_in_app + return result + + async def _fetch_integrated_apps_and_policies(self) -> dict: + all_apps, err = await self._paginate( + lambda after: self.client.list_applications(after=after) + ) + if err is not None: + logger.error(f"Error listing integrated apps: {err}") + return {} + + # Per-app try/except: a single app's policy fetch failure must + # not drop the whole inventory. + result: dict[str, OktaBuiltInApp] = {} + for app in all_apps: + try: + app_model = _to_application_model(app) + except Exception as error: + logger.error( + f"Error projecting Okta app onto pydantic model " + f"(id={getattr(app, 'id', '?')}): " + f"{error.__class__.__name__}: {error}" + ) + continue + if app_model.access_policy_id: + try: + app_model.access_policy = await self._fetch_access_policy( + app_model.access_policy_id + ) + except Exception as error: + logger.error( + f"Error fetching access policy " + f"{app_model.access_policy_id} for app " + f"{app_model.name} ({app_model.id}): " + f"{error.__class__.__name__}: {error}" + ) + app_model.access_policy = None + result[app_model.id] = app_model + return result + + async def _fetch_built_in_app(self, app_name: str) -> Optional["OktaBuiltInApp"]: + # Filter by `name eq` so we don't paginate every app in the org + # for a single match. The two OIN-built-in apps are uniquely + # identified by their internal `name`. + apps, err = await self._paginate( + lambda after: self.client.list_applications( + filter=f'name eq "{app_name}"', after=after + ) + ) + if err is not None: + logger.error(f"Error listing app with name={app_name}: {err}") + return None + if not apps: + return None + return _to_application_model(apps[0]) + + async def _fetch_access_policy( + self, policy_id: str + ) -> Optional["AuthenticationPolicy"]: + # Okta's `list_policy_rules` does not accept an `after` cursor in + # the SDK signature, so we call once with a generous limit. Auth + # policies almost always have <10 rules; a warning is logged if + # the limit is hit. + rule_fetch_limit = 100 + try: + result = await self.client.list_policy_rules( + policy_id, limit=str(rule_fetch_limit) + ) + except ValidationError as ve: + # Upstream Okta SDK ↔ Management API enum drift: the SDK's + # strict pydantic validators (e.g. KnowledgeConstraint.types + # uppercase-only) reject values the API returns lowercase + # (e.g. ["password"]). Fall back to a raw-JSON fetch so the + # STIG evaluation isn't blocked by an upstream SDK bug. + logger.warning( + f"Okta SDK raised ValidationError parsing rules for policy " + f"{policy_id} ({ve.error_count()} error(s)) — falling back " + "to raw-JSON parse. This is an okta-sdk-python deserialization " + "bug; the workaround should be removed once upstream fixes it." + ) + return await self._fetch_access_policy_raw(policy_id, rule_fetch_limit) + + err = result[-1] + if err is not None: + logger.error(f"Error listing rules for access policy {policy_id}: {err}") + return AuthenticationPolicy( + id=policy_id, + name="", + status="", + is_default=False, + rules=[], + ) + all_rules = list(result[0] or []) + if len(all_rules) >= rule_fetch_limit: + logger.warning( + f"Access policy {policy_id} returned {len(all_rules)} rules — " + f"the per-policy fetch limit ({rule_fetch_limit}) was hit; any " + "rules beyond this limit are not evaluated by Prowler. Review " + "the policy in the Okta Admin Console." + ) + rules_out = [_rule_to_model(rule) for rule in all_rules] + return AuthenticationPolicy( + id=policy_id, + name="", + status="", + is_default=False, + rules=rules_out, + ) + + async def _fetch_access_policy_raw( + self, policy_id: str, rule_fetch_limit: int + ) -> Optional["AuthenticationPolicy"]: + """Raw-JSON fallback for `list_policy_rules`. + + Bypasses the Okta SDK's typed deserialization by calling the + request executor directly without a response type. The response + body is then `json.loads`-ed and projected onto our own pydantic + snapshot, which only validates the fields the STIG checks + actually read. This keeps the checks evaluable on tenants where + the Management API returns values the SDK validators reject. + """ + request, error = await self.client._request_executor.create_request( + method="GET", + url=f"/api/v1/policies/{policy_id}/rules?limit={rule_fetch_limit}", + body=None, + headers={"Accept": "application/json"}, + ) + if error is not None: + logger.error( + f"Raw rules fetch (create_request) failed for {policy_id}: {error}" + ) + return AuthenticationPolicy( + id=policy_id, name="", status="", is_default=False, rules=[] + ) + + _response, response_body, error = await self.client._request_executor.execute( + request + ) + if error is not None: + logger.error(f"Raw rules fetch (execute) failed for {policy_id}: {error}") + return AuthenticationPolicy( + id=policy_id, name="", status="", is_default=False, rules=[] + ) + + if isinstance(response_body, (bytes, bytearray)): + try: + response_body = response_body.decode("utf-8") + except UnicodeDecodeError as decode_err: + logger.error( + f"Could not decode rules response for {policy_id}: {decode_err}" + ) + return AuthenticationPolicy( + id=policy_id, name="", status="", is_default=False, rules=[] + ) + try: + rules_data = json.loads(response_body) if response_body else [] + except json.JSONDecodeError as decode_err: + logger.error(f"Could not parse rules JSON for {policy_id}: {decode_err}") + return AuthenticationPolicy( + id=policy_id, name="", status="", is_default=False, rules=[] + ) + + if not isinstance(rules_data, list): + logger.error( + f"Unexpected raw rules payload shape for {policy_id}: " + f"got {type(rules_data).__name__}, expected list" + ) + return AuthenticationPolicy( + id=policy_id, name="", status="", is_default=False, rules=[] + ) + + if len(rules_data) >= rule_fetch_limit: + logger.warning( + f"Access policy {policy_id} returned {len(rules_data)} rules " + f"via raw-JSON fallback — the per-policy fetch limit " + f"({rule_fetch_limit}) was hit; any rules beyond this limit " + "are not evaluated by Prowler." + ) + rules_out = [_raw_rule_to_model(rule) for rule in rules_data] + return AuthenticationPolicy( + id=policy_id, name="", status="", is_default=False, rules=rules_out + ) + + @staticmethod + async def _paginate(fetch): + """Drain all pages of an SDK list call. + + `fetch` is a callable taking the `after` cursor (or None) and + returning the SDK's `(items, resp, err)` tuple. Follows the + `Link: rel="next"` header until exhausted. Mirrors the helper in + `signon_service`. + """ + all_items = [] + result = await fetch(None) + err = result[-1] + if err is not None: + return [], err + items = result[0] + resp = result[1] if len(result) >= 3 else None + all_items.extend(items or []) + while True: + cursor = _next_after_cursor(resp) + if not cursor: + break + result = await fetch(cursor) + err = result[-1] + if err is not None: + return all_items, err + items = result[0] + resp = result[1] if len(result) >= 3 else None + all_items.extend(items or []) + return all_items, None + + +def _policy_id_from_href(href: Optional[str]) -> Optional[str]: + """Extract the trailing policy id from `.../policies/{id}` URLs.""" + if not href: + return None + path = urlparse(href).path or href + segment = path.rstrip("/").rsplit("/", 1)[-1] + return segment or None + + +def _rule_to_model(rule) -> "AuthenticationPolicyRule": + """Project an SDK `AccessPolicyRule` onto our pydantic snapshot. + + Pulls out the two STIG-relevant fields from the deeply nested + `actions.appSignOn.verificationMethod` tree: the assurance `factor_mode` + and whether any possession constraint requires phishing resistance. + """ + actions = getattr(rule, "actions", None) + app_sign_on = getattr(actions, "app_sign_on", None) if actions else None + verification_method = ( + getattr(app_sign_on, "verification_method", None) if app_sign_on else None + ) + factor_mode = _stringify_enum(getattr(verification_method, "factor_mode", None)) + verification_type = _stringify_enum(getattr(verification_method, "type", None)) + constraints = list(getattr(verification_method, "constraints", None) or []) + phishing_resistant_required = False + for constraint in constraints: + possession = getattr(constraint, "possession", None) + if possession is None: + continue + if ( + _stringify_enum(getattr(possession, "phishing_resistant", None)) + == "REQUIRED" + ): + phishing_resistant_required = True + break + + access_action = getattr(app_sign_on, "access", None) if app_sign_on else None + conditions = getattr(rule, "conditions", None) + network = getattr(conditions, "network", None) if conditions else None + return AuthenticationPolicyRule( + id=getattr(rule, "id", "") or "", + name=getattr(rule, "name", "") or "", + priority=getattr(rule, "priority", None), + status=getattr(rule, "status", "") or "", + is_default=bool(getattr(rule, "system", False)), + factor_mode=factor_mode, + possession_phishing_resistant_required=phishing_resistant_required, + constraints_count=len(constraints), + verification_method_type=verification_type, + access=_stringify_enum(access_action), + network_connection=_stringify_enum(getattr(network, "connection", None)), + network_zones_include=list(getattr(network, "include", None) or []), + network_zones_exclude=list(getattr(network, "exclude", None) or []), + ) + + +def _stringify_enum(value) -> Optional[str]: + """Return the string form of an enum-or-string value, or None.""" + if value is None: + return None + return getattr(value, "value", None) or str(value) + + +def _raw_rule_to_model(rule_dict: dict) -> "AuthenticationPolicyRule": + """Project a raw `/api/v1/policies/{id}/rules` JSON rule onto our model. + + Mirrors `_rule_to_model` but reads camelCase JSON keys (`appSignOn`, + `verificationMethod`, `phishingResistant`) instead of the SDK's + snake_case attribute names. Used by the raw-JSON fallback that + activates when the Okta SDK's strict enum validators reject values + the Management API returns. + """ + actions = rule_dict.get("actions") or {} + app_sign_on = actions.get("appSignOn") or {} + verification_method = app_sign_on.get("verificationMethod") or {} + factor_mode = verification_method.get("factorMode") + verification_type = verification_method.get("type") + constraints = verification_method.get("constraints") or [] + phishing_resistant_required = False + for constraint in constraints: + possession = (constraint or {}).get("possession") or {} + if possession.get("phishingResistant") == "REQUIRED": + phishing_resistant_required = True + break + + access_action = app_sign_on.get("access") + conditions = rule_dict.get("conditions") or {} + network = conditions.get("network") or {} + return AuthenticationPolicyRule( + id=rule_dict.get("id") or "", + name=rule_dict.get("name") or "", + priority=rule_dict.get("priority"), + status=rule_dict.get("status") or "", + is_default=bool(rule_dict.get("system", False)), + factor_mode=factor_mode, + possession_phishing_resistant_required=phishing_resistant_required, + constraints_count=len(constraints), + verification_method_type=verification_type, + access=access_action, + network_connection=network.get("connection"), + network_zones_include=list(network.get("include") or []), + network_zones_exclude=list(network.get("exclude") or []), + ) + + +class AdminConsoleAppSettings(BaseModel): + """First-party Okta Admin Console session settings. + + `id` and `name` are set to fixed sentinels so this can be passed as + the `resource` to `CheckReportOkta`, which reads those attributes. + """ + + id: str = "okta-admin-console-app-settings" + name: str = "Okta Admin Console (first-party app settings)" + session_idle_timeout_minutes: Optional[int] = None + session_max_lifetime_minutes: Optional[int] = None + + +class AuthenticationPolicyRule(BaseModel): + id: str + name: str + priority: Optional[int] = None + status: str = "" + is_default: bool = False + factor_mode: Optional[str] = None + possession_phishing_resistant_required: bool = False + constraints_count: int = 0 + verification_method_type: Optional[str] = None + access: Optional[str] = None + network_connection: Optional[str] = None + network_zones_include: list[str] = [] + network_zones_exclude: list[str] = [] + + +class AuthenticationPolicy(BaseModel): + id: str + name: str = "" + status: str = "" + is_default: bool = False + rules: list[AuthenticationPolicyRule] = [] + + +class OktaBuiltInApp(BaseModel): + # `id` matches the Okta-generated `0oa…` app identifier; `name` is the + # canonical internal name (`saasure`, `okta_enduser`). Both are read + # directly by `CheckReportOkta(resource=…)`. + id: str + name: str + label: str = "" + status: str = "" + access_policy_id: Optional[str] = None + access_policy: Optional[AuthenticationPolicy] = None + + +def _application_access_policy_id(app) -> Optional[str]: + links = getattr(app, "links", None) + access_policy_link = getattr(links, "access_policy", None) if links else None + return _policy_id_from_href( + getattr(access_policy_link, "href", None) if access_policy_link else None + ) + + +def _to_application_model(app) -> OktaBuiltInApp: + return OktaBuiltInApp( + id=getattr(app, "id", "") or "", + name=getattr(app, "name", "") or "", + label=getattr(app, "label", "") or "", + status=getattr(app, "status", "") or "", + access_policy_id=_application_access_policy_id(app), + ) diff --git a/prowler/providers/okta/services/application/lib/__init__.py b/prowler/providers/okta/services/application/lib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/okta/services/application/lib/application_helpers.py b/prowler/providers/okta/services/application/lib/application_helpers.py new file mode 100644 index 0000000000..90cccc98ec --- /dev/null +++ b/prowler/providers/okta/services/application/lib/application_helpers.py @@ -0,0 +1,212 @@ +"""Shared helpers for the OKTA application STIG checks.""" + +from typing import Optional + +from prowler.lib.check.models import CheckReportOkta +from prowler.providers.okta.services.application.application_service import ( + AdminConsoleAppSettings, + AuthenticationPolicyRule, + OktaBuiltInApp, +) + + +def active_apps(apps: dict[str, OktaBuiltInApp]) -> list[OktaBuiltInApp]: + """Return active apps sorted by label/name, id as tiebreaker.""" + return sorted( + [ + app + for app in apps.values() + if not app.status or app.status.upper() == "ACTIVE" + ], + key=lambda app: (app.label or app.name, app.id), + ) + + +def top_active_rule( + app: OktaBuiltInApp, +) -> Optional[AuthenticationPolicyRule]: + """Return the topmost active rule on the app's Authentication Policy. + + Mirrors the STIG fix text — *"Click the 'Actions' button next to the + top rule and select 'Edit'"* — by returning the active rule with the + lowest `priority` value (= highest precedence). Downstream checks + separately reject the rule when it is the built-in Catch-all Rule. + + The priority value itself is intentionally not pinned to a specific + integer. Okta indexes Access Policy rule priorities inconsistently + across tenants and policy types (some responses report `0` for the + top rule, others `1`); the STIG only requires that the topmost rule + satisfy the predicate, not that it carry any specific priority literal. + """ + if app.access_policy is None: + return None + active_rules = sorted( + [ + rule + for rule in app.access_policy.rules + if not rule.status or rule.status.upper() == "ACTIVE" + ], + key=lambda rule: ( + rule.priority if rule.priority is not None else float("inf"), + rule.name, + ), + ) + if not active_rules: + return None + return active_rules[0] + + +def app_label(app: OktaBuiltInApp) -> str: + """Format a human-readable label for an Okta application.""" + label = app.label or app.name + return f"Okta app '{label}' (app={app.name}, id={app.id})" + + +def rule_label(rule: AuthenticationPolicyRule) -> str: + """Format whether a rule is the built-in catch-all or a custom rule.""" + if rule.is_default or rule.name == "Catch-all Rule": + return f"built-in Catch-all Rule '{rule.name}'" + return f"non-default rule '{rule.name}'" + + +def rule_has_network_zone(rule: AuthenticationPolicyRule) -> bool: + """Return True when the rule maps User's IP to at least one Network Zone.""" + return bool(rule.network_zones_include or rule.network_zones_exclude) + + +_SCOPE_ADVICE = ( + "Grant it on the service app's Okta API Scopes tab in the Okta Admin " + "Console, then re-run the check." +) + + +def missing_app_scope_finding( + metadata, org_domain: str, scope: str, app_label_hint: str +) -> CheckReportOkta: + """Build the MANUAL finding when an app/policy scope is not granted.""" + placeholder = OktaBuiltInApp( + id="okta-built-in-app-scope-missing", + name="(scope not granted)", + label=app_label_hint, + status="MISSING", + ) + report = CheckReportOkta( + metadata=metadata, resource=placeholder, org_domain=org_domain + ) + report.status = "MANUAL" + report.status_extended = ( + f"Could not evaluate the authentication policy for {app_label_hint}: " + f"the Okta service app is missing the required `{scope}` API scope. " + f"{_SCOPE_ADVICE}" + ) + return report + + +def missing_integrated_apps_scope_finding( + metadata, org_domain: str, scope: str +) -> CheckReportOkta: + """Build the MANUAL finding when the integrated-app inventory scope is not granted.""" + placeholder = OktaBuiltInApp( + id="okta-integrated-apps-scope-missing", + name="(scope not granted)", + label="Okta integrated applications", + status="MISSING", + ) + report = CheckReportOkta( + metadata=metadata, + resource=placeholder, + org_domain=org_domain, + resource_name=placeholder.label, + resource_id=placeholder.id, + ) + report.status = "MANUAL" + report.status_extended = ( + "Could not retrieve Okta integrated applications and their " + f"authentication policies: the Okta service app is missing the " + f"required `{scope}` API scope. {_SCOPE_ADVICE}" + ) + return report + + +def missing_admin_console_settings_scope_finding( + metadata, org_domain: str, scope: str +) -> CheckReportOkta: + """Build the MANUAL finding for the Admin Console idle timeout check when scope is missing.""" + placeholder = AdminConsoleAppSettings() + report = CheckReportOkta( + metadata=metadata, resource=placeholder, org_domain=org_domain + ) + report.status = "MANUAL" + report.status_extended = ( + "Could not retrieve the Okta Admin Console first-party app settings: " + f"the Okta service app is missing the required `{scope}` API scope. " + f"{_SCOPE_ADVICE}" + ) + return report + + +def app_not_found_finding( + metadata, org_domain: str, app_label_hint: str +) -> CheckReportOkta: + """Build the MANUAL finding emitted when a built-in OIN app isn't returned. + + Okta filters the first-party apps (`saasure`, `okta_enduser`) out of + `/api/v1/apps` for every admin role below Super Administrator, so the + check has no way to resolve the app's bound Authentication Policy. + """ + placeholder = OktaBuiltInApp( + id="okta-built-in-app-missing", + name="(app not found)", + label=app_label_hint, + status="MISSING", + ) + report = CheckReportOkta( + metadata=metadata, resource=placeholder, org_domain=org_domain + ) + report.status = "MANUAL" + report.status_extended = ( + f"The {app_label_hint} first-party app was not returned by the Okta " + "API. Okta restricts the visibility of first-party apps " + "(`saasure`, `okta_enduser`) to the Super Administrator role; " + "every other role — including Read-Only Administrator — receives " + "an empty result. Assign Super Administrator to the service app " + "to evaluate this check." + ) + return report + + +def no_active_apps_finding(metadata, org_domain: str) -> CheckReportOkta: + """Build the MANUAL finding emitted when no active apps are returned.""" + placeholder = OktaBuiltInApp( + id="okta-apps-missing", + name="(no active apps)", + label="Okta applications", + status="MISSING", + ) + report = CheckReportOkta( + metadata=metadata, + resource=placeholder, + org_domain=org_domain, + resource_name=placeholder.label, + resource_id=placeholder.id, + ) + report.status = "MANUAL" + report.status_extended = ( + "No active Okta applications were returned by the API. Verify the " + "tenant exposes applications to the Read-Only Administrator role and " + "review the application inventory manually." + ) + return report + + +def policy_missing_finding( + metadata, org_domain: str, app: OktaBuiltInApp +) -> CheckReportOkta: + """Build the FAIL finding when the built-in app has no bound Access Policy.""" + report = CheckReportOkta(metadata=metadata, resource=app, org_domain=org_domain) + report.status = "FAIL" + report.status_extended = ( + f"{app_label(app)} has no Authentication Policy bound to it. " + "Bind an Access Policy in Security > Authentication Policies." + ) + return report diff --git a/prowler/providers/okta/services/signon/__init__.py b/prowler/providers/okta/services/signon/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/okta/services/signon/lib/__init__.py b/prowler/providers/okta/services/signon/lib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/okta/services/signon/lib/signon_helpers.py b/prowler/providers/okta/services/signon/lib/signon_helpers.py new file mode 100644 index 0000000000..7c84fe5733 --- /dev/null +++ b/prowler/providers/okta/services/signon/lib/signon_helpers.py @@ -0,0 +1,146 @@ +"""Shared helpers for the OKTA sign-on STIG checks. + +The four `signon_global_session_*` checks share the same plumbing: +they iterate active Global Session Policies in priority order, locate +each policy's Priority 1 active rule, and emit one finding per policy. +This module centralises that plumbing so each check can stay focused +on its STIG-specific predicate. +""" + +from typing import Optional + +from prowler.lib.check.models import CheckReportOkta +from prowler.providers.okta.services.signon.signon_service import ( + GlobalSessionPolicy, + GlobalSessionPolicyRule, + SignInPage, +) + + +def active_policies( + global_session_policies: dict[str, GlobalSessionPolicy], +) -> list[GlobalSessionPolicy]: + """Return active policies sorted by priority (ascending, name as tiebreaker). + + A policy with no `status` is treated as ACTIVE because the Okta SDK + sometimes omits the field on default policies. + """ + return sorted( + [ + policy + for policy in global_session_policies.values() + if not policy.status or policy.status.upper() == "ACTIVE" + ], + key=lambda policy: ( + policy.priority if policy.priority is not None else float("inf"), + policy.name, + ), + ) + + +def priority_one_active_rule( + policy: GlobalSessionPolicy, +) -> Optional[GlobalSessionPolicyRule]: + """Return the policy's Priority 1 active rule, or None. + + Okta's evaluator skips inactive rules, so we first filter to active + rules and pick the highest-priority one. If that rule is not at + priority 1 we return None — the policy effectively has no + priority-1 rule for evaluation purposes. + """ + active_rules = sorted( + [ + rule + for rule in policy.rules + if not rule.status or rule.status.upper() == "ACTIVE" + ], + key=lambda rule: ( + rule.priority if rule.priority is not None else float("inf"), + rule.name, + ), + ) + if not active_rules: + return None + candidate = active_rules[0] + if candidate.priority != 1: + return None + return candidate + + +def policy_label(policy: GlobalSessionPolicy) -> str: + kind = "default" if policy.is_default else "custom" + priority = policy.priority if policy.priority is not None else "unset" + return f"Global Session Policy '{policy.name}' (priority {priority}, {kind})" + + +def no_active_policies_finding( + metadata, org_domain: str, status_extended: str +) -> CheckReportOkta: + """Build the FAIL finding emitted when no active sign-on policies exist.""" + placeholder = GlobalSessionPolicy( + id="signon-policies-missing", + name="(no active sign-on policies)", + priority=1, + status="MISSING", + is_default=False, + rules=[], + ) + report = CheckReportOkta( + metadata=metadata, resource=placeholder, org_domain=org_domain + ) + report.status = "FAIL" + report.status_extended = status_extended + return report + + +_SCOPE_ADVICE = ( + "Grant it on the service app's Okta API Scopes tab in the Okta Admin " + "Console, then re-run the check." +) + + +def missing_policy_scope_finding( + metadata, org_domain: str, scope: str +) -> CheckReportOkta: + """Build the MANUAL finding for a sign-on policy check when the API scope is not granted.""" + placeholder = GlobalSessionPolicy( + id="signon-policies-scope-missing", + name="(scope not granted)", + priority=1, + status="MISSING", + is_default=False, + rules=[], + ) + report = CheckReportOkta( + metadata=metadata, resource=placeholder, org_domain=org_domain + ) + report.status = "MANUAL" + report.status_extended = ( + f"Could not retrieve Global Session Policies: the Okta service app " + f"is missing the required `{scope}` API scope. {_SCOPE_ADVICE}" + ) + return report + + +def missing_brand_scope_finding( + metadata, org_domain: str, scope: str +) -> CheckReportOkta: + """Build the MANUAL finding for a brand/sign-in-page check when the API scope is not granted.""" + placeholder = SignInPage( + brand_id="signon-brands-scope-missing", + brand_name="(scope not granted)", + is_customized=False, + ) + report = CheckReportOkta( + metadata=metadata, + resource=placeholder, + org_domain=org_domain, + resource_name=placeholder.brand_name, + resource_id=placeholder.brand_id, + ) + report.status = "MANUAL" + report.status_extended = ( + f"Could not retrieve Okta brand sign-in pages: the Okta service app " + f"is missing the required `{scope}` API scope. {_SCOPE_ADVICE}" + ) + return report diff --git a/prowler/providers/okta/services/signon/signon_client.py b/prowler/providers/okta/services/signon/signon_client.py new file mode 100644 index 0000000000..b64a15d8da --- /dev/null +++ b/prowler/providers/okta/services/signon/signon_client.py @@ -0,0 +1,4 @@ +from prowler.providers.common.provider import Provider +from prowler.providers.okta.services.signon.signon_service import Signon + +signon_client = Signon(Provider.get_global_provider()) diff --git a/prowler/providers/okta/services/signon/signon_dod_warning_banner_configured/__init__.py b/prowler/providers/okta/services/signon/signon_dod_warning_banner_configured/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/okta/services/signon/signon_dod_warning_banner_configured/signon_dod_warning_banner_configured.metadata.json b/prowler/providers/okta/services/signon/signon_dod_warning_banner_configured/signon_dod_warning_banner_configured.metadata.json new file mode 100644 index 0000000000..523795c8ac --- /dev/null +++ b/prowler/providers/okta/services/signon/signon_dod_warning_banner_configured/signon_dod_warning_banner_configured.metadata.json @@ -0,0 +1,38 @@ +{ + "Provider": "okta", + "CheckID": "signon_dod_warning_banner_configured", + "CheckTitle": "Okta sign-in page displays the Standard Mandatory DOD Notice and Consent Banner", + "CheckType": [], + "ServiceName": "signon", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "informational", + "ResourceType": "NotDefined", + "ResourceGroup": "governance", + "Description": "Each Okta brand's sign-in page must present the **Standard Mandatory DOD Notice and Consent Banner** (`DTM-08-060`) before granting access.\n\nThe check inspects the sign-in page HTML returned by the Okta Management API, using the *customized* page when present and otherwise falling back to the *default* sign-in page.\n\nAligns with **DISA STIG V-273192 / OKTA-APP-000200**.", + "Risk": "Without the **DOD Notice and Consent Banner**, users are not informed that the system is a U.S. Government information system subject to monitoring.\n\n- **Legal basis** for incident response and prosecution is weakened\n- **Alignment** with federal laws, Executive Orders, directives, and standards is broken\n- **Implied consent** to monitoring cannot be asserted on connection", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://help.okta.com/oie/en-us/content/topics/settings/settings-customization.htm", + "https://developer.okta.com/docs/api/openapi/okta-management/management/tag/CustomPages/", + "https://developer.okta.com/docs/api/openapi/okta-management/management/tag/Brands/" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Follow the supplemental *Okta DOD Warning Banner Configuration Guide* shipped with the **DISA Okta IDaaS STIG** package.\n2. Sign in to the **Okta Admin Console** as a *Super Admin*.\n3. Navigate to **Customizations** > **Brands** and select the brand.\n4. Edit the **Sign-in page** customization.\n5. Insert the full `DTM-08-060` Standard Mandatory DOD Notice and Consent Banner text into the page content.\n6. Publish the customization and verify the banner is presented before sign-in.", + "Terraform": "" + }, + "Recommendation": { + "Text": "Customize the Okta sign-in page for **each brand** to display the **Standard Mandatory DOD Notice and Consent Banner** (`DTM-08-060`) before users authenticate.\n\nApplies only to Okta tenants under **U.S. Department of Defense** scope; non-DOD organizations can mute this check.", + "Url": "https://hub.prowler.com/check/signon_dod_warning_banner_configured" + } + }, + "Categories": [ + "identity-access" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Applicable only to Okta tenants under U.S. Department of Defense scope (DISA Okta IDaaS STIG, control V-273192 / OKTA-APP-000200). For non-DOD organizations this check is not applicable and can be muted." +} diff --git a/prowler/providers/okta/services/signon/signon_dod_warning_banner_configured/signon_dod_warning_banner_configured.py b/prowler/providers/okta/services/signon/signon_dod_warning_banner_configured/signon_dod_warning_banner_configured.py new file mode 100644 index 0000000000..347f04f245 --- /dev/null +++ b/prowler/providers/okta/services/signon/signon_dod_warning_banner_configured/signon_dod_warning_banner_configured.py @@ -0,0 +1,129 @@ +from prowler.lib.check.models import Check, CheckReportOkta +from prowler.providers.okta.services.signon.lib.signon_helpers import ( + missing_brand_scope_finding, +) +from prowler.providers.okta.services.signon.signon_client import signon_client +from prowler.providers.okta.services.signon.signon_service import SignInPage + +# Distinctive marker groups drawn from the DTM-08-060 Standard Mandatory +# DOD Notice and Consent Banner. The HTML can vary across brands, so the +# check looks for the banner's core ideas rather than requiring an exact +# string match. +BANNER_MARKER_GROUPS = ( + ("u.s. government", "us government"), + ("information system", "information systems"), + ("authorized use only", "authorized use"), + ( + "subject to monitoring", + "may be intercepted", + "searched, monitored, and recorded", + "consent to monitoring", + ), +) + + +def _matched_banner_groups(content_lower: str) -> list[str]: + matched_markers: list[str] = [] + for marker_group in BANNER_MARKER_GROUPS: + for marker in marker_group: + if marker in content_lower: + matched_markers.append(marker) + break + return matched_markers + + +class signon_dod_warning_banner_configured(Check): + """STIG V-273192 / OKTA-APP-000200. + + Okta must display the Standard Mandatory DOD Notice and Consent + Banner (DTM-08-060) before granting access to the application. The + check inspects each brand's sign-in page HTML returned by the Okta + Management API, using the customized page when present and otherwise + falling back to the default sign-in page. + """ + + def execute(self) -> list[CheckReportOkta]: + org_domain = signon_client.provider.identity.org_domain + findings: list[CheckReportOkta] = [] + + missing_scope = signon_client.missing_scope.get("sign_in_pages") + if missing_scope: + return [ + missing_brand_scope_finding(self.metadata(), org_domain, missing_scope) + ] + + if not signon_client.sign_in_pages: + placeholder = SignInPage( + brand_id="no-brands-detected", + brand_name="(no brands detected)", + is_customized=False, + ) + report = CheckReportOkta( + metadata=self.metadata(), + resource=placeholder, + org_domain=org_domain, + resource_name=placeholder.brand_name, + resource_id=placeholder.brand_id, + ) + report.status = "MANUAL" + report.status_extended = ( + "No Okta brands were retrieved from the Brands API. Verify " + "the sign-in page for the organization displays the DOD " + "Notice and Consent Banner (DTM-08-060) in the Admin Console." + ) + findings.append(report) + return findings + + for page in signon_client.sign_in_pages.values(): + report = CheckReportOkta( + metadata=self.metadata(), + resource=page, + org_domain=org_domain, + resource_name=page.brand_name or page.brand_id, + resource_id=page.brand_id, + ) + + if page.fetch_error: + report.status = "MANUAL" + report.status_extended = ( + f"Could not retrieve the sign-in page for " + f"brand '{page.brand_name or page.brand_id}' ({page.fetch_error}). " + "Inspect the brand manually to confirm the " + "DOD Notice and Consent Banner (DTM-08-060) is displayed." + ) + findings.append(report) + continue + + if not page.page_content: + report.status = "MANUAL" + report.status_extended = ( + f"Sign-in page content for brand " + f"'{page.brand_name or page.brand_id}' could not be " + "retrieved from the Okta API. Verify the DOD Notice and " + "Consent Banner (DTM-08-060) manually in the Admin Console." + ) + findings.append(report) + continue + + page_type = "customized" if page.is_customized else "default" + content_lower = page.page_content.lower() + matches = _matched_banner_groups(content_lower) + + if len(matches) == len(BANNER_MARKER_GROUPS): + report.status = "PASS" + report.status_extended = ( + f"DOD Notice and Consent Banner detected on the {page_type} " + f"sign-in page for brand '{page.brand_name or page.brand_id}' " + f"({len(matches)} of {len(BANNER_MARKER_GROUPS)} required " + "marker groups matched)." + ) + else: + report.status = "FAIL" + report.status_extended = ( + f"{page_type.title()} sign-in page for brand " + f"'{page.brand_name or page.brand_id}' does not contain " + "the DOD Notice and Consent Banner (DTM-08-060)." + ) + findings.append(report) + + return findings diff --git a/prowler/providers/okta/services/signon/signon_global_session_cookies_not_persistent/__init__.py b/prowler/providers/okta/services/signon/signon_global_session_cookies_not_persistent/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/okta/services/signon/signon_global_session_cookies_not_persistent/signon_global_session_cookies_not_persistent.metadata.json b/prowler/providers/okta/services/signon/signon_global_session_cookies_not_persistent/signon_global_session_cookies_not_persistent.metadata.json new file mode 100644 index 0000000000..c4738fd5bf --- /dev/null +++ b/prowler/providers/okta/services/signon/signon_global_session_cookies_not_persistent/signon_global_session_cookies_not_persistent.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "okta", + "CheckID": "signon_global_session_cookies_not_persistent", + "CheckTitle": "Default Global Session Policy has a Priority 1 non-default rule disabling persistent global session cookies", + "CheckType": [], + "ServiceName": "signon", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "governance", + "Description": "Every active Okta **Global Session Policy** needs a **Priority 1** rule that is **not** the built-in `Default Rule`, setting *Okta global session cookies persist across browser sessions* to `Disabled`.\n\nOkta evaluates policies by group assignment, so a permissive custom policy can govern users. Aligns with **DISA STIG V-273206 / OKTA-APP-001710**.", + "Risk": "Persistent global session cookies keep an authenticated Okta session alive across browser restarts.\n\n- **Surviving sessions** outlive the browsing context the user expected\n- **Cached authorization decisions** remain in effect after the browser closes\n- **Forgotten or shared devices** continue to hold authenticated access until cookies expire on their own", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://help.okta.com/oie/en-us/content/topics/identity-engine/policies/about-okta-sign-on-policies.htm", + "https://developer.okta.com/docs/api/openapi/okta-management/management/tag/Policy/" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the **Okta Admin Console** as a *Super Admin*.\n2. Navigate to **Security** > **Global Session Policy**.\n3. Open the **Default Policy** (and repeat for every other active policy).\n4. Add or edit a non-default rule.\n5. Move that rule to **Priority 1** so it is evaluated before the built-in `Default Rule`.\n6. Set *Okta global session cookies persist across browser sessions* to `Disabled`.\n7. Save the rule.", + "Terraform": "```hcl\nresource \"okta_policy_rule_signon\" \"\" {\n policy_id = okta_policy_signon.default.id\n name = \"\"\n status = \"ACTIVE\"\n priority = 1 # Critical: rule must sit at Priority 1 before the Default Rule\n session_persistent = false # Critical: disable persistent global session cookies\n}\n```" + }, + "Recommendation": { + "Text": "Configure each active **Global Session Policy** so a non-default rule at **Priority 1**:\n- Sets *Okta global session cookies persist across browser sessions* to `Disabled`\n- Is enabled (`ACTIVE`) and evaluated before the built-in `Default Rule`\n\nReview group assignments to confirm the rule actually governs the intended users.", + "Url": "https://hub.prowler.com/check/signon_global_session_cookies_not_persistent" + } + }, + "Categories": [ + "identity-access" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/okta/services/signon/signon_global_session_cookies_not_persistent/signon_global_session_cookies_not_persistent.py b/prowler/providers/okta/services/signon/signon_global_session_cookies_not_persistent/signon_global_session_cookies_not_persistent.py new file mode 100644 index 0000000000..c5ffd849b5 --- /dev/null +++ b/prowler/providers/okta/services/signon/signon_global_session_cookies_not_persistent/signon_global_session_cookies_not_persistent.py @@ -0,0 +1,100 @@ +from prowler.lib.check.models import Check, CheckReportOkta +from prowler.providers.okta.services.signon.lib.signon_helpers import ( + active_policies, + missing_policy_scope_finding, + no_active_policies_finding, + policy_label, + priority_one_active_rule, +) +from prowler.providers.okta.services.signon.signon_client import signon_client +from prowler.providers.okta.services.signon.signon_service import GlobalSessionPolicy + + +class signon_global_session_cookies_not_persistent(Check): + """STIG V-273206 / OKTA-APP-001710. + + Every active Global Session Policy must have an active Priority 1 + rule that is not the built-in Default Rule, and that rule must + disable persistent global session cookies so the session does not + survive across browser restarts. + + Okta evaluates sign-on policies in priority order based on group + assignments, so a permissive custom policy can govern a user's + session even when the Default Policy is strict. The check emits one + finding per active policy to surface that risk. + """ + + def execute(self) -> list[CheckReportOkta]: + org_domain = signon_client.provider.identity.org_domain + + missing_scope = signon_client.missing_scope.get("global_session_policies") + if missing_scope: + return [ + missing_policy_scope_finding(self.metadata(), org_domain, missing_scope) + ] + + policies = active_policies(signon_client.global_session_policies) + if not policies: + return [ + no_active_policies_finding( + self.metadata(), + org_domain, + "No active Okta Global Session Policies were returned by the API. " + "STIG V-273206 requires the policy that governs each user to enforce " + "a Priority 1 non-default rule that disables persistent global " + "session cookies.", + ) + ] + + findings: list[CheckReportOkta] = [] + for policy in policies: + report = CheckReportOkta( + metadata=self.metadata(), resource=policy, org_domain=org_domain + ) + status, status_extended = _evaluate_policy(policy) + report.status = status + report.status_extended = status_extended + findings.append(report) + return findings + + +def _evaluate_policy(policy: GlobalSessionPolicy) -> tuple[str, str]: + label = policy_label(policy) + rule = priority_one_active_rule(policy) + + if rule is None: + return ( + "FAIL", + f"{label} has no Priority 1 active rule. STIG V-273206 requires " + "a non-default Priority 1 rule that disables persistent global " + "session cookies.", + ) + + if rule.is_default or rule.name == "Default Rule": + return ( + "FAIL", + f"{label} uses '{rule.name}' as its active Priority 1 rule. " + "The STIG requires a non-default Priority 1 rule.", + ) + + use_persistent_cookie = rule.use_persistent_cookie + if use_persistent_cookie is None: + return ( + "FAIL", + f"Priority 1 non-default rule '{rule.name}' in {label} " + "does not assert the 'Okta global session cookies persist across " + "browser sessions' setting.", + ) + + if use_persistent_cookie is False: + return ( + "PASS", + f"Priority 1 non-default rule '{rule.name}' in {label} " + "disables persistent global session cookies.", + ) + return ( + "FAIL", + f"Priority 1 non-default rule '{rule.name}' in {label} " + "allows persistent global session cookies, leaving the session active " + "across browser restarts.", + ) diff --git a/prowler/providers/okta/services/signon/signon_global_session_idle_timeout_15min/__init__.py b/prowler/providers/okta/services/signon/signon_global_session_idle_timeout_15min/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/okta/services/signon/signon_global_session_idle_timeout_15min/signon_global_session_idle_timeout_15min.metadata.json b/prowler/providers/okta/services/signon/signon_global_session_idle_timeout_15min/signon_global_session_idle_timeout_15min.metadata.json new file mode 100644 index 0000000000..f31a835864 --- /dev/null +++ b/prowler/providers/okta/services/signon/signon_global_session_idle_timeout_15min/signon_global_session_idle_timeout_15min.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "okta", + "CheckID": "signon_global_session_idle_timeout_15min", + "CheckTitle": "Default Global Session Policy has a Priority 1 non-default rule enforcing 15-minute idle timeout", + "CheckType": [], + "ServiceName": "signon", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "governance", + "Description": "Every active Okta **Global Session Policy** needs a **Priority 1** rule that is **not** the built-in `Default Rule`, setting *Maximum Okta global session idle time* to `15` minutes or less.\n\nOkta evaluates policies by group assignment, so a permissive custom policy can govern users. Threshold override: `okta_max_session_idle_minutes`. Aligns with **DISA STIG V-273186**.", + "Risk": "Without a `15`-minute idle timeout, an unattended workstation leaves an authenticated Okta session open indefinitely.\n\n- **Session takeover** of the user's identity by anyone with physical or remote access\n- **Lateral movement** into every downstream application that trusts Okta SSO\n- **Bypassed reauthentication** even after the user has stepped away", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://help.okta.com/oie/en-us/content/topics/identity-engine/policies/about-okta-sign-on-policies.htm", + "https://developer.okta.com/docs/api/openapi/okta-management/management/tag/Policy/" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the **Okta Admin Console** as a *Super Admin*.\n2. Navigate to **Security** > **Global Session Policy**.\n3. Open the **Default Policy** (and repeat for every other active policy).\n4. Add or edit a non-default rule.\n5. Move that rule to **Priority 1** so it is evaluated before the built-in `Default Rule`.\n6. Set *Maximum Okta global session idle time* to `15` minutes or less.\n7. Save the rule.", + "Terraform": "```hcl\nresource \"okta_policy_rule_signon\" \"\" {\n policy_id = okta_policy_signon.default.id\n name = \"\"\n status = \"ACTIVE\"\n priority = 1 # Critical: rule must sit at Priority 1 before the Default Rule\n session_idle = 15 # Critical: enforce idle timeout at 15 minutes or less\n session_persistent = false # Critical: avoid persistent global session cookies\n}\n```" + }, + "Recommendation": { + "Text": "Configure each active **Global Session Policy** so a non-default rule at **Priority 1**:\n- Sets *Maximum Okta global session idle time* to `15` minutes or less\n- Is enabled (`ACTIVE`) and evaluated before the built-in `Default Rule`\n\nReview group assignments to confirm the rule actually governs the intended users.", + "Url": "https://hub.prowler.com/check/signon_global_session_idle_timeout_15min" + } + }, + "Categories": [ + "identity-access" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/okta/services/signon/signon_global_session_idle_timeout_15min/signon_global_session_idle_timeout_15min.py b/prowler/providers/okta/services/signon/signon_global_session_idle_timeout_15min/signon_global_session_idle_timeout_15min.py new file mode 100644 index 0000000000..3f20a22adb --- /dev/null +++ b/prowler/providers/okta/services/signon/signon_global_session_idle_timeout_15min/signon_global_session_idle_timeout_15min.py @@ -0,0 +1,106 @@ +from prowler.lib.check.models import Check, CheckReportOkta +from prowler.providers.okta.services.signon.lib.signon_helpers import ( + active_policies, + missing_policy_scope_finding, + no_active_policies_finding, + policy_label, + priority_one_active_rule, +) +from prowler.providers.okta.services.signon.signon_client import signon_client +from prowler.providers.okta.services.signon.signon_service import GlobalSessionPolicy + +DEFAULT_THRESHOLD_MINUTES = 15 + + +class signon_global_session_idle_timeout_15min(Check): + """STIG V-273186 / OKTA-APP-000020. + + Every active Global Session Policy must have an active Priority 1 + rule that is not the built-in Default Rule, and that rule must set + the maximum Okta global session idle time to the configured + threshold or lower (defaults to 15 minutes per STIG; override via + `okta_max_session_idle_minutes` in the audit config). + + Okta evaluates sign-on policies in priority order based on group + assignments, so a permissive custom policy can govern a user's + session even when the Default Policy is strict. The check emits one + finding per active policy to surface that risk. + """ + + def execute(self) -> list[CheckReportOkta]: + audit_config = signon_client.audit_config or {} + threshold = audit_config.get( + "okta_max_session_idle_minutes", DEFAULT_THRESHOLD_MINUTES + ) + org_domain = signon_client.provider.identity.org_domain + + missing_scope = signon_client.missing_scope.get("global_session_policies") + if missing_scope: + return [ + missing_policy_scope_finding(self.metadata(), org_domain, missing_scope) + ] + + policies = active_policies(signon_client.global_session_policies) + if not policies: + return [ + no_active_policies_finding( + self.metadata(), + org_domain, + "No active Okta Global Session Policies were returned by the API. " + "STIG V-273186 requires the policy that governs each user to enforce " + "a Priority 1 non-default rule with a 15-minute idle timeout.", + ) + ] + + findings: list[CheckReportOkta] = [] + for policy in policies: + report = CheckReportOkta( + metadata=self.metadata(), resource=policy, org_domain=org_domain + ) + status, status_extended = _evaluate_policy(policy, threshold) + report.status = status + report.status_extended = status_extended + findings.append(report) + return findings + + +def _evaluate_policy(policy: GlobalSessionPolicy, threshold: int) -> tuple[str, str]: + label = policy_label(policy) + rule = priority_one_active_rule(policy) + + if rule is None: + return ( + "FAIL", + f"{label} has no Priority 1 active rule. STIG V-273186 requires " + f"a non-default Priority 1 rule with idle timeout <= {threshold} " + "minutes.", + ) + + if rule.is_default or rule.name == "Default Rule": + return ( + "FAIL", + f"{label} uses '{rule.name}' as its active Priority 1 rule. " + "The STIG requires a non-default Priority 1 rule.", + ) + + idle_timeout = rule.max_session_idle_minutes + if idle_timeout is None: + return ( + "FAIL", + f"Priority 1 non-default rule '{rule.name}' in {label} " + "does not define a maximum Okta global session idle time.", + ) + + if idle_timeout <= threshold: + return ( + "PASS", + f"Priority 1 non-default rule '{rule.name}' in {label} " + f"sets the maximum Okta global session idle time to {idle_timeout} " + f"minutes, meeting the configured threshold of {threshold} minutes.", + ) + return ( + "FAIL", + f"Priority 1 non-default rule '{rule.name}' in {label} " + f"sets the maximum Okta global session idle time to {idle_timeout} " + f"minutes, exceeding the configured threshold of {threshold} minutes.", + ) diff --git a/prowler/providers/okta/services/signon/signon_global_session_lifetime_18h/__init__.py b/prowler/providers/okta/services/signon/signon_global_session_lifetime_18h/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/okta/services/signon/signon_global_session_lifetime_18h/signon_global_session_lifetime_18h.metadata.json b/prowler/providers/okta/services/signon/signon_global_session_lifetime_18h/signon_global_session_lifetime_18h.metadata.json new file mode 100644 index 0000000000..84d88b3c76 --- /dev/null +++ b/prowler/providers/okta/services/signon/signon_global_session_lifetime_18h/signon_global_session_lifetime_18h.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "okta", + "CheckID": "signon_global_session_lifetime_18h", + "CheckTitle": "Default Global Session Policy has a Priority 1 non-default rule limiting session lifetime to 18 hours", + "CheckType": [], + "ServiceName": "signon", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "governance", + "Description": "Every active Okta **Global Session Policy** needs a **Priority 1** rule that is **not** the built-in `Default Rule`, setting *Maximum Okta global session lifetime* to `18` hours or less.\n\nOkta evaluates policies by group assignment, so a permissive custom policy can govern users. Threshold override: `okta_max_session_lifetime_minutes` (minutes). Aligns with **DISA STIG V-273203**.", + "Risk": "Without an enforced session lifetime, an authenticated Okta session can be reused indefinitely without reauthentication.\n\n- **Stolen session material** continues to grant access long after sign-in\n- **Authorization changes** (role revocation, group removal) take effect only on the next reauth\n- **Token replay** against downstream apps stays viable for the session window", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://help.okta.com/oie/en-us/content/topics/identity-engine/policies/about-okta-sign-on-policies.htm", + "https://developer.okta.com/docs/api/openapi/okta-management/management/tag/Policy/" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the **Okta Admin Console** as a *Super Admin*.\n2. Navigate to **Security** > **Global Session Policy**.\n3. Open the **Default Policy** (and repeat for every other active policy).\n4. Add or edit a non-default rule.\n5. Move that rule to **Priority 1** so it is evaluated before the built-in `Default Rule`.\n6. Set *Maximum Okta global session lifetime* to `18` hours or less. Do **not** set it to `0`, which disables the limit.\n7. Save the rule.", + "Terraform": "```hcl\nresource \"okta_policy_rule_signon\" \"\" {\n policy_id = okta_policy_signon.default.id\n name = \"\"\n status = \"ACTIVE\"\n priority = 1 # Critical: rule must sit at Priority 1 before the Default Rule\n session_lifetime = 1080 # Critical: 18 hours in minutes; do not use 0 (disables the limit)\n session_persistent = false # Critical: avoid persistent global session cookies\n}\n```" + }, + "Recommendation": { + "Text": "Configure each active **Global Session Policy** so a non-default rule at **Priority 1**:\n- Sets *Maximum Okta global session lifetime* to `18` hours or less (`1080` minutes)\n- Never sets the lifetime to `0`, which disables the limit\n- Is enabled (`ACTIVE`) and evaluated before the built-in `Default Rule`\n\nReview group assignments to confirm the rule actually governs the intended users.", + "Url": "https://hub.prowler.com/check/signon_global_session_lifetime_18h" + } + }, + "Categories": [ + "identity-access" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/okta/services/signon/signon_global_session_lifetime_18h/signon_global_session_lifetime_18h.py b/prowler/providers/okta/services/signon/signon_global_session_lifetime_18h/signon_global_session_lifetime_18h.py new file mode 100644 index 0000000000..25003eef90 --- /dev/null +++ b/prowler/providers/okta/services/signon/signon_global_session_lifetime_18h/signon_global_session_lifetime_18h.py @@ -0,0 +1,114 @@ +from prowler.lib.check.models import Check, CheckReportOkta +from prowler.providers.okta.services.signon.lib.signon_helpers import ( + active_policies, + missing_policy_scope_finding, + no_active_policies_finding, + policy_label, + priority_one_active_rule, +) +from prowler.providers.okta.services.signon.signon_client import signon_client +from prowler.providers.okta.services.signon.signon_service import GlobalSessionPolicy + +DEFAULT_THRESHOLD_MINUTES = 18 * 60 + + +class signon_global_session_lifetime_18h(Check): + """STIG V-273203 / OKTA-APP-001665. + + Every active Global Session Policy must have an active Priority 1 + rule that is not the built-in Default Rule, and that rule must set + the maximum Okta global session lifetime to the configured threshold + or lower (defaults to 18 hours per STIG; override via + `okta_max_session_lifetime_minutes` in the audit config). + + Okta evaluates sign-on policies in priority order based on group + assignments, so a permissive custom policy can govern a user's + session even when the Default Policy is strict. The check emits one + finding per active policy to surface that risk. + """ + + def execute(self) -> list[CheckReportOkta]: + audit_config = signon_client.audit_config or {} + threshold = audit_config.get( + "okta_max_session_lifetime_minutes", DEFAULT_THRESHOLD_MINUTES + ) + org_domain = signon_client.provider.identity.org_domain + + missing_scope = signon_client.missing_scope.get("global_session_policies") + if missing_scope: + return [ + missing_policy_scope_finding(self.metadata(), org_domain, missing_scope) + ] + + policies = active_policies(signon_client.global_session_policies) + if not policies: + return [ + no_active_policies_finding( + self.metadata(), + org_domain, + "No active Okta Global Session Policies were returned by the API. " + "STIG V-273203 requires the policy that governs each user to enforce " + "a Priority 1 non-default rule with an 18-hour session lifetime.", + ) + ] + + findings: list[CheckReportOkta] = [] + for policy in policies: + report = CheckReportOkta( + metadata=self.metadata(), resource=policy, org_domain=org_domain + ) + status, status_extended = _evaluate_policy(policy, threshold) + report.status = status + report.status_extended = status_extended + findings.append(report) + return findings + + +def _evaluate_policy(policy: GlobalSessionPolicy, threshold: int) -> tuple[str, str]: + label = policy_label(policy) + rule = priority_one_active_rule(policy) + + if rule is None: + return ( + "FAIL", + f"{label} has no Priority 1 active rule. STIG V-273203 requires " + f"a non-default Priority 1 rule with session lifetime <= {threshold} " + "minutes.", + ) + + if rule.is_default or rule.name == "Default Rule": + return ( + "FAIL", + f"{label} uses '{rule.name}' as its active Priority 1 rule. " + "The STIG requires a non-default Priority 1 rule.", + ) + + lifetime = rule.max_session_lifetime_minutes + if lifetime is None: + return ( + "FAIL", + f"Priority 1 non-default rule '{rule.name}' in {label} " + "does not define a maximum Okta global session lifetime.", + ) + + if lifetime == 0: + return ( + "FAIL", + f"Priority 1 non-default rule '{rule.name}' in {label} " + "disables the maximum Okta global session lifetime by setting it " + "to 0 minutes.", + ) + + if lifetime <= threshold: + return ( + "PASS", + f"Priority 1 non-default rule '{rule.name}' in {label} " + f"sets the maximum Okta global session lifetime to {lifetime} " + f"minutes, meeting the configured threshold of {threshold} minutes.", + ) + return ( + "FAIL", + f"Priority 1 non-default rule '{rule.name}' in {label} " + f"sets the maximum Okta global session lifetime to {lifetime} minutes, " + f"exceeding the configured threshold of {threshold} minutes.", + ) diff --git a/prowler/providers/okta/services/signon/signon_global_session_policy_network_zone_enforced/__init__.py b/prowler/providers/okta/services/signon/signon_global_session_policy_network_zone_enforced/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/okta/services/signon/signon_global_session_policy_network_zone_enforced/signon_global_session_policy_network_zone_enforced.metadata.json b/prowler/providers/okta/services/signon/signon_global_session_policy_network_zone_enforced/signon_global_session_policy_network_zone_enforced.metadata.json new file mode 100644 index 0000000000..b1009ba970 --- /dev/null +++ b/prowler/providers/okta/services/signon/signon_global_session_policy_network_zone_enforced/signon_global_session_policy_network_zone_enforced.metadata.json @@ -0,0 +1,38 @@ +{ + "Provider": "okta", + "CheckID": "signon_global_session_policy_network_zone_enforced", + "CheckTitle": "Default Global Session Policy applies a Network Zone condition aligned with the Access Control Policy", + "CheckType": [], + "ServiceName": "signon", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "governance", + "Description": "Every active Okta **Global Session Policy** must apply the *IF User's IP is* condition, mapped to a **Network Zone**, on its **Priority 1** active rule.\n\nUnlike the idle / lifetime / cookie STIGs, this control does **not** exclude the built-in `Default Rule`. Okta evaluates policies by group assignment. Aligns with **DISA STIG V-279691**.", + "Risk": "When the Global Session Policy does not restrict access by **Network Zone**, every authenticated entity establishes a session regardless of source IP.\n\n- **Stolen credentials** reach the Okta dashboard from any internet-routable address\n- **Out-of-band sessions** bypass the organization's Access Control Policy\n- **Network anomalies** cannot become deny decisions at sign-on", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://help.okta.com/oie/en-us/content/topics/security/network/network-zones.htm", + "https://help.okta.com/oie/en-us/content/topics/identity-engine/policies/about-okta-sign-on-policies.htm", + "https://developer.okta.com/docs/api/openapi/okta-management/management/tag/Policy/" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the **Okta Admin Console** as a *Super Admin*.\n2. Navigate to **Security** > **Networks** and define the **Network Zones** (allow / deny) that match the organization's Access Control Policy.\n3. Navigate to **Security** > **Global Session Policy**.\n4. Open the **Default Policy** (and repeat for every other active policy).\n5. Edit the rule that sits at **Priority 1**, or add a new one and move it to **Priority 1**.\n6. Under *Conditions*, set *IF User's IP is* to `In zone` (allow) or `Not in zone` (deny) and select the **Network Zone**.\n7. Save the rule.", + "Terraform": "```hcl\nresource \"okta_policy_rule_signon\" \"\" {\n policy_id = okta_policy_signon.default.id\n name = \"\"\n status = \"ACTIVE\"\n priority = 1 # Critical: rule must sit at Priority 1\n network_connection = \"ZONE\" # Critical: bind the rule to a Network Zone\n network_includes = [okta_network_zone.allowed.id] # Critical: zones that reflect the Access Control Policy\n}\n```" + }, + "Recommendation": { + "Text": "Configure the **Priority 1** active rule in each Global Session Policy so it:\n- Maps the *IF User's IP is* condition to a **Network Zone** aligned with the organization's Access Control Policy\n- Uses `In zone` for allow-list zones and `Not in zone` for deny-list zones\n- Is enabled (`ACTIVE`) and evaluated before the built-in `Default Rule`, or *is* the `Default Rule` itself\n\nReview group assignments to confirm the rule actually governs the intended users.", + "Url": "https://hub.prowler.com/check/signon_global_session_policy_network_zone_enforced" + } + }, + "Categories": [ + "identity-access" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/okta/services/signon/signon_global_session_policy_network_zone_enforced/signon_global_session_policy_network_zone_enforced.py b/prowler/providers/okta/services/signon/signon_global_session_policy_network_zone_enforced/signon_global_session_policy_network_zone_enforced.py new file mode 100644 index 0000000000..7a7e51706c --- /dev/null +++ b/prowler/providers/okta/services/signon/signon_global_session_policy_network_zone_enforced/signon_global_session_policy_network_zone_enforced.py @@ -0,0 +1,95 @@ +from prowler.lib.check.models import Check, CheckReportOkta +from prowler.providers.okta.services.signon.lib.signon_helpers import ( + active_policies, + missing_policy_scope_finding, + no_active_policies_finding, + policy_label, + priority_one_active_rule, +) +from prowler.providers.okta.services.signon.signon_client import signon_client +from prowler.providers.okta.services.signon.signon_service import GlobalSessionPolicy + + +class signon_global_session_policy_network_zone_enforced(Check): + """STIG V-279691 / OKTA-APP-003242. + + Every active Global Session Policy must apply an "IF User's IP is" + condition mapped to a Network Zone on its Priority 1 active rule so + access can be allowed or denied per the organization's Access + Control Policy. + + Unlike the idle / lifetime / persistent-cookie STIGs, V-279691 does + not exclude the built-in Default Rule, so a zone condition on the + Default Rule is still effective when no non-default rule sits at + Priority 1. + + The check emits one finding per active policy because Okta evaluates + sign-on policies in priority order based on group assignments, and a + permissive custom policy can govern a user's session even when the + Default Policy is strict. + """ + + def execute(self) -> list[CheckReportOkta]: + org_domain = signon_client.provider.identity.org_domain + + missing_scope = signon_client.missing_scope.get("global_session_policies") + if missing_scope: + return [ + missing_policy_scope_finding(self.metadata(), org_domain, missing_scope) + ] + + policies = active_policies(signon_client.global_session_policies) + if not policies: + return [ + no_active_policies_finding( + self.metadata(), + org_domain, + "No active Okta Global Session Policies were returned by the API. " + "STIG V-279691 requires the policy that governs each user to map " + "User's IP to a Network Zone on its Priority 1 active rule.", + ) + ] + + findings: list[CheckReportOkta] = [] + for policy in policies: + report = CheckReportOkta( + metadata=self.metadata(), resource=policy, org_domain=org_domain + ) + status, status_extended = _evaluate_policy(policy) + report.status = status + report.status_extended = status_extended + findings.append(report) + return findings + + +def _evaluate_policy(policy: GlobalSessionPolicy) -> tuple[str, str]: + label = policy_label(policy) + rule = priority_one_active_rule(policy) + + if rule is None: + return ( + "FAIL", + f"{label} has no Priority 1 active rule. STIG V-279691 requires " + "the policy to apply an IP-based Network Zone condition on its " + "Priority 1 active rule.", + ) + + rule_kind = ( + "built-in Default Rule" + if rule.is_default or rule.name == "Default Rule" + else "non-default rule" + ) + has_zones = bool(rule.network_zones_include or rule.network_zones_exclude) + + if has_zones: + return ( + "PASS", + f"Priority 1 active {rule_kind} '{rule.name}' in {label} maps " + "User's IP to a Network Zone.", + ) + return ( + "FAIL", + f"Priority 1 active {rule_kind} '{rule.name}' in {label} does not " + "map User's IP to a Network Zone. The policy cannot allow or deny " + "access based on the organization's Access Control Policy.", + ) diff --git a/prowler/providers/okta/services/signon/signon_service.py b/prowler/providers/okta/services/signon/signon_service.py new file mode 100644 index 0000000000..7c32363501 --- /dev/null +++ b/prowler/providers/okta/services/signon/signon_service.py @@ -0,0 +1,287 @@ +from typing import Optional +from urllib.parse import parse_qs, urlparse + +from pydantic import BaseModel + +from prowler.lib.logger import logger +from prowler.providers.okta.lib.service.service import OktaService + + +def _next_after_cursor(resp) -> Optional[str]: + """Extract the `after` cursor from a `Link: ...; rel="next"` header. + + Returns None when there is no next page. Header format follows RFC 5988 + and Okta's pagination guide. + """ + if resp is None: + return None + headers = getattr(resp, "headers", None) or {} + link = headers.get("link") or headers.get("Link") or "" + if not link: + return None + for part in link.split(","): + if 'rel="next"' not in part: + continue + url_segment = part.split(";", 1)[0].strip().lstrip("<").rstrip(">") + cursor = parse_qs(urlparse(url_segment).query).get("after", [None])[0] + if cursor: + return cursor + return None + + +REQUIRED_SCOPES: dict[str, str] = { + "global_session_policies": "okta.policies.read", + "sign_in_pages": "okta.brands.read", +} + + +class Signon(OktaService): + """Fetches OKTA_SIGN_ON policies, rules, and brand sign-in pages. + + Populates `self.global_session_policies` keyed by policy id. Each + policy carries its rules; downstream checks read directly from this + structure. + + Also populates `self.sign_in_pages` keyed by brand id with sign-in page + HTML used by the DOD warning-banner check. When a brand has no + customized page, the service falls back to the default sign-in page + exposed by the Okta Management API and tracks it with + `is_customized=False`. + + Before each fetch the service compares its required OAuth scope + (see `REQUIRED_SCOPES`) against the access token's granted scopes + (`provider.identity.granted_scopes`). When a scope is known to be + missing, the fetch is skipped and the resource is recorded in + `self.missing_scope` so checks can report the missing scope explicitly + instead of emitting a misleading "no resources returned" finding. + When granted_scopes is empty (token decode unavailable), the service + treats permissions as unknown and attempts the fetch — preserving + the prior behavior. + """ + + def __init__(self, provider): + super().__init__(__class__.__name__, provider) + granted = set(getattr(provider.identity, "granted_scopes", None) or []) + self.missing_scope: dict[str, Optional[str]] = { + resource: (scope if granted and scope not in granted else None) + for resource, scope in REQUIRED_SCOPES.items() + } + + self.global_session_policies: dict[str, GlobalSessionPolicy] = ( + {} + if self.missing_scope["global_session_policies"] + else self._list_global_session_policies() + ) + self.sign_in_pages: dict[str, SignInPage] = ( + {} if self.missing_scope["sign_in_pages"] else self._list_sign_in_pages() + ) + + def _list_global_session_policies(self) -> dict: + logger.info("Signon - Listing OKTA_SIGN_ON policies and rules...") + try: + return self._run(self._fetch_all()) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return {} + + async def _fetch_all(self) -> dict: + result: dict[str, GlobalSessionPolicy] = {} + all_policies, err = await self._paginate( + lambda after: self.client.list_policies(type="OKTA_SIGN_ON", after=after) + ) + if err is not None: + logger.error(f"Error listing OKTA_SIGN_ON policies: {err}") + return result + + for policy in all_policies: + rules = await self._fetch_rules(policy.id) + result[policy.id] = GlobalSessionPolicy( + id=policy.id, + name=getattr(policy, "name", "") or "", + priority=getattr(policy, "priority", None), + status=getattr(policy, "status", "") or "", + is_default=bool(getattr(policy, "system", False)), + rules=rules, + ) + return result + + async def _fetch_rules(self, policy_id: str) -> list: + # Okta's `list_policy_rules` endpoint does not expose an `after` + # cursor in the SDK signature, so we call once with a generous + # `limit`. Tenants with more rules per policy than the limit would + # silently truncate; this is rare (most policies have <10 rules). + rule_fetch_limit = 100 + rules_out: list[GlobalSessionPolicyRule] = [] + result = await self.client.list_policy_rules( + policy_id, limit=str(rule_fetch_limit) + ) + err = result[-1] + if err is not None: + logger.error(f"Error listing rules for policy {policy_id}: {err}") + return rules_out + all_rules = list(result[0] or []) + if len(all_rules) >= rule_fetch_limit: + logger.warning( + f"Policy {policy_id} returned {len(all_rules)} rules — the " + f"per-policy fetch limit ({rule_fetch_limit}) was hit; any " + "rules beyond this limit are not evaluated by Prowler. " + "Review the policy in the Okta Admin Console." + ) + + for rule in all_rules: + actions = getattr(rule, "actions", None) + signon = getattr(actions, "signon", None) if actions else None + session = getattr(signon, "session", None) if signon else None + conditions = getattr(rule, "conditions", None) + network = getattr(conditions, "network", None) if conditions else None + rules_out.append( + GlobalSessionPolicyRule( + id=getattr(rule, "id", "") or "", + name=getattr(rule, "name", "") or "", + priority=getattr(rule, "priority", None), + status=getattr(rule, "status", "") or "", + is_default=bool(getattr(rule, "system", False)), + max_session_idle_minutes=getattr( + session, "max_session_idle_minutes", None + ), + max_session_lifetime_minutes=getattr( + session, "max_session_lifetime_minutes", None + ), + use_persistent_cookie=getattr( + session, "use_persistent_cookie", None + ), + network_zones_include=list(getattr(network, "include", None) or []), + network_zones_exclude=list(getattr(network, "exclude", None) or []), + ) + ) + return rules_out + + def _list_sign_in_pages(self) -> dict: + logger.info("Signon - Listing brand sign-in pages...") + try: + return self._run(self._fetch_brands_and_pages()) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return {} + + async def _fetch_brands_and_pages(self) -> dict: + result: dict[str, SignInPage] = {} + all_brands, err = await self._paginate( + lambda after: self.client.list_brands(after=after) + ) + if err is not None: + logger.error(f"Error listing brands: {err}") + return result + + for brand in all_brands: + brand_id = getattr(brand, "id", "") or "" + brand_name = getattr(brand, "name", "") or "" + result[brand_id] = await self._fetch_sign_in_page(brand_id, brand_name) + return result + + async def _fetch_sign_in_page(self, brand_id: str, brand_name: str) -> "SignInPage": + page_result = await self.client.get_customized_sign_in_page(brand_id) + page_err = page_result[-1] + page_data = page_result[0] + if page_err is None: + return SignInPage( + brand_id=brand_id, + brand_name=brand_name, + is_customized=True, + page_content=getattr(page_data, "page_content", None), + ) + + if not self._is_missing_customized_page_error(page_err): + return SignInPage( + brand_id=brand_id, + brand_name=brand_name, + is_customized=False, + fetch_error=str(page_err), + ) + + default_page_result = await self.client.get_default_sign_in_page(brand_id) + default_page_err = default_page_result[-1] + default_page_data = default_page_result[0] + if default_page_err is not None: + return SignInPage( + brand_id=brand_id, + brand_name=brand_name, + is_customized=False, + fetch_error=str(default_page_err), + ) + + return SignInPage( + brand_id=brand_id, + brand_name=brand_name, + is_customized=False, + page_content=getattr(default_page_data, "page_content", None), + ) + + @staticmethod + def _is_missing_customized_page_error(error) -> bool: + err_text = str(error).lower() + return "404" in err_text or "not found" in err_text or "e0000007" in err_text + + @staticmethod + async def _paginate(fetch): + """Drain all pages of an SDK list call. + + `fetch` is a callable that takes the `after` cursor (or None for + the first page) and returns the SDK's standard `(items, resp, err)` + tuple. We follow `Link: rel="next"` headers until exhausted. + """ + all_items = [] + result = await fetch(None) + # Defensive against the SDK's 2-tuple early-error path: error is last. + err = result[-1] + if err is not None: + return [], err + items = result[0] + resp = result[1] if len(result) >= 3 else None + all_items.extend(items or []) + while True: + cursor = _next_after_cursor(resp) + if not cursor: + break + result = await fetch(cursor) + err = result[-1] + if err is not None: + return all_items, err + items = result[0] + resp = result[1] if len(result) >= 3 else None + all_items.extend(items or []) + return all_items, None + + +class GlobalSessionPolicyRule(BaseModel): + id: str + name: str + priority: Optional[int] = None + status: str = "" + is_default: bool = False + max_session_idle_minutes: Optional[int] = None + max_session_lifetime_minutes: Optional[int] = None + use_persistent_cookie: Optional[bool] = None + network_zones_include: list[str] = [] + network_zones_exclude: list[str] = [] + + +class GlobalSessionPolicy(BaseModel): + id: str + name: str + priority: Optional[int] = None + status: str = "" + is_default: bool = False + rules: list[GlobalSessionPolicyRule] = [] + + +class SignInPage(BaseModel): + brand_id: str + brand_name: str = "" + is_customized: bool = False + page_content: Optional[str] = None + fetch_error: Optional[str] = None diff --git a/prowler/providers/oraclecloud/oraclecloud_provider.py b/prowler/providers/oraclecloud/oraclecloud_provider.py index c5abaf3929..5aa959d985 100644 --- a/prowler/providers/oraclecloud/oraclecloud_provider.py +++ b/prowler/providers/oraclecloud/oraclecloud_provider.py @@ -66,6 +66,7 @@ class OraclecloudProvider(Provider): _compartments: list = [] _mutelist: OCIMutelist audit_metadata: Audit_Metadata + _home_region: str = "us-ashburn-1" def __init__( self, @@ -160,6 +161,15 @@ class OraclecloudProvider(Provider): # Get regions self._regions = self.get_regions_to_audit(region) + # Determine the tenancy home region from the full subscription list, independent of + # the --region filter, so tenancy-level APIs (e.g. the Audit configuration) always + # target the home region instead of a filtered, non-home region. + all_subscribed_regions = self.get_regions_to_audit() + self._home_region = next( + (region.key for region in all_subscribed_regions if region.is_home_region), + self._regions[0].key if self._regions else "us-ashburn-1", + ) + logger.info(f"Home region is: {self._home_region}") # Get compartments self._compartments = self.get_compartments_to_audit( @@ -217,6 +227,10 @@ class OraclecloudProvider(Provider): def regions(self): return self._regions + @property + def home_region(self): + return self._home_region + @property def compartments(self): return self._compartments diff --git a/prowler/providers/oraclecloud/services/audit/audit_service.py b/prowler/providers/oraclecloud/services/audit/audit_service.py index fe7751a10e..38022d1d87 100644 --- a/prowler/providers/oraclecloud/services/audit/audit_service.py +++ b/prowler/providers/oraclecloud/services/audit/audit_service.py @@ -19,9 +19,15 @@ class Audit(OCIService): def __get_configuration__(self): """Get Audit configuration.""" try: - audit_client = self._create_oci_client(oci.audit.AuditClient) + home_region = self.provider.home_region + audit_client = self._create_oci_client( + oci.audit.AuditClient, + config_overrides={"region": home_region}, + ) - logger.info("Audit - Getting Configuration...") + logger.info( + f"Audit - Getting Configuration from home region ({home_region})..." + ) try: config = audit_client.get_configuration( diff --git a/prowler/providers/oraclecloud/services/identity/identity_service.py b/prowler/providers/oraclecloud/services/identity/identity_service.py index a0932bd54f..d36ed5c0ac 100644 --- a/prowler/providers/oraclecloud/services/identity/identity_service.py +++ b/prowler/providers/oraclecloud/services/identity/identity_service.py @@ -1,6 +1,7 @@ """OCI Identity Service Module.""" from datetime import datetime +from threading import Lock from typing import Optional import oci @@ -26,6 +27,7 @@ class Identity(OCIService): self.policies = [] self.dynamic_groups = [] self.domains = [] + self._domains_lock = Lock() self.password_policy = None self.root_compartment_resources = [] self.active_non_root_compartments = [] @@ -61,8 +63,8 @@ class Identity(OCIService): regional_client: Regional OCI client """ try: - # Identity is a global service, use home region - if regional_client.region not in self.provider.identity.region: + # Only use one region for global users + if regional_client.region != self.provider.home_region: return identity_client = self.__get_client__(regional_client.region) @@ -312,7 +314,8 @@ class Identity(OCIService): def __list_groups__(self, regional_client): """List all IAM groups.""" try: - if regional_client.region not in self.provider.identity.region: + # Only use one region for global groups + if regional_client.region != self.provider.home_region: return identity_client = self.__get_client__(regional_client.region) @@ -355,7 +358,8 @@ class Identity(OCIService): def __list_policies__(self, regional_client): """List all IAM policies.""" try: - if regional_client.region not in self.provider.identity.region: + # Only use one region for global policies + if regional_client.region != self.provider.home_region: return identity_client = self.__get_client__(regional_client.region) @@ -399,8 +403,8 @@ class Identity(OCIService): def __list_dynamic_groups__(self, regional_client): """List all dynamic groups in the tenancy.""" try: - # Dynamic groups are only in the home region - if regional_client.region not in self.provider.identity.region: + # Only use one region for global dynamic groups + if regional_client.region != self.provider.home_region: return identity_client = self.__get_client__(regional_client.region) @@ -447,10 +451,6 @@ class Identity(OCIService): def __list_domains__(self, regional_client): """List all identity domains.""" try: - # Domains are only in the home region - if regional_client.region not in self.provider.identity.region: - return - identity_client = self.__get_client__(regional_client.region) logger.info("Identity - Listing Identity Domains...") @@ -458,6 +458,7 @@ class Identity(OCIService): try: # List all domains in the tenancy for compartment in self.audited_compartments: + domains = oci.pagination.list_call_get_all_results( identity_client.list_domains, compartment_id=compartment.id, @@ -465,20 +466,38 @@ class Identity(OCIService): ).data for domain in domains: - self.domains.append( - IdentityDomain( - id=domain.id, - display_name=domain.display_name, - description=domain.description or "", - url=domain.url, - home_region=domain.home_region, - compartment_id=compartment.id, - lifecycle_state=domain.lifecycle_state, - time_created=domain.time_created, - region=regional_client.region, - password_policies=[], + + # Threads run __list_domains__ concurrently per + # region; serialize the dedupe-then-append so two + # regions returning the same domain cannot race + # past each other and produce duplicates or lose + # the home-region preference. + with self._domains_lock: + existing = next( + (d for d in self.domains if d.id == domain.id), + None, + ) + if existing is not None: + # Prefer the entry from the domain's home region + if domain.home_region == regional_client.region: + self.domains.remove(existing) + else: + continue + + self.domains.append( + IdentityDomain( + id=domain.id, + display_name=domain.display_name, + description=domain.description or "", + url=domain.url, + home_region=domain.home_region, + compartment_id=compartment.id, + lifecycle_state=domain.lifecycle_state, + time_created=domain.time_created, + region=regional_client.region, + password_policies=[], + ) ) - ) except Exception as error: logger.error( @@ -493,8 +512,8 @@ class Identity(OCIService): def __list_domain_password_policies__(self, regional_client): """List password policies for all identity domains.""" try: - # Password policies are only in the home region - if regional_client.region not in self.provider.identity.region: + # Only use one region for all domain scan + if regional_client.region != self.provider.home_region: return logger.info("Identity - Listing Domain Password Policies...") @@ -551,7 +570,8 @@ class Identity(OCIService): def __get_password_policy__(self, regional_client): """Get the password policy for the tenancy.""" try: - if regional_client.region not in self.provider.identity.region: + # Only use one region for global password policies + if regional_client.region != self.provider.home_region: return identity_client = self.__get_client__(regional_client.region) @@ -578,8 +598,8 @@ class Identity(OCIService): def __search_root_compartment_resources__(self, regional_client): """Search for resources in the root compartment using OCI Resource Search.""" try: - # Search is a global service, use home region - if regional_client.region not in self.provider.identity.region: + # Only use one region for global search + if regional_client.region != self.provider.home_region: return logger.info("Identity - Searching for resources in root compartment...") @@ -626,10 +646,9 @@ class Identity(OCIService): def __search_active_non_root_compartments__(self, regional_client): """Search for active non-root compartments using OCI Resource Search.""" try: - # Search is a global service, use home region - if regional_client.region not in self.provider.identity.region: + # Only use one region for global search + if regional_client.region != self.provider.home_region: return - logger.info("Identity - Searching for active non-root compartments...") # Create search client using the helper method for proper authentication diff --git a/prowler/providers/scaleway/__init__.py b/prowler/providers/scaleway/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/scaleway/exceptions/__init__.py b/prowler/providers/scaleway/exceptions/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/scaleway/exceptions/exceptions.py b/prowler/providers/scaleway/exceptions/exceptions.py new file mode 100644 index 0000000000..05e1b256ae --- /dev/null +++ b/prowler/providers/scaleway/exceptions/exceptions.py @@ -0,0 +1,99 @@ +# Exceptions codes from 15000 to 15999 are reserved for Scaleway exceptions +from prowler.exceptions.exceptions import ProwlerException + + +class ScalewayBaseException(ProwlerException): + """Base exception for Scaleway provider errors.""" + + SCALEWAY_ERROR_CODES = { + (15000, "ScalewayCredentialsError"): { + "message": "Scaleway credentials not found or invalid.", + "remediation": ( + "Set the SCW_ACCESS_KEY and SCW_SECRET_KEY environment variables " + "with a valid Scaleway API key. Generate one at " + "https://console.scaleway.com/iam/api-keys." + ), + }, + (15001, "ScalewayAuthenticationError"): { + "message": "Authentication to the Scaleway API failed.", + "remediation": ( + "Verify your Scaleway API key is valid, has not expired, and that " + "the bearer has IAM read permissions on the target organization." + ), + }, + (15002, "ScalewaySessionError"): { + "message": "Failed to create a Scaleway API session.", + "remediation": ( + "Check network connectivity and ensure the Scaleway API is " + "reachable at https://api.scaleway.com." + ), + }, + (15003, "ScalewayIdentityError"): { + "message": "Failed to retrieve Scaleway identity information.", + "remediation": ( + "Ensure the API key has permissions to read IAM users and the " + "owning organization metadata." + ), + }, + (15004, "ScalewayAPIError"): { + "message": "An error occurred while calling the Scaleway API.", + "remediation": ( + "Check the Scaleway API status at https://status.scaleway.com " + "and retry. Run with --log-level DEBUG for the full traceback." + ), + }, + } + + def __init__(self, code, file=None, original_exception=None, message=None): + provider = "Scaleway" + error_info = self.SCALEWAY_ERROR_CODES.get((code, self.__class__.__name__)) + if error_info is None: + error_info = { + "message": message or "Unknown Scaleway error.", + "remediation": "Check the Scaleway API documentation for more details.", + } + elif message: + error_info = error_info.copy() + error_info["message"] = message + super().__init__( + code=code, + source=provider, + file=file, + original_exception=original_exception, + error_info=error_info, + ) + + +class ScalewayCredentialsError(ScalewayBaseException): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 15000, file=file, original_exception=original_exception, message=message + ) + + +class ScalewayAuthenticationError(ScalewayBaseException): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 15001, file=file, original_exception=original_exception, message=message + ) + + +class ScalewaySessionError(ScalewayBaseException): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 15002, file=file, original_exception=original_exception, message=message + ) + + +class ScalewayIdentityError(ScalewayBaseException): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 15003, file=file, original_exception=original_exception, message=message + ) + + +class ScalewayAPIError(ScalewayBaseException): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 15004, file=file, original_exception=original_exception, message=message + ) diff --git a/prowler/providers/scaleway/lib/__init__.py b/prowler/providers/scaleway/lib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/scaleway/lib/arguments/__init__.py b/prowler/providers/scaleway/lib/arguments/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/scaleway/lib/arguments/arguments.py b/prowler/providers/scaleway/lib/arguments/arguments.py new file mode 100644 index 0000000000..7d3fd7dc0c --- /dev/null +++ b/prowler/providers/scaleway/lib/arguments/arguments.py @@ -0,0 +1,36 @@ +def init_parser(self): + """Init the Scaleway provider CLI parser.""" + scaleway_parser = self.subparsers.add_parser( + "scaleway", + parents=[self.common_providers_parser], + help="Scaleway Provider", + ) + + # Authentication + # Credentials are read exclusively from the standard Scaleway environment + # variables (SCW_ACCESS_KEY / SCW_SECRET_KEY) to avoid leaking secrets into + # shell history and process listings. There are no credential CLI flags. + + # Scope + scope_subparser = scaleway_parser.add_argument_group("Scope") + scope_subparser.add_argument( + "--organization-id", + nargs="?", + default=None, + metavar="SCW_DEFAULT_ORGANIZATION_ID", + help="Scaleway organization ID to scope the audit.", + ) + scope_subparser.add_argument( + "--project-id", + nargs="?", + default=None, + metavar="SCW_DEFAULT_PROJECT_ID", + help="Default Scaleway project ID for project-scoped resources.", + ) + scope_subparser.add_argument( + "--region", + nargs="?", + default=None, + metavar="SCW_DEFAULT_REGION", + help="Default Scaleway region (fr-par, nl-ams, pl-waw).", + ) diff --git a/prowler/providers/scaleway/lib/mutelist/__init__.py b/prowler/providers/scaleway/lib/mutelist/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/scaleway/lib/mutelist/mutelist.py b/prowler/providers/scaleway/lib/mutelist/mutelist.py new file mode 100644 index 0000000000..d09bc5d435 --- /dev/null +++ b/prowler/providers/scaleway/lib/mutelist/mutelist.py @@ -0,0 +1,20 @@ +from prowler.lib.check.models import CheckReportScaleway +from prowler.lib.mutelist.mutelist import Mutelist +from prowler.lib.outputs.utils import unroll_dict, unroll_tags + + +class ScalewayMutelist(Mutelist): + """Scaleway-specific mutelist helper.""" + + def is_finding_muted( + self, + finding: CheckReportScaleway, + organization_id: str, + ) -> bool: + return self.is_muted( + organization_id, + finding.check_metadata.CheckID, + finding.region or "global", + finding.resource_id or finding.resource_name, + unroll_dict(unroll_tags(finding.resource_tags)), + ) diff --git a/prowler/providers/scaleway/lib/service/__init__.py b/prowler/providers/scaleway/lib/service/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/scaleway/lib/service/service.py b/prowler/providers/scaleway/lib/service/service.py new file mode 100644 index 0000000000..0218bf72ae --- /dev/null +++ b/prowler/providers/scaleway/lib/service/service.py @@ -0,0 +1,44 @@ +from prowler.lib.logger import logger +from prowler.providers.scaleway.exceptions.exceptions import ScalewayAPIError + + +class ScalewayService: + """Base class for Scaleway services. + + Centralizes the provider context (audit/fixer configuration, the + scoping organization, the authenticated ``scaleway.Client``) so each + service only worries about which Scaleway API to call. + """ + + def __init__(self, service: str, provider): + self.provider = provider + self.audit_config = provider.audit_config + self.fixer_config = provider.fixer_config + self.service = service.lower() if not service.islower() else service + + # Shared authenticated client and the organization in scope + self.client = provider.session.client + self.organization_id = provider.identity.organization_id + + def _safe_call(self, label: str, fn, *args, **kwargs): + """Run a Scaleway SDK call and surface failures as ScalewayAPIError. + + Args: + label: Human-readable label for the call (used in logs). + fn: SDK function to invoke. + + Returns: + The SDK function result, or ``None`` if the call failed. + """ + try: + return fn(*args, **kwargs) + except Exception as error: + logger.error( + f"{self.service} - {label} failed: " + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + raise ScalewayAPIError( + file=__file__, + original_exception=error, + message=f"Scaleway API call '{label}' failed.", + ) diff --git a/prowler/providers/scaleway/models.py b/prowler/providers/scaleway/models.py new file mode 100644 index 0000000000..6dc4bb7c4b --- /dev/null +++ b/prowler/providers/scaleway/models.py @@ -0,0 +1,55 @@ +from typing import Any, Literal, Optional + +from pydantic.v1 import BaseModel, Field + +from prowler.config.config import output_file_timestamp +from prowler.providers.common.models import ProviderOutputOptions + +ScalewayBearerType = Literal["user", "application"] + + +class ScalewaySession(BaseModel): + """Scaleway API session information. + + Stores the credentials and the underlying ``scaleway.Client`` so every + service can reuse the same authenticated client. + """ + + access_key: str + # Excluded from serialization and repr: the whole authentication relies + # on this secret, so it must never leak through .dict()/.json()/logs. + secret_key: str = Field(exclude=True, repr=False) + organization_id: Optional[str] = None + default_project_id: Optional[str] = None + default_region: Optional[str] = None + client: Any = Field(default=None, exclude=True) + + class Config: + arbitrary_types_allowed = True + + +class ScalewayIdentityInfo(BaseModel): + """Scaleway identity and scoping information.""" + + organization_id: str + bearer_id: Optional[str] = None + bearer_type: Optional[ScalewayBearerType] = None + bearer_email: Optional[str] = None + account_root_user_id: Optional[str] = None + + +class ScalewayOutputOptions(ProviderOutputOptions): + """Customize output filenames for Scaleway scans.""" + + def __init__(self, arguments, bulk_checks_metadata, identity: ScalewayIdentityInfo): + super().__init__(arguments, bulk_checks_metadata) + if ( + not hasattr(arguments, "output_filename") + or arguments.output_filename is None + ): + account_fragment = identity.organization_id or "scaleway" + self.output_filename = ( + f"prowler-output-{account_fragment}-{output_file_timestamp}" + ) + else: + self.output_filename = arguments.output_filename diff --git a/prowler/providers/scaleway/scaleway_provider.py b/prowler/providers/scaleway/scaleway_provider.py new file mode 100644 index 0000000000..5c113483dd --- /dev/null +++ b/prowler/providers/scaleway/scaleway_provider.py @@ -0,0 +1,378 @@ +import os + +from colorama import Fore, Style +from scaleway import Client +from scaleway.iam.v1alpha1 import IamV1Alpha1API + +from prowler.config.config import ( + default_config_file_path, + get_default_mute_file_path, + load_and_validate_config_file, +) +from prowler.lib.logger import logger +from prowler.lib.utils.utils import print_boxes +from prowler.providers.common.models import Audit_Metadata, Connection +from prowler.providers.common.provider import Provider +from prowler.providers.scaleway.exceptions.exceptions import ( + ScalewayAuthenticationError, + ScalewayCredentialsError, + ScalewayIdentityError, + ScalewaySessionError, +) +from prowler.providers.scaleway.lib.mutelist.mutelist import ScalewayMutelist +from prowler.providers.scaleway.models import ( + ScalewayIdentityInfo, + ScalewaySession, +) + + +class ScalewayProvider(Provider): + """Scaleway provider. + + Authenticates against the Scaleway API using an API key (access key + + secret key) and exposes a single global session that every service + reuses. Scaleway scopes everything to an organization, so the + organization ID is the audit identity. + """ + + _type: str = "scaleway" + _session: ScalewaySession + _identity: ScalewayIdentityInfo + _audit_config: dict + _fixer_config: dict + _mutelist: ScalewayMutelist + audit_metadata: Audit_Metadata + + def __init__( + self, + # Authentication credentials + access_key: str = None, + secret_key: str = None, + organization_id: str = None, + project_id: str = None, + region: str = None, + # Provider configuration + config_path: str = None, + config_content: dict | None = None, + fixer_config: dict = {}, + mutelist_path: str = None, + mutelist_content: dict = None, + ): + logger.info("Instantiating Scaleway provider...") + + if config_content: + self._audit_config = config_content + else: + if not config_path: + config_path = default_config_file_path + self._audit_config = load_and_validate_config_file(self._type, config_path) + + self._session = ScalewayProvider.setup_session( + access_key=access_key, + secret_key=secret_key, + organization_id=organization_id, + project_id=project_id, + region=region, + ) + + self._identity = ScalewayProvider.setup_identity(self._session) + + self._fixer_config = fixer_config + + if mutelist_content: + self._mutelist = ScalewayMutelist(mutelist_content=mutelist_content) + else: + if not mutelist_path: + mutelist_path = get_default_mute_file_path(self.type) + self._mutelist = ScalewayMutelist(mutelist_path=mutelist_path) + + Provider.set_global_provider(self) + + @property + def type(self): + return self._type + + @property + def session(self): + return self._session + + @property + def identity(self): + return self._identity + + @property + def audit_config(self): + return self._audit_config + + @property + def fixer_config(self): + return self._fixer_config + + @property + def mutelist(self) -> ScalewayMutelist: + return self._mutelist + + @staticmethod + def setup_session( + access_key: str = None, + secret_key: str = None, + organization_id: str = None, + project_id: str = None, + region: str = None, + ) -> ScalewaySession: + """Initialize the Scaleway API session. + + Credentials can be provided as arguments (for API/SDK use) or read + from the official Scaleway environment variables: + + - ``SCW_ACCESS_KEY`` + - ``SCW_SECRET_KEY`` + - ``SCW_DEFAULT_ORGANIZATION_ID`` + - ``SCW_DEFAULT_PROJECT_ID`` + - ``SCW_DEFAULT_REGION`` + + Args: + access_key: Scaleway API access key. + secret_key: Scaleway API secret key. + organization_id: Default organization ID to scope the audit. + project_id: Default project ID for project-scoped resources. + region: Default region. + + Returns: + ScalewaySession: The initialized session, holding the + authenticated ``scaleway.Client``. + + Raises: + ScalewayCredentialsError: Access or secret key missing. + ScalewaySessionError: Client instantiation failed. + """ + access = access_key or os.environ.get("SCW_ACCESS_KEY", "") + secret = secret_key or os.environ.get("SCW_SECRET_KEY", "") + org = organization_id or os.environ.get("SCW_DEFAULT_ORGANIZATION_ID") or None + project = project_id or os.environ.get("SCW_DEFAULT_PROJECT_ID") or None + default_region = region or os.environ.get("SCW_DEFAULT_REGION") or "fr-par" + + if not access or not secret: + raise ScalewayCredentialsError( + file=os.path.basename(__file__), + message=( + "Scaleway credentials not found. Provide access_key and " + "secret_key or set the SCW_ACCESS_KEY and SCW_SECRET_KEY " + "environment variables." + ), + ) + + try: + client = Client( + access_key=access, + secret_key=secret, + default_organization_id=org, + default_project_id=project, + default_region=default_region, + ) + return ScalewaySession( + access_key=access, + secret_key=secret, + organization_id=org, + default_project_id=project, + default_region=default_region, + client=client, + ) + except Exception as error: + logger.critical( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + raise ScalewaySessionError( + file=os.path.basename(__file__), + original_exception=error, + ) + + @staticmethod + def setup_identity(session: ScalewaySession) -> ScalewayIdentityInfo: + """Resolve the audit identity by calling Scaleway IAM. + + Uses ``iam.get_api_key`` on the current access key to discover the + bearer (user vs application). When the bearer is a user, the + owning organization is read from the user record; otherwise we + require ``SCW_DEFAULT_ORGANIZATION_ID``. + """ + try: + iam = IamV1Alpha1API(session.client) + current_key = iam.get_api_key(access_key=session.access_key) + + bearer_id = current_key.user_id or current_key.application_id + bearer_type = ( + "user" + if current_key.user_id + else ("application" if current_key.application_id else None) + ) + + organization_id = session.organization_id + bearer_email = None + account_root_user_id = None + + # If the bearer is a user, resolve the org from the user record + # and surface the email + root user id for the credentials banner. + if current_key.user_id: + user = iam.get_user(user_id=current_key.user_id) + organization_id = organization_id or user.organization_id + bearer_email = user.email + account_root_user_id = user.account_root_user_id + elif current_key.application_id and not organization_id: + # Application keys do not expose the org directly without an + # extra call. The default org from env is preferred. + logger.warning( + "Scaleway application-scoped API key without " + "SCW_DEFAULT_ORGANIZATION_ID. Resource discovery may fail." + ) + # NOTE: application-scoped keys never resolve account_root_user_id + # here (the IAM API does not expose it for an application bearer). + # The IAM service falls back to the org's user list to recover it; + # if that is unavailable, iam_api_keys_no_root_owned degrades to + # MANUAL rather than silently PASSing root-owned keys. + + if not organization_id: + raise ScalewayIdentityError( + file=os.path.basename(__file__), + message=( + "Could not determine the Scaleway organization ID. " + "Set SCW_DEFAULT_ORGANIZATION_ID or use a user-scoped " + "API key." + ), + ) + + return ScalewayIdentityInfo( + organization_id=organization_id, + bearer_id=bearer_id, + bearer_type=bearer_type, + bearer_email=bearer_email, + account_root_user_id=account_root_user_id, + ) + except ScalewayIdentityError: + raise + except Exception as error: + logger.critical( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + raise ScalewayIdentityError( + file=os.path.basename(__file__), + original_exception=error, + ) + + @staticmethod + def validate_credentials(session: ScalewaySession) -> None: + """Smoke-test credentials by resolving the current API key. + + Uses ``iam.get_api_key`` because it does not require any prior + knowledge of the bearer or the owning organization. + + Args: + session: The Scaleway session to validate. + + Raises: + ScalewayAuthenticationError: Authentication or authorization + failed against the Scaleway IAM API. + """ + try: + iam = IamV1Alpha1API(session.client) + iam.get_api_key(access_key=session.access_key) + except Exception as error: + raise ScalewayAuthenticationError( + file=os.path.basename(__file__), + original_exception=error, + ) + + def print_credentials(self) -> None: + report_title = ( + f"{Style.BRIGHT}Using the Scaleway credentials below:{Style.RESET_ALL}" + ) + report_lines = [ + f"Authentication: {Fore.YELLOW}API Key{Style.RESET_ALL}", + f"Access Key: {Fore.YELLOW}{self._session.access_key}{Style.RESET_ALL}", + f"Organization ID: {Fore.YELLOW}{self._identity.organization_id}{Style.RESET_ALL}", + ] + if self._identity.bearer_type: + report_lines.append( + f"Bearer: {Fore.YELLOW}{self._identity.bearer_type}" + f" ({self._identity.bearer_email or self._identity.bearer_id})" + f"{Style.RESET_ALL}" + ) + if self._session.default_region: + report_lines.append( + f"Default Region: {Fore.YELLOW}{self._session.default_region}{Style.RESET_ALL}" + ) + + print_boxes(report_lines, report_title) + + @staticmethod + def test_connection( + access_key: str = None, + secret_key: str = None, + organization_id: str = None, + raise_on_exception: bool = True, + provider_id: str = None, + ) -> Connection: + """Test connection to Scaleway. + + Args: + access_key: Scaleway access key (falls back to SCW_ACCESS_KEY). + secret_key: Scaleway secret key (falls back to SCW_SECRET_KEY). + organization_id: Organization ID to scope the audit. + raise_on_exception: Whether to raise or return errors. + provider_id: Expected Scaleway organization ID. When provided, + the resolved identity must match it; otherwise the test + fails with ``ScalewayAuthenticationError``. + + Returns: + Connection: Connection object with is_connected status. + """ + try: + session = ScalewayProvider.setup_session( + access_key=access_key, + secret_key=secret_key, + organization_id=organization_id, + ) + ScalewayProvider.validate_credentials(session) + + # Guard for API callers that already know the expected + # organization: the credentials must point to that exact org. + if provider_id: + identity = ScalewayProvider.setup_identity(session) + if identity.organization_id != provider_id: + raise ScalewayAuthenticationError( + file=os.path.basename(__file__), + message=( + "The provided credentials do not have access to " + f"the Scaleway organization with ID: {provider_id}" + ), + ) + + return Connection(is_connected=True) + + except ( + ScalewayCredentialsError, + ScalewaySessionError, + ScalewayAuthenticationError, + ScalewayIdentityError, + ) as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + if raise_on_exception: + raise error + return Connection(is_connected=False, error=error) + + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + formatted_error = ScalewayAuthenticationError( + file=os.path.basename(__file__), + original_exception=error, + ) + if raise_on_exception: + raise formatted_error + return Connection(is_connected=False, error=formatted_error) + + def validate_arguments(self) -> None: + return None diff --git a/prowler/providers/scaleway/services/__init__.py b/prowler/providers/scaleway/services/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/scaleway/services/iam/__init__.py b/prowler/providers/scaleway/services/iam/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/scaleway/services/iam/iam_api_keys_no_root_owned/__init__.py b/prowler/providers/scaleway/services/iam/iam_api_keys_no_root_owned/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/scaleway/services/iam/iam_api_keys_no_root_owned/iam_api_keys_no_root_owned.metadata.json b/prowler/providers/scaleway/services/iam/iam_api_keys_no_root_owned/iam_api_keys_no_root_owned.metadata.json new file mode 100644 index 0000000000..15c6e800e0 --- /dev/null +++ b/prowler/providers/scaleway/services/iam/iam_api_keys_no_root_owned/iam_api_keys_no_root_owned.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "scaleway", + "CheckID": "iam_api_keys_no_root_owned", + "CheckTitle": "Scaleway IAM API keys must not be owned by the account root user", + "CheckType": [], + "ServiceName": "iam", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "critical", + "ResourceType": "NotDefined", + "ResourceGroup": "IAM", + "Description": "**Scaleway API keys** are checked to ensure none is bound to the **account root user**. The account root user is the original Scaleway account owner; its credentials bypass IAM policies and grant unrestricted access to the entire organization.", + "Risk": "API keys owned by the **account root user** cannot be scoped down with IAM policies. Leaking one of these keys yields immediate full control over every project, resource and billing setting in the organization, and rotating them disrupts every automation depending on root credentials.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.scaleway.com/en/docs/identity-and-access-management/iam/concepts/#root-account", + "https://www.scaleway.com/en/docs/identity-and-access-management/iam/how-to/create-api-keys/" + ], + "Remediation": { + "Code": { + "CLI": "scw iam api-key delete ", + "NativeIaC": "", + "Other": "1. Sign in to the Scaleway console as a user with IAM admin permissions.\n2. Create a dedicated IAM user or application scoped with the minimum required policy.\n3. Generate a new API key for that bearer and roll it out to the workloads currently using the root key.\n4. Delete the API key owned by the account root user from the IAM > API keys page.", + "Terraform": "" + }, + "Recommendation": { + "Text": "Never use API keys owned by the **account root user** for automation. Create scoped **IAM users** or **applications**, attach **least-privilege policies**, and rotate any existing root API keys to that new bearer.", + "Url": "https://hub.prowler.com/check/iam_api_keys_no_root_owned" + } + }, + "Categories": [ + "identity-access" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/scaleway/services/iam/iam_api_keys_no_root_owned/iam_api_keys_no_root_owned.py b/prowler/providers/scaleway/services/iam/iam_api_keys_no_root_owned/iam_api_keys_no_root_owned.py new file mode 100644 index 0000000000..5e98762758 --- /dev/null +++ b/prowler/providers/scaleway/services/iam/iam_api_keys_no_root_owned/iam_api_keys_no_root_owned.py @@ -0,0 +1,94 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportScaleway +from prowler.providers.scaleway.services.iam.iam_client import iam_client +from prowler.providers.scaleway.services.iam.iam_service import ( + ScalewayIAMDataUnavailable, +) + + +class iam_api_keys_no_root_owned(Check): + """Ensure no Scaleway IAM API key is owned by the account root user. + + The account root user is the original Scaleway account owner. API keys + bound to that bearer bypass IAM policies and grant unrestricted access + to the entire organization; rotating or losing them is a critical + incident. Day-to-day automation should rely on IAM users or + applications scoped through policies instead. + """ + + def execute(self) -> List[CheckReportScaleway]: + """Iterate over the API keys cached by the IAM service. + + The check degrades to ``MANUAL`` when the IAM service could not + load the prerequisite data (users or API keys) — emitting ``PASS`` + in those cases would silently mask the very condition the check + exists to detect. + + Returns: + One ``CheckReportScaleway`` per discovered API key. ``FAIL`` + when the bearer is the account root user, ``PASS`` otherwise. + A single ``MANUAL`` report is emitted when underlying IAM data + is unavailable. + """ + findings: List[CheckReportScaleway] = [] + + # If we could not even load the users we cannot tell who the root + # bearer is, so every API key would falsely PASS. Surface MANUAL + # explicitly so the operator investigates. + if not iam_client.users_loaded or not iam_client.api_keys_loaded: + placeholder = ScalewayIAMDataUnavailable( + organization_id=iam_client.organization_id + ) + report = CheckReportScaleway(metadata=self.metadata(), resource=placeholder) + report.status = "MANUAL" + report.status_extended = ( + "Could not retrieve Scaleway IAM users or API keys for " + f"organization {iam_client.organization_id}. Verify the " + "API key has the IAMReadOnly policy and rerun." + ) + findings.append(report) + return findings + + root_user_id = iam_client.account_root_user_id + + # The account root user could not be resolved (typically an + # application-scoped API key with no IAM users visible). Without it + # every key would fall through to PASS, masking root-owned keys, so + # surface MANUAL instead of a silent clean result. + if not root_user_id: + placeholder = ScalewayIAMDataUnavailable( + organization_id=iam_client.organization_id + ) + report = CheckReportScaleway(metadata=self.metadata(), resource=placeholder) + report.status = "MANUAL" + report.status_extended = ( + "Could not determine the Scaleway account root user for " + f"organization {iam_client.organization_id}. This typically " + "happens with application-scoped API keys when no IAM users " + "are visible. Verify the API key has the IAMReadOnly policy " + "and rerun." + ) + findings.append(report) + return findings + + for api_key in iam_client.api_keys: + report = CheckReportScaleway(metadata=self.metadata(), resource=api_key) + + if api_key.user_id == root_user_id: + report.status = "FAIL" + report.status_extended = ( + f"Scaleway API key {api_key.access_key} is owned by the " + f"account root user ({root_user_id}). Replace it with an " + f"API key bound to a dedicated IAM user or application." + ) + else: + report.status = "PASS" + report.status_extended = ( + f"Scaleway API key {api_key.access_key} is not owned by " + f"the account root user." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/scaleway/services/iam/iam_client.py b/prowler/providers/scaleway/services/iam/iam_client.py new file mode 100644 index 0000000000..af0704e629 --- /dev/null +++ b/prowler/providers/scaleway/services/iam/iam_client.py @@ -0,0 +1,4 @@ +from prowler.providers.common.provider import Provider +from prowler.providers.scaleway.services.iam.iam_service import IAM + +iam_client = IAM(Provider.get_global_provider()) diff --git a/prowler/providers/scaleway/services/iam/iam_service.py b/prowler/providers/scaleway/services/iam/iam_service.py new file mode 100644 index 0000000000..f37b5a84f3 --- /dev/null +++ b/prowler/providers/scaleway/services/iam/iam_service.py @@ -0,0 +1,166 @@ +from typing import Optional + +from pydantic.v1 import BaseModel +from scaleway.iam.v1alpha1 import IamV1Alpha1API + +from prowler.lib.logger import logger +from prowler.providers.scaleway.lib.service.service import ScalewayService + + +class IAM(ScalewayService): + """Scaleway IAM service. + + Loads the users in scope plus every API key tied to the current + organization. Checks consume the materialized lists; nothing in this + class is lazy. Each load operation tracks success/failure separately + so checks can degrade to ``MANUAL`` when data is incomplete instead of + falsely passing. + """ + + def __init__(self, provider): + super().__init__("iam", provider) + self._api = IamV1Alpha1API(self.client) + + # Cached state — populated eagerly during construction + self.users: list[ScalewayUser] = [] + self.api_keys: list[ScalewayAPIKey] = [] + + # Load status flags — checks consult these to surface MANUAL when + # the underlying API call failed rather than reporting empty lists + # as a clean PASS. + self.users_loaded: bool = False + self.api_keys_loaded: bool = False + + self._load_users() + self._load_api_keys() + + # Prefer the root user id resolved at authentication time from the + # audit identity. Application-scoped API keys do not expose it on + # the identity, so fall back to the loaded user list (every user + # record carries the org's account_root_user_id). When neither is + # available the root-key check degrades to MANUAL instead of + # silently PASSing root-owned keys. + self.account_root_user_id: Optional[str] = ( + provider.identity.account_root_user_id + or next( + (u.account_root_user_id for u in self.users if u.account_root_user_id), + None, + ) + ) + + def _load_users(self) -> None: + """List every IAM user in the audited organization.""" + try: + users = self._api.list_users_all(organization_id=self.organization_id) + for user in users: + self.users.append( + ScalewayUser( + id=user.id, + email=user.email, + username=user.username, + organization_id=user.organization_id, + account_root_user_id=user.account_root_user_id, + mfa=bool(getattr(user, "mfa", False)), + type_=( + str(user.type_) if getattr(user, "type_", None) else None + ), + status=( + str(user.status) if getattr(user, "status", None) else None + ), + ) + ) + + self.users_loaded = True + + except Exception as error: + logger.error( + f"{self.service} - Error listing users: " + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + def _load_api_keys(self) -> None: + """List every API key in the audited organization.""" + try: + api_keys = self._api.list_api_keys_all(organization_id=self.organization_id) + for key in api_keys: + self.api_keys.append( + ScalewayAPIKey( + access_key=key.access_key, + description=key.description, + user_id=key.user_id, + application_id=key.application_id, + default_project_id=key.default_project_id, + editable=bool(key.editable), + managed=bool(getattr(key, "managed", False)), + creation_ip=key.creation_ip, + created_at=str(key.created_at) if key.created_at else None, + updated_at=str(key.updated_at) if key.updated_at else None, + expires_at=str(key.expires_at) if key.expires_at else None, + ) + ) + + self.api_keys_loaded = True + + except Exception as error: + logger.error( + f"{self.service} - Error listing API keys: " + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + +class ScalewayUser(BaseModel): + """Subset of a Scaleway IAM user surface that the checks need.""" + + id: str + email: Optional[str] = None + username: Optional[str] = None + organization_id: Optional[str] = None + account_root_user_id: Optional[str] = None + mfa: bool = False + type_: Optional[str] = None + status: Optional[str] = None + # Provide name/id for CheckReportScaleway + name: str = "" + + def __init__(self, **data): + super().__init__(**data) + self.name = self.email or self.username or self.id + + +class ScalewayAPIKey(BaseModel): + """Subset of a Scaleway IAM API key surface that the checks need.""" + + access_key: str + description: Optional[str] = None + user_id: Optional[str] = None + application_id: Optional[str] = None + default_project_id: Optional[str] = None + editable: bool = False + managed: bool = False + creation_ip: Optional[str] = None + created_at: Optional[str] = None + updated_at: Optional[str] = None + expires_at: Optional[str] = None + # Provide name/id for CheckReportScaleway + name: str = "" + id: str = "" + + def __init__(self, **data): + super().__init__(**data) + self.id = self.access_key + self.name = self.description or self.access_key + + +class ScalewayIAMDataUnavailable(BaseModel): + """Stand-in resource used when the IAM service failed to load. + + Lets checks materialize a ``MANUAL`` finding (instead of a silent + ``PASS``) when users or API keys could not be retrieved. + ``CheckReportScaleway`` reads ``name``/``id``/``organization_id``/ + ``region`` off the resource, so exposing those is enough. + """ + + organization_id: str + name: str = "iam-data-unavailable" + id: str = "iam-data-unavailable" + region: str = "global" diff --git a/prowler/providers/vercel/models.py b/prowler/providers/vercel/models.py index 5f730bde76..d83e043d1e 100644 --- a/prowler/providers/vercel/models.py +++ b/prowler/providers/vercel/models.py @@ -9,7 +9,7 @@ from prowler.providers.common.models import ProviderOutputOptions class VercelSession(BaseModel): """Vercel API session information.""" - token: str + token: str = Field(exclude=True, repr=False) team_id: Optional[str] = None base_url: str = "https://api.vercel.com" http_session: Any = Field(default=None, exclude=True) diff --git a/pyproject.toml b/pyproject.toml index 74660c9c30..9c41081213 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,29 @@ [build-system] -build-backend = "poetry.core.masonry.api" -requires = ["poetry-core>=2.0"] +build-backend = "hatchling.build" +requires = ["hatchling"] + +[dependency-groups] +dev = [ + "bandit==1.8.3", + "black==25.1.0", + "coverage==7.6.12", + "docker==7.1.0", + "filelock==3.20.3", + "flake8==7.1.2", + "freezegun==1.5.1", + "mock==5.2.0", + "moto[all]==5.1.11", + "openapi-schema-validator==0.6.3", + "openapi-spec-validator==0.7.1", + "prek==0.3.9", + "pylint==3.3.4", + "pytest==8.3.5", + "pytest-cov==6.0.0", + "pytest-env==1.1.5", + "pytest-randomly==3.16.0", + "pytest-xdist==3.6.1", + "vulture==2.14" +] # https://peps.python.org/pep-0621/ [project] @@ -45,7 +68,7 @@ dependencies = [ "boto3==1.40.61", "botocore==1.40.61", "colorama==0.4.6", - "cryptography==46.0.6", + "cryptography==46.0.7", "dash==3.1.1", "dash-bootstrap-components==2.0.3", "defusedxml==0.7.1", @@ -59,6 +82,7 @@ dependencies = [ "microsoft-kiota-abstractions==1.9.2", "msgraph-sdk==1.55.0", "numpy==2.0.2", + "okta==3.4.2", "openstacksdk==4.2.0", "pandas==2.2.3", "py-ocsf-models==0.8.1", @@ -87,7 +111,8 @@ dependencies = [ "alibabacloud_actiontrail20200706==2.4.1", "alibabacloud_cs20151215==6.1.0", "alibabacloud-rds20140815==12.0.0", - "alibabacloud-sls20201230==5.9.0" + "alibabacloud-sls20201230==5.9.0", + "scaleway==2.10.3" ] description = "Prowler is an Open Source security tool to perform AWS, GCP and Azure security best practices assessments, audits, incident response, continuous monitoring, hardening and forensics readiness. It contains hundreds of controls covering CIS, NIST 800, NIST CSF, CISA, RBI, FedRAMP, PCI-DSS, GDPR, HIPAA, FFIEC, SOC2, GXP, AWS Well-Architected Framework Security Pillar, AWS Foundational Technical Review (FTR), ENS (Spanish National Security Scheme) and your custom security frameworks." license = "Apache-2.0" @@ -95,7 +120,7 @@ maintainers = [{name = "Prowler Engineering", email = "engineering@prowler.com"} name = "prowler" readme = "README.md" requires-python = ">=3.10,<3.13" -version = "5.26.0" +version = "5.29.0" [project.scripts] prowler = "prowler.__main__:prowler" @@ -106,41 +131,11 @@ prowler = "prowler.__main__:prowler" "Homepage" = "https://github.com/prowler-cloud/prowler" "Issue tracker" = "https://github.com/prowler-cloud/prowler/issues" -[tool.poetry] -packages = [ - {include = "prowler"}, - {include = "dashboard"} -] -requires-poetry = ">=2.0" +[tool.hatch.build.targets.sdist] +include = ["prowler", "dashboard"] -[tool.poetry.group.dev.dependencies] -bandit = "1.8.3" -black = "25.1.0" -coverage = "7.6.12" -docker = "7.1.0" -filelock = "3.20.3" -flake8 = "7.1.2" -freezegun = "1.5.1" -mock = "5.2.0" -moto = {extras = ["all"], version = "5.1.11"} -openapi-schema-validator = "0.6.3" -openapi-spec-validator = "0.7.1" -prek = "0.3.9" -pylint = "3.3.4" -pytest = "8.3.5" -pytest-cov = "6.0.0" -pytest-env = "1.1.5" -pytest-randomly = "3.16.0" -pytest-xdist = "3.6.1" -safety = "3.7.0" -vulture = "2.14" - -[tool.poetry-version-plugin] -source = "init" - -[tool.poetry_bumpversion.file."prowler/config/config.py"] -replace = 'prowler_version = "{new_version}"' -search = 'prowler_version = "{current_version}"' +[tool.hatch.build.targets.wheel] +packages = ["prowler", "dashboard"] [tool.pytest.ini_options] pythonpath = [ @@ -154,3 +149,214 @@ AWS_DEFAULT_REGION = 'us-east-1' AWS_SECRET_ACCESS_KEY = 'testing' AWS_SECURITY_TOKEN = 'testing' AWS_SESSION_TOKEN = 'testing' + +[tool.uv] +# Transitive pins matching the current lock to prevent silent drift on `uv lock` +# (e.g. supply chain hijacks via newer releases). Bump deliberately. +constraint-dependencies = [ + "about-time==4.2.1", + "aenum==3.1.17", + "aiofiles==24.1.0", + "aiohappyeyeballs==2.6.1", + "aiohttp==3.13.5", + "aiosignal==1.4.0", + "alibabacloud-actiontrail20200706==2.4.1", + "alibabacloud-credentials==1.0.3", + "alibabacloud-credentials-api==1.0.0", + "alibabacloud-cs20151215==6.1.0", + "alibabacloud-darabonba-array==0.1.0", + "alibabacloud-darabonba-encode-util==0.0.2", + "alibabacloud-darabonba-map==0.0.1", + "alibabacloud-darabonba-signature-util==0.0.4", + "alibabacloud-darabonba-string==0.0.4", + "alibabacloud-darabonba-time==0.0.1", + "alibabacloud-ecs20140526==7.2.5", + "alibabacloud-endpoint-util==0.0.4", + "alibabacloud-gateway-oss==0.0.17", + "alibabacloud-gateway-sls==0.4.2", + "alibabacloud-gateway-sls-util==0.4.1", + "alibabacloud-gateway-spi==0.0.3", + "alibabacloud-openapi-util==0.2.4", + "alibabacloud-oss-util==0.0.6", + "alibabacloud-oss20190517==1.0.6", + "alibabacloud-ram20150501==1.2.0", + "alibabacloud-sas20181203==6.1.0", + "alibabacloud-sts20150401==1.1.6", + "alibabacloud-tea==0.4.3", + "alibabacloud-tea-openapi==0.4.4", + "alibabacloud-tea-util==0.3.14", + "alibabacloud-tea-xml==0.0.3", + "alibabacloud-vpc20160428==6.13.0", + "aliyun-log-fastpb==0.3.0", + "annotated-types==0.7.0", + "antlr4-python3-runtime==4.13.2", + "anyio==4.13.0", + "apscheduler==3.11.2", + "astroid==3.3.11", + "async-timeout==5.0.1", + "attrs==26.1.0", + "aws-sam-translator==1.109.0", + "aws-xray-sdk==2.15.0", + "azure-common==1.1.28", + "azure-core==1.41.0", + "azure-mgmt-core==1.6.0", + "bandit==1.8.3", + "black==25.1.0", + "blinker==1.9.0", + "certifi==2026.4.22", + "cffi==2.0.0", + "cfn-lint==1.51.0", + "charset-normalizer==3.4.7", + "circuitbreaker==2.1.3", + "click==8.3.3", + "click-plugins==1.1.1.2", + "contextlib2==21.6.0", + "coverage==7.6.12", + "darabonba-core==1.0.5", + "decorator==5.2.1", + "dill==0.4.1", + "distro==1.9.0", + "dnspython==2.8.0", + "docker==7.1.0", + "dogpile-cache==1.5.0", + "durationpy==0.10", + "email-validator==2.2.0", + "exceptiongroup==1.3.1", + "execnet==2.1.2", + "filelock==3.20.3", + "flake8==7.1.2", + "flask==3.1.3", + "freezegun==1.5.1", + "frozenlist==1.8.0", + "google-api-core==2.30.3", + "google-auth==2.52.0", + "googleapis-common-protos==1.75.0", + "graphemeu==0.7.2", + "graphql-core==3.2.8", + "h11==0.16.0", + "hpack==4.1.0", + "httpcore==1.0.9", + "httplib2==0.31.2", + "httpx==0.28.1", + "hyperframe==6.1.0", + "iamdata==0.1.202605131", + "idna==3.15", + "importlib-metadata==8.7.1", + "iniconfig==2.3.0", + "iso8601==2.1.0", + "isodate==0.7.2", + "isort==6.1.0", + "itsdangerous==2.2.0", + "jinja2==3.1.6", + "jmespath==1.1.0", + "joserfc==1.6.5", + "jsonpatch==1.33", + "jsonpath-ng==1.8.0", + "jsonpointer==3.1.1", + "jsonschema-path==0.3.4", + "jsonschema-specifications==2025.9.1", + "jwcrypto==1.5.7", + "keystoneauth1==5.14.0", + "lazy-object-proxy==1.12.0", + "lz4==4.4.5", + "markdown-it-py==4.2.0", + "markupsafe==3.0.3", + "mccabe==0.7.0", + "mdurl==0.1.2", + "microsoft-kiota-authentication-azure==1.9.2", + "microsoft-kiota-http==1.9.2", + "microsoft-kiota-serialization-form==1.9.2", + "microsoft-kiota-serialization-json==1.9.2", + "microsoft-kiota-serialization-multipart==1.9.2", + "microsoft-kiota-serialization-text==1.9.2", + "mock==5.2.0", + "moto==5.1.11", + "mpmath==1.3.0", + "msal==1.36.0", + "msal-extensions==1.3.1", + "msgraph-core==1.3.8", + "msrest==0.7.1", + "multidict==6.7.1", + "multipart==1.3.1", + "mypy-extensions==1.1.0", + "narwhals==2.21.0", + "nest-asyncio==1.6.0", + "networkx==3.4.2", + "oauthlib==3.3.1", + "openapi-schema-validator==0.6.3", + "openapi-spec-validator==0.7.1", + "opentelemetry-api==1.41.1", + "opentelemetry-sdk==1.41.1", + "opentelemetry-semantic-conventions==0.62b1", + "os-service-types==1.8.2", + "packaging==26.2", + "pathable==0.4.4", + "pathspec==1.1.1", + "pbr==7.0.3", + "platformdirs==4.9.6", + "plotly==6.7.0", + "pluggy==1.6.0", + "prek==0.3.9", + "propcache==0.5.2", + "proto-plus==1.28.0", + "protobuf==7.34.1", + "psutil==7.2.2", + "py-partiql-parser==0.6.1", + "pyasn1==0.6.3", + "pyasn1-modules==0.4.2", + "pycodestyle==2.12.1", + "pycparser==3.0", + "pycryptodomex==3.23.0", + "pydantic-core==2.41.5", + "pydash==8.0.6", + "pyflakes==3.2.0", + "pygments==2.20.0", + "pyjwt==2.12.1", + "pylint==3.3.4", + "pynacl==1.6.2", + "pyopenssl==26.2.0", + "pyparsing==3.3.2", + "pytest==8.3.5", + "pytest-cov==6.0.0", + "pytest-env==1.1.5", + "pytest-randomly==3.16.0", + "pytest-xdist==3.6.1", + "pywin32==311", + "pyyaml==6.0.3", + "referencing==0.36.2", + "regex==2026.5.9", + "requests==2.34.0", + "requests-file==3.0.1", + "requests-oauthlib==2.0.0", + "requestsexceptions==1.4.0", + "responses==0.26.0", + "retrying==1.4.2", + "rfc3339-validator==0.1.4", + "rich==15.0.0", + "rpds-py==0.30.0", + "s3transfer==0.14.0", + "setuptools==82.0.1", + "six==1.17.0", + "sniffio==1.3.1", + "std-uritemplate==2.0.8", + "stevedore==5.7.0", + "sympy==1.14.0", + "tldextract==5.3.1", + "tomli==2.4.1", + "tomlkit==0.15.0", + "typing-extensions==4.15.0", + "typing-inspection==0.4.2", + "tzdata==2026.2", + "uritemplate==4.2.0", + "urllib3==2.7.0", + "vulture==2.14", + "websocket-client==1.9.0", + "werkzeug==3.1.8", + "wrapt==2.1.2", + "xlsxwriter==3.2.9", + "xmltodict==1.0.4", + "yarl==1.23.0", + "zipp==3.23.1", + "zstd==1.5.7.3" +] +override-dependencies = ["okta==3.4.2"] diff --git a/scripts/setup-git-hooks.sh b/scripts/setup-git-hooks.sh index c89ab607a7..c1d930b20f 100755 --- a/scripts/setup-git-hooks.sh +++ b/scripts/setup-git-hooks.sh @@ -1,7 +1,7 @@ #!/bin/bash # Setup Git Hooks for Prowler -# This script installs prek hooks using the project's Poetry environment +# This script installs prek hooks using the project's uv-managed environment # or a system-wide prek installation set -e @@ -32,27 +32,27 @@ fi echo "" -# Full setup requires Poetry for system hooks (pylint, bandit, safety, vulture, trufflehog) +# Full setup requires uv for system hooks (pylint, bandit, vulture, trufflehog) # These are installed as Python dev dependencies and used by local hooks in .pre-commit-config.yaml -if command -v poetry &>/dev/null && [ -f "pyproject.toml" ]; then - if poetry run prek --version &>/dev/null 2>&1; then - echo -e "${GREEN}✓${NC} prek and dependencies found via Poetry" +if command -v uv &>/dev/null && [ -f "pyproject.toml" ]; then + if uv run prek --version &>/dev/null 2>&1; then + echo -e "${GREEN}✓${NC} prek and dependencies found via uv" else echo -e "${YELLOW}📦 Installing project dependencies (including prek)...${NC}" - poetry install --with dev + uv sync fi echo -e "${YELLOW}🔗 Installing prek hooks...${NC}" - poetry run prek install --overwrite + uv run prek install --overwrite elif command -v prek &>/dev/null; then - # prek is available system-wide but without Poetry dev deps + # prek is available system-wide but without uv dev deps echo -e "${GREEN}✓${NC} prek found in PATH" echo -e "${YELLOW}🔗 Installing prek hooks...${NC}" prek install --overwrite echo "" - echo -e "${YELLOW}⚠️ Warning: Some hooks require Python tools installed via Poetry:${NC}" - echo -e " pylint, bandit, safety, vulture, trufflehog" + echo -e "${YELLOW}⚠️ Warning: Some hooks require Python tools installed via uv:${NC}" + echo -e " pylint, bandit, vulture, trufflehog" echo -e " These hooks will be skipped unless you install them or run:" - echo -e " ${GREEN}poetry install --with dev${NC}" + echo -e " ${GREEN}uv sync${NC}" else echo -e "${RED}❌ prek is not installed${NC}" echo -e "${YELLOW} Install prek using one of these methods:${NC}" diff --git a/skills/README.md b/skills/README.md index 1d689962af..47bf7e5562 100644 --- a/skills/README.md +++ b/skills/README.md @@ -36,7 +36,7 @@ After running setup, restart your AI coding assistant to load the skills. Skills are automatically discovered by the AI agent. To manually load a skill during a session: -``` +```text Read skills/{skill-name}/SKILL.md ``` @@ -50,7 +50,7 @@ Reusable patterns for common technologies: |-------|-------------| | `typescript` | Const types, flat interfaces, utility types | | `react-19` | React 19 patterns, React Compiler | -| `nextjs-15` | App Router, Server Actions, streaming | +| `nextjs-16` | App Router, Server Actions, proxy.ts, streaming | | `tailwind-4` | cn() utility, Tailwind 4 patterns | | `playwright` | Page Object Model, selectors | | `vitest` | Unit testing, React Testing Library | @@ -90,7 +90,7 @@ Patterns tailored for Prowler development: ## Directory Structure -``` +```text skills/ ├── {skill-name}/ │ ├── SKILL.md # Required - main instrunsction and metadata @@ -118,7 +118,7 @@ This reads `metadata.scope` and `metadata.auto_invoke` from each `SKILL.md` and Use the `skill-creator` skill for guidance: -``` +```text Read skills/skill-creator/SKILL.md ``` diff --git a/skills/django-drf/SKILL.md b/skills/django-drf/SKILL.md index 93ea6219f1..7d73ed1543 100644 --- a/skills/django-drf/SKILL.md +++ b/skills/django-drf/SKILL.md @@ -54,7 +54,7 @@ When implementing a new endpoint, review these patterns in order: ## Decision Trees ### Which Serializer? -``` +```text GET list/retrieve → Serializer POST create → CreateSerializer PATCH update → UpdateSerializer @@ -62,7 +62,7 @@ PATCH update → UpdateSerializer ``` ### Which Base Serializer? -``` +```text Read-only serializer → BaseModelSerializerV1 Create with tenant_id → RLSSerializer + BaseWriteSerializer (auto-injects tenant_id on create) Update with validation → BaseWriteSerializer (tenant_id already exists on object) @@ -70,14 +70,14 @@ Non-model data → BaseSerializerV1 ``` ### Which Filter Base? -``` +```text Direct FK to Provider → BaseProviderFilter FK via Scan → BaseScanProviderFilter No provider relation → FilterSet ``` ### Which Base ViewSet? -``` +```text RLS-protected model → BaseRLSViewSet (most common) Tenant operations → BaseTenantViewset User operations → BaseUserViewset @@ -85,7 +85,7 @@ No RLS required → BaseViewSet (rare) ``` ### Resource Name Format? -``` +```text Single word model → plural lowercase (Provider → providers) Multi-word model → plural lowercase kebab (ProviderGroup → provider-groups) Through/join model → parent-child pattern (UserRoleRelationship → user-roles) @@ -456,16 +456,16 @@ def get_object(self): ```bash # Development -cd api && poetry run python src/backend/manage.py runserver -cd api && poetry run python src/backend/manage.py shell +cd api && uv run python src/backend/manage.py runserver +cd api && uv run python src/backend/manage.py shell # Database -cd api && poetry run python src/backend/manage.py makemigrations -cd api && poetry run python src/backend/manage.py migrate +cd api && uv run python src/backend/manage.py makemigrations +cd api && uv run python src/backend/manage.py migrate # Testing -cd api && poetry run pytest -x --tb=short -cd api && poetry run make lint +cd api && uv run pytest -x --tb=short +cd api && uv run make lint ``` --- @@ -490,7 +490,7 @@ When implementing or debugging, query these libraries via `mcp_context7_query-do | **drf-spectacular** | `/tfranzel/drf-spectacular` | OpenAPI schema, `@extend_schema` | **Example queries:** -``` +```text mcp_context7_query-docs(libraryId="/websites/django-rest-framework", query="ViewSet get_queryset best practices") mcp_context7_query-docs(libraryId="/tfranzel/drf-spectacular", query="extend_schema examples for custom actions") mcp_context7_query-docs(libraryId="/websites/djangoproject_en_5_2", query="model constraints and indexes") diff --git a/skills/django-drf/references/file-locations.md b/skills/django-drf/references/file-locations.md index 30dab71550..d0f63ad042 100644 --- a/skills/django-drf/references/file-locations.md +++ b/skills/django-drf/references/file-locations.md @@ -16,7 +16,7 @@ ## ViewSet Hierarchy -``` +```text BaseViewSet (minimal - no RLS/auth) │ ├── BaseRLSViewSet (+ tenant filtering, RLS-protected models) @@ -31,7 +31,7 @@ BaseViewSet (minimal - no RLS/auth) ## Serializer Hierarchy -``` +```text BaseModelSerializerV1 (JSON:API defaults, read_only_fields) │ ├── RLSSerializer (auto-injects tenant_id from request) @@ -47,7 +47,7 @@ BaseModelSerializerV1 (JSON:API defaults, read_only_fields) ## Filter Hierarchy -``` +```text FilterSet (django-filter) │ ├── CommonFindingFilters (mixin for date ranges, delta, status) diff --git a/skills/django-drf/references/json-api-conventions.md b/skills/django-drf/references/json-api-conventions.md index c51326b0b2..9a546671fa 100644 --- a/skills/django-drf/references/json-api-conventions.md +++ b/skills/django-drf/references/json-api-conventions.md @@ -2,7 +2,7 @@ ## Content Type -``` +```http Content-Type: application/vnd.api+json Accept: application/vnd.api+json ``` diff --git a/skills/django-migration-psql/SKILL.md b/skills/django-migration-psql/SKILL.md index d292036dd0..bca8949fda 100644 --- a/skills/django-migration-psql/SKILL.md +++ b/skills/django-migration-psql/SKILL.md @@ -364,7 +364,7 @@ Batch utilities: `api/db_utils.py` (`batch_delete`, `create_objects_in_batches`, ## Decision tree -``` +```text Auto-generated migration? ├── Yes → Split it following the rules below └── No → Review it against the rules below @@ -420,7 +420,7 @@ When implementing or debugging migration patterns, query these libraries via `mc | django-postgres-extra | `/SectorLabs/django-postgres-extra` | Partitioned models, `PostgresPartitionedModel`, partition management | **Example queries:** -``` +```text mcp_context7_query-docs(libraryId="/websites/djangoproject_en_5_1", query="migration operations AddIndex RunPython atomic") mcp_context7_query-docs(libraryId="/websites/djangoproject_en_5_1", query="database indexes Meta class concurrently") mcp_context7_query-docs(libraryId="/websites/postgresql_org_docs_current", query="CREATE INDEX CONCURRENTLY partitioned table") diff --git a/skills/gh-aw/SKILL.md b/skills/gh-aw/SKILL.md index b45d4d151e..d4c3a0c829 100644 --- a/skills/gh-aw/SKILL.md +++ b/skills/gh-aw/SKILL.md @@ -30,7 +30,7 @@ allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch ## File Layout -``` +```text .github/ ├── workflows/ │ ├── {name}.md # Frontmatter + thin context dispatcher @@ -308,7 +308,7 @@ After modifying any `.github/workflows/*.md`: Add to repo root so lock files auto-resolve on merge: -``` +```text .github/workflows/*.lock.yml linguist-generated=true merge=ours ``` diff --git a/skills/jsonapi/SKILL.md b/skills/jsonapi/SKILL.md index a8959a2199..db11146929 100644 --- a/skills/jsonapi/SKILL.md +++ b/skills/jsonapi/SKILL.md @@ -35,7 +35,7 @@ This skill focuses on **spec compliance**. For **implementation patterns** (View If Context7 MCP is available, query the JSON:API spec directly: -``` +```text mcp_context7_resolve-library-id(query="jsonapi specification") mcp_context7_query-docs(libraryId="", query="[specific topic: relationships, errors, etc.]") ``` @@ -44,7 +44,7 @@ mcp_context7_query-docs(libraryId="", query="[specific topic: relat If Context7 is not available, fetch from the official spec: -``` +```text WebFetch(url="https://jsonapi.org/format/", prompt="Extract rules for [specific topic]") ``` diff --git a/skills/nextjs-15/SKILL.md b/skills/nextjs-15/SKILL.md deleted file mode 100644 index 793c1db2e1..0000000000 --- a/skills/nextjs-15/SKILL.md +++ /dev/null @@ -1,150 +0,0 @@ ---- -name: nextjs-15 -description: > - Next.js 15 App Router patterns. - Trigger: When working in Next.js App Router (app/), Server Components vs Client Components, Server Actions, Route Handlers, caching/revalidation, and streaming/Suspense. -license: Apache-2.0 -metadata: - author: prowler-cloud - version: "1.0" - scope: [root, ui] - auto_invoke: "App Router / Server Actions" -allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task ---- - -## App Router File Conventions - -``` -app/ -├── layout.tsx # Root layout (required) -├── page.tsx # Home page (/) -├── loading.tsx # Loading UI (Suspense) -├── error.tsx # Error boundary -├── not-found.tsx # 404 page -├── (auth)/ # Route group (no URL impact) -│ ├── login/page.tsx # /login -│ └── signup/page.tsx # /signup -├── api/ -│ └── route.ts # API handler -└── _components/ # Private folder (not routed) -``` - -## Server Components (Default) - -```typescript -// No directive needed - async by default -export default async function Page() { - const data = await db.query(); - return ; -} -``` - -## Server Actions - -```typescript -// app/actions.ts -"use server"; - -import { revalidatePath } from "next/cache"; -import { redirect } from "next/navigation"; - -export async function createUser(formData: FormData) { - const name = formData.get("name") as string; - - await db.users.create({ data: { name } }); - - revalidatePath("/users"); - redirect("/users"); -} - -// Usage -
    - - -
    -``` - -## Data Fetching - -```typescript -// Parallel -async function Page() { - const [users, posts] = await Promise.all([ - getUsers(), - getPosts(), - ]); - return ; -} - -// Streaming with Suspense -}> - - -``` - -## Route Handlers (API) - -```typescript -// app/api/users/route.ts -import { NextRequest, NextResponse } from "next/server"; - -export async function GET(request: NextRequest) { - const users = await db.users.findMany(); - return NextResponse.json(users); -} - -export async function POST(request: NextRequest) { - const body = await request.json(); - const user = await db.users.create({ data: body }); - return NextResponse.json(user, { status: 201 }); -} -``` - -## Middleware - -```typescript -// middleware.ts (root level) -import { NextResponse } from "next/server"; -import type { NextRequest } from "next/server"; - -export function middleware(request: NextRequest) { - const token = request.cookies.get("token"); - - if (!token && request.nextUrl.pathname.startsWith("/dashboard")) { - return NextResponse.redirect(new URL("/login", request.url)); - } - - return NextResponse.next(); -} - -export const config = { - matcher: ["/dashboard/:path*"], -}; -``` - -## Metadata - -```typescript -// Static -export const metadata = { - title: "My App", - description: "Description", -}; - -// Dynamic -export async function generateMetadata({ params }) { - const product = await getProduct(params.id); - return { title: product.name }; -} -``` - -## server-only Package - -```typescript -import "server-only"; - -// This will error if imported in client component -export async function getSecretData() { - return db.secrets.findMany(); -} -``` diff --git a/skills/nextjs-16/SKILL.md b/skills/nextjs-16/SKILL.md new file mode 100644 index 0000000000..cea38e255d --- /dev/null +++ b/skills/nextjs-16/SKILL.md @@ -0,0 +1,160 @@ +--- +name: nextjs-16 +description: > + Next.js 16 App Router patterns. + Trigger: When working in Next.js App Router (app/), Server Components vs Client Components, Server Actions, Route Handlers, proxy.ts, caching/revalidation, Cache Components, and streaming/Suspense. +license: Apache-2.0 +metadata: + author: prowler-cloud + version: "1.0" + scope: [root, ui] + auto_invoke: "App Router / Server Actions" +allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task +--- + +## App Router File Conventions + +```text +app/ +├── layout.tsx # Root layout (required) +├── page.tsx # Home page (/) +├── loading.tsx # Loading UI (Suspense) +├── error.tsx # Error boundary +├── not-found.tsx # 404 page +├── (auth)/ # Route group (no URL impact) +│ ├── login/page.tsx # /login +│ └── signup/page.tsx # /signup +├── api/ +│ └── route.ts # API handler +└── _components/ # Private folder (not routed) +``` + +## Next.js 16 Notes + +- Use `proxy.ts` for request-boundary logic. `middleware.ts` is deprecated in Next.js 16. +- `proxy.ts` runs on the Node.js runtime and cannot be configured for Edge. +- Keep `proxy.ts` matchers narrow. Exclude `api`, static files, and image assets unless the route explicitly needs proxy logic. +- Route Handlers in `app/api/**/route.ts` are the right fit for health checks, webhooks, backend-for-frontend endpoints, and server-only proxy calls. + +## Server Components (Default) + +```typescript +// No directive needed - async by default +export default async function Page() { + const data = await db.query(); + return ; +} +``` + +## Server Actions + +```typescript +"use server"; + +import { revalidatePath } from "next/cache"; +import { redirect } from "next/navigation"; + +export async function createUser(formData: FormData) { + const name = formData.get("name") as string; + + await db.users.create({ data: { name } }); + + revalidatePath("/users"); + redirect("/users"); +} +``` + +## Data Fetching + +```typescript +async function Page() { + const [users, posts] = await Promise.all([getUsers(), getPosts()]); + + return ; +} + +}> + +; +``` + +## Caching and Revalidation + +```typescript +import { revalidatePath, revalidateTag } from "next/cache"; + +export async function refreshDashboard() { + "use server"; + + revalidatePath("/"); + revalidateTag("dashboard"); +} +``` + +- Use `revalidatePath` for route-level invalidation after mutations. +- Use `revalidateTag` when data fetches share a cache tag across routes. +- With Cache Components enabled, put `"use cache"` only in pure server-side cached functions. Do not cache auth, tenant-scoped, or per-user responses unless the cache key explicitly isolates them. + +## Route Handlers (API) + +```typescript +// app/api/users/route.ts +import { NextResponse } from "next/server"; + +export async function GET() { + const users = await db.users.findMany(); + return NextResponse.json(users); +} + +export async function POST(request: Request) { + const body = await request.json(); + const user = await db.users.create({ data: body }); + return NextResponse.json(user, { status: 201 }); +} +``` + +## Proxy + +```typescript +// proxy.ts (root level) +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; + +export function proxy(request: NextRequest) { + const token = request.cookies.get("token"); + + if (!token && request.nextUrl.pathname.startsWith("/dashboard")) { + return NextResponse.redirect(new URL("/login", request.url)); + } + + return NextResponse.next(); +} + +export const config = { + matcher: ["/dashboard/:path*"], +}; +``` + +## Metadata + +```typescript +export const metadata = { + title: "My App", + description: "Description", +}; + +export async function generateMetadata() { + const product = await getProduct(); + return { title: product.name }; +} +``` + +## server-only Package + +```typescript +import "server-only"; + +export async function getSecretData() { + return db.secrets.findMany(); +} +``` diff --git a/skills/playwright/SKILL.md b/skills/playwright/SKILL.md index d9009d6f4a..60c1db9fd5 100644 --- a/skills/playwright/SKILL.md +++ b/skills/playwright/SKILL.md @@ -36,7 +36,7 @@ allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task ## File Structure -``` +```text tests/ ├── base-page.ts # Parent class for ALL pages ├── helpers.ts # Shared utilities @@ -182,14 +182,14 @@ export class SignUpPage extends BasePage { ## Refactoring Guidelines -### Move to `BasePage` when: +### Move to `BasePage` when - ✅ Navigation helpers used by multiple pages (`waitForPageLoad()`, `getCurrentUrl()`) - ✅ Common UI interactions (notifications, modals, theme toggles) - ✅ Verification patterns repeated across pages (`isVisible()`, `waitForVisible()`) - ✅ Error handling that applies to all pages - ✅ Screenshot utilities for debugging -### Move to `helpers.ts` when: +### Move to `helpers.ts` when - ✅ Test data generation (`generateUniqueEmail()`, `generateTestUser()`) - ✅ Setup/teardown utilities (`createTestUser()`, `cleanupTestData()`) - ✅ Custom assertions (`expectNotificationToContain()`) diff --git a/skills/postgresql-indexing/SKILL.md b/skills/postgresql-indexing/SKILL.md index 7fac9f4ecd..615000ad9e 100644 --- a/skills/postgresql-indexing/SKILL.md +++ b/skills/postgresql-indexing/SKILL.md @@ -225,7 +225,7 @@ Rebuild invalid indexes without locking writes: REINDEX INDEX CONCURRENTLY index_name; ``` -### Understanding _ccnew and _ccold artifacts +### Understanding _ccnew and_ccold artifacts When `CREATE INDEX CONCURRENTLY` or `REINDEX INDEX CONCURRENTLY` is interrupted, temporary indexes may remain: @@ -377,7 +377,8 @@ VACUUM (ANALYZE) table_name; | PostgreSQL | `/websites/postgresql_org_docs_current` | Index types, EXPLAIN, partitioned table indexing, REINDEX | **Example queries:** -``` + +```text mcp_context7_query-docs(libraryId="/websites/postgresql_org_docs_current", query="CREATE INDEX CONCURRENTLY partitioned table") mcp_context7_query-docs(libraryId="/websites/postgresql_org_docs_current", query="EXPLAIN ANALYZE BUFFERS query plan") mcp_context7_query-docs(libraryId="/websites/postgresql_org_docs_current", query="partial index WHERE clause") diff --git a/skills/prowler-api/SKILL.md b/skills/prowler-api/SKILL.md index 1e654a3e03..0c7dc86222 100644 --- a/skills/prowler-api/SKILL.md +++ b/skills/prowler-api/SKILL.md @@ -61,7 +61,7 @@ Provider.objects.filter(connected=True) # Requires rls_transaction context ### RLS Transaction Flow -``` +```text Request → Authentication → BaseRLSViewSet.initial() │ ├─ Extract tenant_id from JWT @@ -92,7 +92,7 @@ When implementing Prowler-specific API features: ## Decision Trees ### Which Base Model? -``` +```text Tenant-scoped data → RowLevelSecurityProtectedModel Global/shared data → models.Model + BaseSecurityConstraint (rare) Partitioned time-series → PostgresPartitionedModel + RowLevelSecurityProtectedModel @@ -100,14 +100,14 @@ Soft-deletable → Add is_deleted + ActiveProviderManager ``` ### Which Manager? -``` +```text Normal queries → Model.objects (excludes deleted) Include deleted records → Model.all_objects Celery task context → Must use rls_transaction() first ``` ### Which Database? -``` +```text Standard API queries → default (automatic via ViewSet) Read-only operations → replica (automatic for GET in BaseRLSViewSet) Auth/admin operations → MainRouter.admin_db @@ -115,7 +115,7 @@ Cross-tenant lookups → MainRouter.admin_db (use sparingly!) ``` ### Celery Task Decorator Order? -``` +```python @shared_task(base=RLSTask, name="...", queue="...") @set_tenant # First: sets tenant context @handle_provider_deletion # Second: handles deleted providers @@ -432,7 +432,7 @@ def process_finding(tenant_id, finding_uid, data): Run before every production deployment: ```bash -cd api && poetry run python src/backend/manage.py check --deploy +cd api && uv run python src/backend/manage.py check --deploy ``` ### Critical Settings @@ -454,18 +454,18 @@ cd api && poetry run python src/backend/manage.py check --deploy ```bash # Development -cd api && poetry run python src/backend/manage.py runserver -cd api && poetry run python src/backend/manage.py shell +cd api && uv run python src/backend/manage.py runserver +cd api && uv run python src/backend/manage.py shell # Celery -cd api && poetry run celery -A config.celery worker -l info -Q scans,overview -cd api && poetry run celery -A config.celery beat -l info +cd api && uv run celery -A config.celery worker -l info -Q scans,overview +cd api && uv run celery -A config.celery beat -l info # Testing -cd api && poetry run pytest -x --tb=short +cd api && uv run pytest -x --tb=short # Production checks -cd api && poetry run python src/backend/manage.py check --deploy +cd api && uv run python src/backend/manage.py check --deploy ``` --- @@ -496,7 +496,7 @@ When implementing or debugging Prowler-specific patterns, query these libraries | **Django** | `/websites/djangoproject_en_5_2` | Models, ORM, constraints, indexes | **Example queries:** -``` +```text mcp_context7_query-docs(libraryId="/websites/celeryq_dev_en_stable", query="shared_task decorator retry patterns") mcp_context7_query-docs(libraryId="/celery/django-celery-beat", query="periodic task database scheduler") mcp_context7_query-docs(libraryId="/websites/djangoproject_en_5_2", query="model constraints CheckConstraint UniqueConstraint") diff --git a/skills/prowler-api/references/configuration.md b/skills/prowler-api/references/configuration.md index 677f999cc4..0a68d58951 100644 --- a/skills/prowler-api/references/configuration.md +++ b/skills/prowler-api/references/configuration.md @@ -2,7 +2,7 @@ ## Settings File Structure -``` +```text api/src/backend/config/ ├── django/ │ ├── base.py # Base settings (all environments) diff --git a/skills/prowler-api/references/modeling-decisions.md b/skills/prowler-api/references/modeling-decisions.md index 68923c4ef4..c11ed585ec 100644 --- a/skills/prowler-api/references/modeling-decisions.md +++ b/skills/prowler-api/references/modeling-decisions.md @@ -247,7 +247,7 @@ class JSONAPIMeta: ## Decision Tree: New Model -``` +```text Is it tenant-scoped data? ├── Yes → Inherit RowLevelSecurityProtectedModel │ Add RowLevelSecurityConstraint diff --git a/skills/prowler-api/references/production-settings.md b/skills/prowler-api/references/production-settings.md index 12b64483bf..15425f624f 100644 --- a/skills/prowler-api/references/production-settings.md +++ b/skills/prowler-api/references/production-settings.md @@ -3,7 +3,7 @@ ## Django Deployment Checklist Command ```bash -cd api && poetry run python src/backend/manage.py check --deploy +cd api && uv run python src/backend/manage.py check --deploy ``` This command checks for common deployment issues and missing security settings. diff --git a/skills/prowler-attack-paths-query/SKILL.md b/skills/prowler-attack-paths-query/SKILL.md index fb25d4fa43..6dad976983 100644 --- a/skills/prowler-attack-paths-query/SKILL.md +++ b/skills/prowler-attack-paths-query/SKILL.md @@ -228,7 +228,7 @@ AWS_QUERIES: list[AttackPathsQueryDefinition] = [ **FIRST**, read all files in the queries module to understand the structure, type definitions, registration, and existing style: -``` +```text api/src/backend/api/attack_paths/queries/ ├── __init__.py # Module exports ├── types.py # AttackPathsQueryDefinition, AttackPathsQueryParameterDefinition @@ -250,7 +250,7 @@ grep cartography api/pyproject.toml Build the schema URL (ALWAYS use the specific tag, not master/main): -``` +```text # Git dependency (prowler-cloud/cartography@0.126.1): https://raw.githubusercontent.com/prowler-cloud/cartography/refs/tags/0.126.1/docs/root/modules/{provider}/schema.md @@ -283,7 +283,7 @@ Add the constant to the `{PROVIDER}_QUERIES` list. ### Query ID -``` +```text {provider}-{category}-{description} ``` @@ -291,7 +291,7 @@ Examples: `aws-ec2-privesc-passrole-iam`, `aws-ec2-instances-internet-exposed` ### Query constant name -``` +```text {PROVIDER}_{CATEGORY}_{DESCRIPTION} ``` diff --git a/skills/prowler-changelog/SKILL.md b/skills/prowler-changelog/SKILL.md index 94f119e2ba..67e853b0fd 100644 --- a/skills/prowler-changelog/SKILL.md +++ b/skills/prowler-changelog/SKILL.md @@ -71,10 +71,13 @@ allowed-tools: Read, Edit, Write, Glob, Grep, Bash - **Blank line after section header** before first entry - **Blank line between sections** - Be specific: what changed, not why (that's in the PR) +- Keep entries readable: use spaces around inline code and product names, and wrap endpoints, commands, errors, task names, and file paths in backticks +- Avoid long run-on sentences; split complex changes into one concise result plus one concise context clause - One entry per PR (can link multiple PRs for related changes) - No period at the end - Do NOT start with redundant verbs (section header already provides the action) - **CRITICAL: Preserve section order** — when adding a new section to the UNRELEASED block, insert it in the correct position relative to existing sections (Added → Changed → Deprecated → Removed → Fixed → Security). Never append a new section at the top or bottom without checking order +- **CRITICAL: ALWAYS link to the PR, NEVER to the issue.** Every entry MUST use `https://github.com/prowler-cloud/prowler/pull/N`. Linking to `/issues/N` is FORBIDDEN, even when the PR fixes an issue. The issue↔PR relationship belongs in the PR body (`Fixes #N`), not in the changelog. If a fix has no PR yet, do not add the entry until the PR exists. ### Semantic Versioning Rules @@ -114,6 +117,34 @@ Prowler follows [semver.org](https://semver.org/): --- # Horizontal rule between versions ``` +## Mandatory Changelog Preflight + +Before editing any `CHANGELOG.md`, always inspect the active release boundary: + +1. Read the UNRELEASED block plus the latest three released version blocks: + ```bash + awk '/^## \[/{n++} n<=4 {print}' ui/CHANGELOG.md + ``` +2. Identify the **only writable block**: the block whose header contains `(Prowler UNRELEASED)`. +3. Treat every block whose header contains `(Prowler vX.Y.Z)` as immutable. Do not add, move, reword, reorder, or deduplicate entries there. +4. If your PR's entry appears in any of the latest three released blocks, remove it from the released block and add it to the correct section in the UNRELEASED block. +5. If there is no UNRELEASED block at the top, stop and ask before editing. + +**Do not trust the current topmost matching section name.** A released block can contain the same section heading (`### 🚀 Added`, `### 🔄 Changed`, etc.). Always anchor edits to the `Prowler UNRELEASED` version block first. + +## Mandatory Human Confirmation Gate + +Before creating or editing any changelog file (`CHANGELOG.md`), the agent MUST stop and get explicit user confirmation. This applies even when the changelog gate is failing, the required edit seems obvious, or the user asked to "fix the changelog". + +Present the proposed changelog action before writing: + +1. Target file path. +2. Target version block and section. +3. Exact entry to add, move, remove, or rewrite. +4. Reason the changelog is needed. + +Only proceed after an explicit approval such as "confirm", "approved", "sí", or equivalent. If the user rejects or does not answer, do not edit or create the changelog. Offer alternatives such as adding `no-changelog` when appropriate. + ## Adding a Changelog Entry ### Step 1: Determine Affected Component(s) @@ -146,6 +177,8 @@ git diff main...HEAD --name-only **CRITICAL:** Add new entries at the BOTTOM of each section, NOT at the top. +**CRITICAL:** The link MUST point to the PR (`/pull/N`). Linking to `/issues/N` is FORBIDDEN. If the PR closes an issue, that mapping goes in the PR body via `Fixes #N` — never in the changelog entry. + ```markdown ## [1.17.0] (Prowler UNRELEASED) @@ -175,6 +208,15 @@ This maintains chronological order within each section (oldest at top, newest at - Node.js from 20.x to 24.13.0 LTS, patching 8 CVEs [(#9797)](https://github.com/prowler-cloud/prowler/pull/9797) ``` +### Readable Technical Entries + +```markdown +# GOOD - Technical but readable +### 🐞 Fixed +- `POST /api/v1/scans` no longer intermittently fails with `Scan matching query does not exist`; scan dispatch now publishes the `scan-perform` Celery task after the transaction commits [(#11122)](https://github.com/prowler-cloud/prowler/pull/11122) +- `entra_users_mfa_capable` no longer flags disabled guest users; Microsoft Graph is now the source of truth for `account_enabled` because EXO `Get-User` omits guest users [(#11002)](https://github.com/prowler-cloud/prowler/pull/11002) +``` + ### Bad Entries ```markdown @@ -189,6 +231,9 @@ This maintains chronological order within each section (oldest at top, newest at - Added new feature for users # Missing PR link, redundant verb - Add search bar [(#123)] # Redundant verb (section already says "Added") - This PR adds a cool new thing (#123) # Wrong link format, conversational +- Some bug fix [(#123)](https://github.com/prowler-cloud/prowler/issues/123) # FORBIDDEN: must link to /pull/N, never /issues/N +- POST /api/v1/scanswas intermittently failing withScan matching query does not existin thescan-performworker (#11122) # Missing spaces/backticks, unreadable +- entra_users_mfa_capable no longer flags disabled guest users by requesting accountEnabled and userType from Microsoft Graph via $select and using Graph as the source of truth for account_enabled (EXO Get-User does not return guest users) (#11002) # Run-on sentence, identifiers not formatted ``` ## PR Changelog Gate diff --git a/skills/prowler-changelog/assets/entry-templates.md b/skills/prowler-changelog/assets/entry-templates.md index dbca5bf25f..cc74efbb63 100644 --- a/skills/prowler-changelog/assets/entry-templates.md +++ b/skills/prowler-changelog/assets/entry-templates.md @@ -20,6 +20,8 @@ This maintains chronological order: oldest entries at top, newest at bottom. ## Entry Patterns > **Note:** Section headers already provide the verb. Entries describe WHAT, not the action. +> +> **Link target rule:** Every entry MUST link to the PR (`https://github.com/prowler-cloud/prowler/pull/N`). Linking to `/issues/N` is FORBIDDEN — even when the PR fixes an issue. The issue↔PR mapping belongs in the PR body (`Fixes #N`), not here. ### Feature Addition (🚀 Added) ```markdown @@ -40,6 +42,8 @@ This maintains chronological order: oldest entries at top, newest at bottom. - {What was broken} in {component} [(#XXXX)](https://github.com/prowler-cloud/prowler/pull/XXXX) ``` +> When a PR fixes a reported issue, the link still goes to the PR (`/pull/N`), never the issue (`/issues/N`). Reference the issue from the PR body with `Fixes #N`. + ### Security Patch (🔐 Security) ```markdown - Node.js from 20.x to 24.13.0 LTS, patching 8 CVEs [(#XXXX)](https://github.com/prowler-cloud/prowler/pull/XXXX) diff --git a/skills/prowler-commit/SKILL.md b/skills/prowler-commit/SKILL.md index 0f67cbe882..30dbbc49cd 100644 --- a/skills/prowler-commit/SKILL.md +++ b/skills/prowler-commit/SKILL.md @@ -28,7 +28,7 @@ metadata: ## Commit Format -``` +```text type(scope): concise description - Key change 1 @@ -68,7 +68,7 @@ type(scope): concise description ### Title Line -``` +```text # GOOD - Concise and clear feat(api): add provider connection retry logic fix(ui): resolve dashboard loading state @@ -83,7 +83,7 @@ fix(ui): fix the bug in dashboard component on line 45 ### Body (Bullet Points) -``` +```text # GOOD - High-level changes - Add retry mechanism for failed connections - Document task composition patterns @@ -132,7 +132,7 @@ fix(ui): fix the bug in dashboard component on line 45 ## Decision Tree -``` +```text Single file changed? ├─ Yes → May omit body, title only └─ No → Include body with key changes diff --git a/skills/prowler-compliance-review/SKILL.md b/skills/prowler-compliance-review/SKILL.md index a494858eba..06f371f81b 100644 --- a/skills/prowler-compliance-review/SKILL.md +++ b/skills/prowler-compliance-review/SKILL.md @@ -53,7 +53,7 @@ diff dashboard/compliance/{new_framework}.py \ ## Decision Tree -``` +```text JSON Valid? ├── No → FAIL: Fix JSON syntax errors └── Yes ↓ @@ -168,16 +168,16 @@ After validation passes, test the framework with Prowler: ```bash # Verify framework is detected -poetry run python prowler-cli.py {provider} --list-compliance | grep {framework} +uv run python prowler-cli.py {provider} --list-compliance | grep {framework} # Run a quick test with a single check from the framework -poetry run python prowler-cli.py {provider} --compliance {framework} --check {check_name} +uv run python prowler-cli.py {provider} --compliance {framework} --check {check_name} # Run full compliance scan (dry-run with limited checks) -poetry run python prowler-cli.py {provider} --compliance {framework} --checks-limit 5 +uv run python prowler-cli.py {provider} --compliance {framework} --checks-limit 5 # Generate compliance report in multiple formats -poetry run python prowler-cli.py {provider} --compliance {framework} -M csv json html +uv run python prowler-cli.py {provider} --compliance {framework} -M csv json html ``` --- diff --git a/skills/prowler-compliance/SKILL.md b/skills/prowler-compliance/SKILL.md index 51c68eb05f..f119c7fa9b 100644 --- a/skills/prowler-compliance/SKILL.md +++ b/skills/prowler-compliance/SKILL.md @@ -59,7 +59,7 @@ See "Compliance Framework Location" and "Framework-Specific Attribute Structures **Every framework directory follows this exact convention** — do not deviate: -``` +```text {framework}/ ├── __init__.py ├── {framework}.py # ONLY get_{framework}_table() — NO function docstring @@ -85,7 +85,7 @@ See "Compliance Framework Location" and "Framework-Specific Attribute Structures ### The CLI Pipeline (end-to-end) -``` +```text prowler aws --compliance ccc_aws ↓ Compliance.get_bulk("aws") → parses prowler/compliance/aws/*.json @@ -483,6 +483,7 @@ Prowler ThreatScore is a custom security scoring framework developed by Prowler ## Available Compliance Frameworks ### AWS (41 frameworks) + | Framework | File Name | |-----------|-----------| | CIS 1.4, 1.5, 2.0, 3.0, 4.0, 5.0 | `cis_{version}_aws.json` | @@ -508,6 +509,7 @@ Prowler ThreatScore is a custom security scoring framework developed by Prowler | NIS2 | `nis2_aws.json` | ### Azure (15+ frameworks) + | Framework | File Name | |-----------|-----------| | CIS 2.0, 2.1, 3.0, 4.0 | `cis_{version}_azure.json` | @@ -518,6 +520,7 @@ Prowler ThreatScore is a custom security scoring framework developed by Prowler | NIST CSF 2.0 | `nist_csf_2.0_azure.json` | ### GCP (15+ frameworks) + | Framework | File Name | |-----------|-----------| | CIS 2.0, 3.0, 4.0 | `cis_{version}_gcp.json` | @@ -528,6 +531,7 @@ Prowler ThreatScore is a custom security scoring framework developed by Prowler | NIST CSF 2.0 | `nist_csf_2.0_gcp.json` | ### Kubernetes (6 frameworks) + | Framework | File Name | |-----------|-----------| | CIS 1.8, 1.10, 1.11 | `cis_{version}_kubernetes.json` | @@ -561,7 +565,7 @@ done The sync tooling is split into three layers so adding a new framework only takes a YAML config (and optionally a new parser module for an unfamiliar upstream format): -``` +```text skills/prowler-compliance/assets/ ├── sync_framework.py # generic runner — works for any framework ├── configs/ @@ -902,7 +906,7 @@ Add fixtures to `tests/lib/outputs/compliance/fixtures.py`: one `Compliance` obj **The table dispatcher file (`{framework}.py`) MUST NOT import `Finding`** (directly or transitively). The cycle is: -``` +```text compliance.compliance imports get_{framework}_table → {framework}.py imports ComplianceOutput → compliance_output imports Finding diff --git a/skills/prowler-compliance/references/compliance-docs.md b/skills/prowler-compliance/references/compliance-docs.md index 62d619953f..a8d11484a9 100644 --- a/skills/prowler-compliance/references/compliance-docs.md +++ b/skills/prowler-compliance/references/compliance-docs.md @@ -46,7 +46,7 @@ Each framework type has a specific Pydantic model in `compliance_models.py`: ## File Naming Convention -``` +```text {framework}_{version}_{provider}.json ``` diff --git a/skills/prowler-docs/SKILL.md b/skills/prowler-docs/SKILL.md index 41a97f5ff9..4c4c64967f 100644 --- a/skills/prowler-docs/SKILL.md +++ b/skills/prowler-docs/SKILL.md @@ -104,7 +104,7 @@ Reference without articles: ## Documentation Structure -``` +```text docs/ ├── getting-started/ ├── tutorials/ diff --git a/skills/prowler-provider/SKILL.md b/skills/prowler-provider/SKILL.md index c9f2811e97..8042cd9f5a 100644 --- a/skills/prowler-provider/SKILL.md +++ b/skills/prowler-provider/SKILL.md @@ -25,7 +25,7 @@ Use this skill when: Every provider MUST follow this structure: -``` +```text prowler/providers/{provider}/ ├── __init__.py ├── {provider}_provider.py # Main provider class @@ -155,19 +155,19 @@ Current providers: ```bash # Run provider -poetry run python prowler-cli.py {provider} +uv run python prowler-cli.py {provider} # List services for provider -poetry run python prowler-cli.py {provider} --list-services +uv run python prowler-cli.py {provider} --list-services # List checks for provider -poetry run python prowler-cli.py {provider} --list-checks +uv run python prowler-cli.py {provider} --list-checks # Run specific service -poetry run python prowler-cli.py {provider} --services {service} +uv run python prowler-cli.py {provider} --services {service} # Debug mode -poetry run python prowler-cli.py {provider} --log-level DEBUG +uv run python prowler-cli.py {provider} --log-level DEBUG ``` ## Resources diff --git a/skills/prowler-readme-table/SKILL.md b/skills/prowler-readme-table/SKILL.md index d6f9c6e780..6cea125ca0 100644 --- a/skills/prowler-readme-table/SKILL.md +++ b/skills/prowler-readme-table/SKILL.md @@ -34,7 +34,7 @@ python3 prowler-cli.py --list- The CLI output ends with a summary line like: -``` +```text There are 572 available checks. There is 1 available Compliance Framework. ``` diff --git a/skills/prowler-sdk-check/SKILL.md b/skills/prowler-sdk-check/SKILL.md index c9319f705c..1901e996d4 100644 --- a/skills/prowler-sdk-check/SKILL.md +++ b/skills/prowler-sdk-check/SKILL.md @@ -16,7 +16,7 @@ allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task ## Check Structure -``` +```text prowler/providers/{provider}/services/{service}/{check_name}/ ├── __init__.py ├── {check_name}.py @@ -73,13 +73,13 @@ For detailed field documentation, see `references/metadata-docs.md`. ### 5. Verify Check Detection ```bash -poetry run python prowler-cli.py {provider} --list-checks | grep {check_name} +uv run python prowler-cli.py {provider} --list-checks | grep {check_name} ``` ### 6. Run Check Locally ```bash -poetry run python prowler-cli.py {provider} --log-level ERROR --verbose --check {check_name} +uv run python prowler-cli.py {provider} --log-level ERROR --verbose --check {check_name} ``` ### 7. Create Tests @@ -90,7 +90,7 @@ See `prowler-test-sdk` skill for test patterns (PASS, FAIL, no resources, error ## Check Naming Convention -``` +```text {service}_{resource}_{security_control} ``` @@ -243,16 +243,16 @@ class ec2_instance_hardened(Check): ```bash # Verify detection -poetry run python prowler-cli.py {provider} --list-checks | grep {check_name} +uv run python prowler-cli.py {provider} --list-checks | grep {check_name} # Run check -poetry run python prowler-cli.py {provider} --log-level ERROR --verbose --check {check_name} +uv run python prowler-cli.py {provider} --log-level ERROR --verbose --check {check_name} # Run with specific profile/credentials -poetry run python prowler-cli.py aws --profile myprofile --check {check_name} +uv run python prowler-cli.py aws --profile myprofile --check {check_name} # Run multiple checks -poetry run python prowler-cli.py {provider} --check {check1} {check2} {check3} +uv run python prowler-cli.py {provider} --check {check1} {check2} {check3} ``` ## Resources diff --git a/skills/prowler-test-api/SKILL.md b/skills/prowler-test-api/SKILL.md index fc00ed31cc..5b3e2ae6a1 100644 --- a/skills/prowler-test-api/SKILL.md +++ b/skills/prowler-test-api/SKILL.md @@ -28,7 +28,7 @@ allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task ## 1. Fixture Dependency Chain -``` +```text create_test_user (session) ─► tenants_fixture (function) ─► authenticated_client │ └─► providers_fixture ─► scans_fixture ─► findings_fixture @@ -153,9 +153,9 @@ api_key = "sk-fake-test-key-for-unit-testing-only" ## Commands ```bash -cd api && poetry run pytest -x --tb=short -cd api && poetry run pytest -k "test_provider" -cd api && poetry run pytest api/src/backend/api/tests/test_rbac.py +cd api && uv run pytest -x --tb=short +cd api && uv run pytest -k "test_provider" +cd api && uv run pytest api/src/backend/api/tests/test_rbac.py ``` --- diff --git a/skills/prowler-test-api/references/test-api-docs.md b/skills/prowler-test-api/references/test-api-docs.md index 0e7d02821a..0150fbfe87 100644 --- a/skills/prowler-test-api/references/test-api-docs.md +++ b/skills/prowler-test-api/references/test-api-docs.md @@ -14,7 +14,7 @@ ## Fixture Dependency Graph -``` +```text create_test_user (session) │ └─► tenants_fixture (function) @@ -171,28 +171,28 @@ from conftest import ( ```bash # Full test suite -cd api && poetry run pytest +cd api && uv run pytest # Fast fail on first error -cd api && poetry run pytest -x +cd api && uv run pytest -x # Short traceback -cd api && poetry run pytest --tb=short +cd api && uv run pytest --tb=short # Specific file -cd api && poetry run pytest api/src/backend/api/tests/test_views.py +cd api && uv run pytest api/src/backend/api/tests/test_views.py # Pattern match -cd api && poetry run pytest -k "Provider" +cd api && uv run pytest -k "Provider" # Verbose with print output -cd api && poetry run pytest -v -s +cd api && uv run pytest -v -s # With coverage -cd api && poetry run pytest --cov=api --cov-report=html +cd api && uv run pytest --cov=api --cov-report=html # Parallel execution -cd api && poetry run pytest -n auto +cd api && uv run pytest -n auto ``` --- diff --git a/skills/prowler-test-sdk/SKILL.md b/skills/prowler-test-sdk/SKILL.md index b165381540..ccf9ecdf03 100644 --- a/skills/prowler-test-sdk/SKILL.md +++ b/skills/prowler-test-sdk/SKILL.md @@ -265,7 +265,7 @@ from tests.providers.kubernetes.kubernetes_fixtures import set_mocked_kubernetes ## Test File Structure -``` +```text tests/providers/{provider}/services/{service}/ ├── {service}_service_test.py # Service tests └── {check_name}/ @@ -309,16 +309,16 @@ assert result[0].project_id == GCP_PROJECT_ID # GCP ```bash # All SDK tests -poetry run pytest -n auto -vvv tests/ +uv run pytest -n auto -vvv tests/ # Specific provider -poetry run pytest tests/providers/{provider}/ -v +uv run pytest tests/providers/{provider}/ -v # Specific check -poetry run pytest tests/providers/{provider}/services/{service}/{check_name}/ -v +uv run pytest tests/providers/{provider}/services/{service}/{check_name}/ -v # Stop on first failure -poetry run pytest -x tests/ +uv run pytest -x tests/ ``` ## Resources diff --git a/skills/prowler-test-ui/SKILL.md b/skills/prowler-test-ui/SKILL.md index 558525932d..2cd583eb32 100644 --- a/skills/prowler-test-ui/SKILL.md +++ b/skills/prowler-test-ui/SKILL.md @@ -19,7 +19,7 @@ allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task ## Prowler UI Test Structure -``` +```text ui/tests/ ├── base-page.ts # Prowler-specific base page ├── helpers.ts # Prowler test utilities @@ -35,13 +35,13 @@ ui/tests/ **⚠️ ALWAYS verify BEFORE completing any E2E task:** -### When CREATING new tests: +### When CREATING new tests - [ ] `{page-name}-page.ts` - Page Object created/updated - [ ] `{page-name}.spec.ts` - Tests added with correct tags (@TEST-ID) - [ ] `{page-name}.md` - Documentation created with ALL test cases - [ ] Test IDs in `.md` match tags in `.spec.ts` -### When MODIFYING existing tests: +### When MODIFYING existing tests - [ ] `{page-name}.md` MUST be updated if: - Test cases were added/removed - Test flow changed (steps) @@ -49,7 +49,7 @@ ui/tests/ - Tags or priorities changed - [ ] Test IDs synchronized between `.md` and `.spec.ts` -### Quick validation: +### Quick validation ```bash # Verify .md exists for each test folder ls ui/tests/{feature}/{feature}.md @@ -59,7 +59,8 @@ grep -o "@[A-Z]*-E2E-[0-9]*" ui/tests/{feature}/{feature}.spec.ts | sort -u grep -o "\`[A-Z]*-E2E-[0-9]*\`" ui/tests/{feature}/{feature}.md | sort -u ``` -**❌ An E2E change is NOT considered complete without updating the corresponding .md file** +> [!IMPORTANT] +> ❌ An E2E change is NOT considered complete without updating the corresponding `.md` file. --- diff --git a/skills/prowler-ui/SKILL.md b/skills/prowler-ui/SKILL.md index 3e6407889e..f0f2bae08e 100644 --- a/skills/prowler-ui/SKILL.md +++ b/skills/prowler-ui/SKILL.md @@ -1,7 +1,7 @@ --- name: prowler-ui description: > - Prowler UI-specific patterns. For generic patterns, see: typescript, react-19, nextjs-15, tailwind-4. + Prowler UI-specific patterns. For generic patterns, see: typescript, react-19, nextjs-16, tailwind-4. Trigger: When working inside ui/ on Prowler-specific conventions (shadcn vs HeroUI legacy, folder placement, actions/adapters, shared types/hooks/lib). license: Apache-2.0 metadata: @@ -18,7 +18,7 @@ allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task - `typescript` - Const types, flat interfaces - `react-19` - No useMemo/useCallback, compiler -- `nextjs-15` - App Router, Server Actions +- `nextjs-16` - App Router, Server Actions - `tailwind-4` - cn() utility, styling rules - `zod-4` - Schema validation - `zustand-5` - State management @@ -27,8 +27,8 @@ allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task ## Tech Stack (Versions) -``` -Next.js 15.5.9 | React 19.2.2 | Tailwind 4.1.13 | shadcn/ui +```text +Next.js 16.2.3 | React 19.2.5 | Tailwind 4.1.18 | shadcn/ui Zod 4.1.11 | React Hook Form 7.62.0 | Zustand 5.0.8 NextAuth 5.0.0-beta.30 | Recharts 2.15.4 HeroUI 2.8.4 (LEGACY - do not add new components) @@ -43,7 +43,7 @@ HeroUI 2.8.4 (LEGACY - do not add new components) ### Component Placement -``` +```text New feature UI? → shadcn/ui + Tailwind Existing HeroUI feature? → Keep HeroUI (don't mix) Used 1 feature? → features/{feature}/components/ @@ -54,7 +54,7 @@ Server component? → No directive needed ### Code Location -``` +```text Server action → actions/{feature}/{feature}.ts Data transform → actions/{feature}/{feature}.adapter.ts Types (shared 2+) → types/{domain}.ts @@ -69,7 +69,7 @@ HeroUI components → components/ui/ (LEGACY) ### Styling Decision -``` +```text Tailwind class exists? → className Dynamic value? → style prop Conditional styles? → cn() @@ -85,7 +85,7 @@ Recharts/library? → CHART_COLORS constant + var() ## Project Structure -``` +```text ui/ ├── app/ │ ├── (auth)/ # Auth pages (login, signup) diff --git a/skills/prowler/SKILL.md b/skills/prowler/SKILL.md index 667ad0cc4b..bb49447249 100644 --- a/skills/prowler/SKILL.md +++ b/skills/prowler/SKILL.md @@ -16,22 +16,22 @@ allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task | Component | Stack | Location | |-----------|-------|----------| -| SDK | Python 3.10+, Poetry | `prowler/` | +| SDK | Python 3.10+, uv | `prowler/` | | API | Django 5.1, DRF, Celery | `api/` | -| UI | Next.js 15, React 19, Tailwind 4 | `ui/` | +| UI | Next.js 16, React 19, Tailwind 4 | `ui/` | | MCP | FastMCP 2.13.1 | `mcp_server/` | ## Quick Commands ```bash # SDK -poetry install --with dev -poetry run python prowler-cli.py aws --check check_name -poetry run pytest tests/ +uv sync +uv run python prowler-cli.py aws --check check_name +uv run pytest tests/ # API -cd api && poetry run python src/backend/manage.py runserver -cd api && poetry run pytest +cd api && uv run python src/backend/manage.py runserver +cd api && uv run pytest # UI cd ui && pnpm run dev diff --git a/skills/react-19/SKILL.md b/skills/react-19/SKILL.md index 519ae9f15e..645292aa83 100644 --- a/skills/react-19/SKILL.md +++ b/skills/react-19/SKILL.md @@ -2,7 +2,7 @@ name: react-19 description: > React 19 patterns with React Compiler. - Trigger: When writing React 19 components/hooks in .tsx (React Compiler rules, hook patterns, refs as props). If using Next.js App Router/Server Actions, also use nextjs-15. + Trigger: When writing React 19 components/hooks in .tsx (React Compiler rules, hook patterns, refs as props). If using Next.js App Router/Server Actions, also use nextjs-16. license: Apache-2.0 metadata: author: prowler-cloud diff --git a/skills/skill-creator/SKILL.md b/skills/skill-creator/SKILL.md index 11787abe2b..41b8e7e67d 100644 --- a/skills/skill-creator/SKILL.md +++ b/skills/skill-creator/SKILL.md @@ -29,7 +29,7 @@ Create a skill when: ## Skill Structure -``` +```text skills/{skill-name}/ ├── SKILL.md # Required - main skill file ├── assets/ # Optional - templates, schemas, examples @@ -43,7 +43,7 @@ skills/{skill-name}/ ## SKILL.md Template -```markdown +````markdown --- name: {skill-name} description: > @@ -77,7 +77,7 @@ metadata: - **Templates**: See [assets/](assets/) for {description} - **Documentation**: See [references/](references/) for local docs -``` +```` --- @@ -94,7 +94,7 @@ metadata: ## Decision: assets/ vs references/ -``` +```text Need code templates? → assets/ Need JSON schemas? → assets/ Need example configs? → assets/ @@ -108,7 +108,7 @@ Link to external guides? → references/ (with local path) ## Decision: Prowler-Specific vs Generic -``` +```text Patterns apply to ANY project? → Generic skill (e.g., pytest, typescript) Patterns are Prowler-specific? → prowler-{name} skill Generic skill needs Prowler info? → Add references/ pointing to Prowler docs diff --git a/skills/skill-creator/assets/SKILL-TEMPLATE.md b/skills/skill-creator/assets/SKILL-TEMPLATE.md index 7639240245..581cafd9c4 100644 --- a/skills/skill-creator/assets/SKILL-TEMPLATE.md +++ b/skills/skill-creator/assets/SKILL-TEMPLATE.md @@ -38,7 +38,7 @@ Use this skill when: ## Decision Tree -``` +```text {Question 1}? → {Action A} {Question 2}? → {Action B} Otherwise → {Default action} diff --git a/skills/tailwind-4/SKILL.md b/skills/tailwind-4/SKILL.md index 51f57576af..e67d2ea944 100644 --- a/skills/tailwind-4/SKILL.md +++ b/skills/tailwind-4/SKILL.md @@ -14,7 +14,7 @@ allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task ## Styling Decision Tree -``` +```text Tailwind class exists? → className="..." Dynamic value? → style={{ width: `${x}%` }} Conditional styles? → cn("base", condition && "variant") diff --git a/skills/tdd/SKILL.md b/skills/tdd/SKILL.md index 60887c342c..d62d359053 100644 --- a/skills/tdd/SKILL.md +++ b/skills/tdd/SKILL.md @@ -20,7 +20,7 @@ allowed-tools: Read, Edit, Write, Glob, Grep, Bash, Task ## TDD Cycle (MANDATORY) -``` +```text +-----------------------------------------+ | RED -> GREEN -> REFACTOR | | ^ | | @@ -28,7 +28,7 @@ allowed-tools: Read, Edit, Write, Glob, Grep, Bash, Task +-----------------------------------------+ ``` -**The question is NOT "should I write tests?" but "what tests do I need?"** +The question is NOT "should I write tests?" but "what tests do I need?" --- @@ -75,7 +75,7 @@ pnpm test:coverage -- components/feature/ fd "*_test.py" tests/providers/aws/services/ec2/ # 2. Run specific test -poetry run pytest tests/providers/aws/services/ec2/ec2_ami_public/ -v +uv run pytest tests/providers/aws/services/ec2/ec2_ami_public/ -v # 3. Read existing tests ``` @@ -87,14 +87,14 @@ poetry run pytest tests/providers/aws/services/ec2/ec2_ami_public/ -v fd "test_*.py" api/src/backend/api/tests/ # 2. Run specific test -poetry run pytest api/src/backend/api/tests/test_models.py -v +uv run pytest api/src/backend/api/tests/test_models.py -v # 3. Read existing tests ``` ### Decision Tree (All Stacks) -``` +```text +------------------------------------------+ | Does test file exist for this code? | +----------+-----------------------+-------+ @@ -122,7 +122,7 @@ poetry run pytest api/src/backend/api/tests/test_models.py -v ### For NEW Functionality -**UI (Vitest)** +#### UI (Vitest) ```typescript describe("PriceCalculator", () => { @@ -139,7 +139,7 @@ describe("PriceCalculator", () => { }); ``` -**SDK (pytest)** +#### SDK (pytest) ```python class Test_ec2_ami_public: @@ -159,7 +159,7 @@ class Test_ec2_ami_public: assert len(result) == 0 ``` -**API (pytest-django)** +#### API (pytest-django) ```python @pytest.mark.django_db @@ -192,18 +192,18 @@ Write a test that **reproduces the bug** first: **API:** `assert response.status_code == 403 # Currently returns 200` -**Run -> Should FAIL (reproducing the bug)** +Run -> Should FAIL (reproducing the bug). ### For REFACTORING Capture ALL current behavior BEFORE refactoring: -``` +```text # Any stack: run ALL existing tests, they should PASS # This is your safety net - if any fail after refactoring, you broke something ``` -**Run -> All should PASS (baseline)** +Run -> All should PASS (baseline). --- @@ -288,13 +288,13 @@ Tests GREEN -> Improve code quality WITHOUT changing behavior. - Add types/validation - Reduce duplication -**Run tests after EACH change -> Must stay GREEN** +Run tests after EACH change -> Must stay GREEN. --- ## Quick Reference -``` +```text +------------------------------------------------+ | TDD WORKFLOW | +------------------------------------------------+ @@ -320,7 +320,7 @@ Tests GREEN -> Improve code quality WITHOUT changing behavior. ## Anti-Patterns (NEVER DO) -``` +```python # ANY language: # 1. Code first, tests after @@ -356,16 +356,16 @@ pnpm test ComponentName # Filter by name ### SDK (`prowler/`) ```bash -poetry run pytest tests/path/ -v # Run specific tests -poetry run pytest tests/path/ -v -k "test_name" # Filter by name -poetry run pytest -n auto tests/ # Parallel run -poetry run pytest --cov=./prowler tests/ # Coverage +uv run pytest tests/path/ -v # Run specific tests +uv run pytest tests/path/ -v -k "test_name" # Filter by name +uv run pytest -n auto tests/ # Parallel run +uv run pytest --cov=./prowler tests/ # Coverage ``` ### API (`api/`) ```bash -poetry run pytest -x --tb=short # Run all (stop on first fail) -poetry run pytest api/src/backend/api/tests/test_file.py # Specific file -poetry run pytest -k "test_name" -v # Filter by name +uv run pytest -x --tb=short # Run all (stop on first fail) +uv run pytest api/src/backend/api/tests/test_file.py # Specific file +uv run pytest -k "test_name" -v # Filter by name ``` diff --git a/skills/vitest/SKILL.md b/skills/vitest/SKILL.md index 11190cbaef..29ad237a3e 100644 --- a/skills/vitest/SKILL.md +++ b/skills/vitest/SKILL.md @@ -181,7 +181,7 @@ expect(screen.getByRole("button")).toBeDisabled(); ## File Organization -``` +```text components/ ├── Button/ │ ├── Button.tsx diff --git a/tests/config/config_test.py b/tests/config/config_test.py index 0fe00d792f..8ab79a44fa 100644 --- a/tests/config/config_test.py +++ b/tests/config/config_test.py @@ -75,6 +75,7 @@ config_aws = { "mute_non_default_regions": False, "max_unused_access_keys_days": 45, "max_console_access_days": 45, + "max_unused_sagemaker_access_days": 90, "shodan_api_key": None, "max_security_group_rules": 50, "max_ec2_instance_age_in_days": 180, diff --git a/tests/config/fixtures/config.yaml b/tests/config/fixtures/config.yaml index 1b63e2387f..39cba5f27d 100644 --- a/tests/config/fixtures/config.yaml +++ b/tests/config/fixtures/config.yaml @@ -20,6 +20,8 @@ aws: max_unused_access_keys_days: 45 # aws.iam_user_console_access_unused --> CIS recommends 45 days max_console_access_days: 45 + # aws.iam_user_access_not_stale_to_sagemaker --> default 90 days + max_unused_sagemaker_access_days: 90 # AWS EC2 Configuration # aws.ec2_elastic_ip_shodan @@ -486,3 +488,11 @@ m365: # Exchange Mailbox Settings # m365.exchange_mailbox_properties_auditing_enabled audit_log_age: 90 # maximum number of days to keep audit logs + +okta: + # Okta Sign-On Policies + # okta.signon_global_session_idle_timeout_15min + okta_max_session_idle_minutes: 15 + # Okta Applications + # okta.application_admin_console_session_idle_timeout_15min + okta_admin_console_idle_timeout_max_minutes: 15 diff --git a/tests/lib/check/check_test.py b/tests/lib/check/check_test.py index cda33e8fc6..b9492d6754 100644 --- a/tests/lib/check/check_test.py +++ b/tests/lib/check/check_test.py @@ -1149,6 +1149,40 @@ class TestCheck: ) assert findings[0].muted is True + def test_execute_azure_mutelist_passes_subscription_id_and_name(self): + """Test that execute() passes Azure subscription ID and display name.""" + subscription_id = "12345678-1234-1234-1234-123456789012" + subscription_name = "subscription_1" + + finding = Mock() + finding.status = "PASS" + finding.muted = False + finding.subscription = subscription_id + + check = Mock() + check.CheckID = "azure_test_check" + check.execute = Mock(return_value=[finding]) + + provider = mock.MagicMock() + provider.type = "azure" + provider.identity.subscriptions = {subscription_id: subscription_name} + provider.mutelist.mutelist = {"Accounts": {subscription_name: {}}} + provider.mutelist.is_finding_muted = Mock(return_value=True) + + findings = execute( + check=check, + global_provider=provider, + custom_checks_metadata=None, + output_options=None, + ) + + provider.mutelist.is_finding_muted.assert_called_once_with( + subscription_id=subscription_id, + subscription_name=subscription_name, + finding=finding, + ) + assert findings[0].muted is True + def test_execute_check_exception_only_logs(self, caplog): caplog.set_level(ERROR) diff --git a/tests/lib/cli/parser_test.py b/tests/lib/cli/parser_test.py index d42b1482b3..d667be9e66 100644 --- a/tests/lib/cli/parser_test.py +++ b/tests/lib/cli/parser_test.py @@ -17,7 +17,7 @@ prowler_command = "prowler" # capsys # https://docs.pytest.org/en/7.1.x/how-to/capture-stdout-stderr.html -prowler_default_usage_error = "usage: prowler [-h] [--version] {aws,azure,gcp,kubernetes,m365,github,googleworkspace,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack,vercel,dashboard,iac,image,llm} ..." +prowler_default_usage_error = "usage: prowler [-h] [--version] {aws,azure,gcp,kubernetes,m365,github,googleworkspace,okta,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack,scaleway,vercel,dashboard,iac,image,llm} ..." def mock_get_available_providers(): diff --git a/tests/lib/outputs/compliance/asd_essential_eight/__init__.py b/tests/lib/outputs/compliance/asd_essential_eight/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/lib/outputs/compliance/essential_eight/essential_eight_aws_test.py b/tests/lib/outputs/compliance/asd_essential_eight/asd_essential_eight_aws_test.py similarity index 80% rename from tests/lib/outputs/compliance/essential_eight/essential_eight_aws_test.py rename to tests/lib/outputs/compliance/asd_essential_eight/asd_essential_eight_aws_test.py index 2abbdadb9c..90042c9d57 100644 --- a/tests/lib/outputs/compliance/essential_eight/essential_eight_aws_test.py +++ b/tests/lib/outputs/compliance/asd_essential_eight/asd_essential_eight_aws_test.py @@ -4,13 +4,13 @@ from unittest import mock from freezegun import freeze_time from mock import patch -from prowler.lib.outputs.compliance.essential_eight.essential_eight_aws import ( - EssentialEightAWS, +from prowler.lib.outputs.compliance.asd_essential_eight.asd_essential_eight_aws import ( + ASDEssentialEightAWS, ) -from prowler.lib.outputs.compliance.essential_eight.models import ( - EssentialEightAWSModel, +from prowler.lib.outputs.compliance.asd_essential_eight.models import ( + ASDEssentialEightAWSModel, ) -from tests.lib.outputs.compliance.fixtures import ESSENTIAL_EIGHT_AWS +from tests.lib.outputs.compliance.fixtures import ASD_ESSENTIAL_EIGHT_AWS from tests.lib.outputs.fixtures.fixtures import generate_finding_output from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_EU_WEST_1 @@ -18,26 +18,26 @@ from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_EU_WEST_1 # clause 8: removal of unsupported online services). The second Requirement is # E8-6.1 (Restrict Office macros, clause 1) which has no Checks and is therefore # emitted as a manual row. -COMPLIANCE_NAME = "Essential-Eight-Nov 2023" +COMPLIANCE_NAME = "ASD-Essential-Eight-Nov 2023" -class TestEssentialEightAWS: +class TestASDEssentialEightAWS: def test_output_transform(self): findings = [generate_finding_output(compliance={COMPLIANCE_NAME: "E8-1.8"})] - output = EssentialEightAWS(findings, ESSENTIAL_EIGHT_AWS) + output = ASDEssentialEightAWS(findings, ASD_ESSENTIAL_EIGHT_AWS) output_data = output.data[0] - assert isinstance(output_data, EssentialEightAWSModel) + assert isinstance(output_data, ASDEssentialEightAWSModel) assert output_data.Provider == "aws" - assert output_data.Framework == ESSENTIAL_EIGHT_AWS.Framework - assert output_data.Name == ESSENTIAL_EIGHT_AWS.Name - assert output_data.Description == ESSENTIAL_EIGHT_AWS.Description + assert output_data.Framework == ASD_ESSENTIAL_EIGHT_AWS.Framework + assert output_data.Name == ASD_ESSENTIAL_EIGHT_AWS.Name + assert output_data.Description == ASD_ESSENTIAL_EIGHT_AWS.Description assert output_data.AccountId == AWS_ACCOUNT_NUMBER assert output_data.Region == AWS_REGION_EU_WEST_1 assert output_data.Requirements_Id == "E8-1.8" assert ( output_data.Requirements_Description - == ESSENTIAL_EIGHT_AWS.Requirements[0].Description + == ASD_ESSENTIAL_EIGHT_AWS.Requirements[0].Description ) assert output_data.Requirements_Attributes_Section == "1 Patch applications" assert output_data.Requirements_Attributes_MaturityLevel == "ML1" @@ -49,7 +49,7 @@ class TestEssentialEightAWS: ) assert ( output_data.Requirements_Attributes_Description - == ESSENTIAL_EIGHT_AWS.Requirements[0].Attributes[0].Description + == ASD_ESSENTIAL_EIGHT_AWS.Requirements[0].Attributes[0].Description ) assert output_data.Status == "PASS" assert output_data.StatusExtended == "" @@ -60,7 +60,7 @@ class TestEssentialEightAWS: def test_manual_requirement(self): findings = [generate_finding_output(compliance={COMPLIANCE_NAME: "E8-1.8"})] - output = EssentialEightAWS(findings, ESSENTIAL_EIGHT_AWS) + output = ASDEssentialEightAWS(findings, ASD_ESSENTIAL_EIGHT_AWS) # E8-6.1 (macros) has no Checks -> emitted as a manual row, non-applicable manual_rows = [row for row in output.data if row.Status == "MANUAL"] @@ -90,13 +90,13 @@ class TestEssentialEightAWS: @freeze_time("2025-01-01 00:00:00") @mock.patch( - "prowler.lib.outputs.compliance.essential_eight.essential_eight_aws.timestamp", + "prowler.lib.outputs.compliance.asd_essential_eight.asd_essential_eight_aws.timestamp", "2025-01-01 00:00:00", ) def test_batch_write_data_to_file(self): mock_file = StringIO() findings = [generate_finding_output(compliance={COMPLIANCE_NAME: "E8-1.8"})] - output = EssentialEightAWS(findings, ESSENTIAL_EIGHT_AWS) + output = ASDEssentialEightAWS(findings, ASD_ESSENTIAL_EIGHT_AWS) output._file_descriptor = mock_file with patch.object(mock_file, "close", return_value=None): diff --git a/tests/lib/outputs/compliance/fixtures.py b/tests/lib/outputs/compliance/fixtures.py index 9aac01392f..f085460abd 100644 --- a/tests/lib/outputs/compliance/fixtures.py +++ b/tests/lib/outputs/compliance/fixtures.py @@ -1,4 +1,5 @@ from prowler.lib.check.compliance_models import ( + ASDEssentialEight_Requirement_Attribute, AWS_Well_Architected_Requirement_Attribute, CCC_Requirement_Attribute, CIS_Requirement_Attribute, @@ -7,7 +8,6 @@ from prowler.lib.check.compliance_models import ( ENS_Requirement_Attribute, ENS_Requirement_Attribute_Nivel, ENS_Requirement_Attribute_Tipos, - EssentialEight_Requirement_Attribute, Generic_Compliance_Requirement_Attribute, ISO27001_2013_Requirement_Attribute, KISA_ISMSP_Requirement_Attribute, @@ -128,7 +128,7 @@ CIS_2_0_GCP = Compliance( Description="This CIS Benchmark is the product of a community consensus process and consists of secure configuration guidelines developed for Google Cloud Computing Platform", Requirements=[ Compliance_Requirement( - Checks=["apikeys_key_exits"], + Checks=["apikeys_key_exits", "service_test_check_id"], Id="2.13", Description="Ensure That Microsoft Defender for Databases Is Set To 'On'", Attributes=[ @@ -177,7 +177,7 @@ CIS_1_8_KUBERNETES = Compliance( Description="This CIS Kubernetes Benchmark provides prescriptive guidance for establishing a secure configuration posture for Kubernetes v1.27.", Requirements=[ Compliance_Requirement( - Checks=["apiserver_always_pull_images_plugin"], + Checks=["apiserver_always_pull_images_plugin", "service_test_check_id"], Id="1.1.3", Description="Ensure that the controller manager pod specification file permissions are set to 600 or more restrictive", Attributes=[ @@ -259,6 +259,7 @@ CIS_4_0_M365 = Compliance( Compliance_Requirement( Checks=[ "mfa_delete_enabled", + "service_test_check_id", ], Id="2.1.3", Description="Ensure MFA Delete is enabled on S3 buckets", @@ -336,6 +337,7 @@ MITRE_ATTACK_AWS = Compliance( "inspector2_active_findings_exist", "awslambda_function_not_publicly_accessible", "ec2_instance_public_ip", + "service_test_check_id", ], ), Mitre_Requirement( @@ -412,6 +414,7 @@ MITRE_ATTACK_AZURE = Compliance( "defender_ensure_notify_emails_to_owners", "defender_ensure_system_updates_are_applied", "defender_ensure_wdatp_is_enabled", + "service_test_check_id", ], ), Mitre_Requirement( @@ -467,6 +470,7 @@ MITRE_ATTACK_GCP = Compliance( "compute_instance_public_ip", "compute_public_address_shodan", "kms_key_not_publicly_accessible", + "service_test_check_id", ], ), Mitre_Requirement( @@ -514,7 +518,7 @@ ENS_RD2022_AWS = Compliance( Dependencias=[], ) ], - Checks=["cloudtrail_log_file_validation_enabled"], + Checks=["cloudtrail_log_file_validation_enabled", "service_test_check_id"], ), Compliance_Requirement( Id="op.exp.8.aws.ct.4", @@ -562,7 +566,7 @@ ENS_RD2022_AZURE = Compliance( Dependencias=[], ) ], - Checks=["cloudtrail_log_file_validation_enabled"], + Checks=["cloudtrail_log_file_validation_enabled", "service_test_check_id"], ), Compliance_Requirement( Id="op.exp.8.azure.ct.4", @@ -609,7 +613,7 @@ ENS_RD2022_GCP = Compliance( Dependencias=[], ) ], - Checks=["cloudtrail_log_file_validation_enabled"], + Checks=["cloudtrail_log_file_validation_enabled", "service_test_check_id"], ), Compliance_Requirement( Id="op.exp.8.gcp.ct.4", @@ -666,7 +670,10 @@ AWS_WELL_ARCHITECTED = Compliance( ImplementationGuidanceUrl="https://docs.aws.amazon.com/wellarchitected/latest/security-pillar/sec_securely_operate_multi_accounts.html#implementation-guidance.", ) ], - Checks=["organizations_account_part_of_organizations"], + Checks=[ + "organizations_account_part_of_organizations", + "service_test_check_id", + ], ), Compliance_Requirement( Id="SEC01-BP02", @@ -709,7 +716,7 @@ ISO27001_2013_AWS = Compliance( Check_Summary="Setup Encryption at rest for RDS instances", ) ], - Checks=["rds_instance_storage_encrypted"], + Checks=["rds_instance_storage_encrypted", "service_test_check_id"], ), Compliance_Requirement( Id="A.10.2", @@ -759,6 +766,7 @@ NIST_800_53_REVISION_4_AWS = Compliance( "rds_instance_integration_cloudwatch_logs", "redshift_cluster_audit_logging", "securityhub_enabled", + "service_test_check_id", ], ), Compliance_Requirement( @@ -815,6 +823,7 @@ KISA_ISMSP_AWS = Compliance( Checks=[ "cloudwatch_log_metric_filter_authentication_failures", "cognito_user_pool_mfa_enabled", + "service_test_check_id", ], ), Compliance_Requirement( @@ -872,6 +881,7 @@ PROWLER_THREATSCORE_AWS = Compliance( ], Checks=[ "iam_root_mfa_enabled", + "service_test_check_id", ], ), Compliance_Requirement( @@ -916,6 +926,7 @@ PROWLER_THREATSCORE_AZURE = Compliance( ], Checks=[ "iam_root_mfa_enabled", + "service_test_check_id", ], ), Compliance_Requirement( @@ -960,6 +971,7 @@ PROWLER_THREATSCORE_GCP = Compliance( ], Checks=[ "iam_root_mfa_enabled", + "service_test_check_id", ], ), Compliance_Requirement( @@ -1004,6 +1016,7 @@ PROWLER_THREATSCORE_M365 = Compliance( ], Checks=[ "iam_root_mfa_enabled", + "service_test_check_id", ], ), Compliance_Requirement( @@ -1191,8 +1204,8 @@ CCC_GCP_FIXTURE = Compliance( ], ) -ESSENTIAL_EIGHT_AWS = Compliance( - Framework="Essential-Eight", +ASD_ESSENTIAL_EIGHT_AWS = Compliance( + Framework="ASD-Essential-Eight", Name="ASD Essential Eight Maturity Model - Maturity Level One (AWS)", Version="Nov 2023", Provider="AWS", @@ -1202,7 +1215,7 @@ ESSENTIAL_EIGHT_AWS = Compliance( Id="E8-1.8", Description="Online services that are no longer supported by vendors are removed.", Attributes=[ - EssentialEight_Requirement_Attribute( + ASDEssentialEight_Requirement_Attribute( Section="1 Patch applications", MaturityLevel="ML1", AssessmentStatus="Automated", @@ -1226,7 +1239,7 @@ ESSENTIAL_EIGHT_AWS = Compliance( Id="E8-6.1", Description="Microsoft Office macros are disabled for users that do not have a demonstrated business requirement.", Attributes=[ - EssentialEight_Requirement_Attribute( + ASDEssentialEight_Requirement_Attribute( Section="6 Restrict Microsoft Office macros", MaturityLevel="ML1", AssessmentStatus="Manual", diff --git a/tests/lib/outputs/compliance/generic/generic_aws_test.py b/tests/lib/outputs/compliance/generic/generic_aws_test.py index cda9a08bd3..335d1860ea 100644 --- a/tests/lib/outputs/compliance/generic/generic_aws_test.py +++ b/tests/lib/outputs/compliance/generic/generic_aws_test.py @@ -5,6 +5,11 @@ from unittest import mock from freezegun import freeze_time from mock import patch +from prowler.lib.check.compliance_models import ( + Compliance, + Compliance_Requirement, + Generic_Compliance_Requirement_Attribute, +) from prowler.lib.outputs.compliance.generic.generic import GenericCompliance from prowler.lib.outputs.compliance.generic.models import GenericComplianceModel from tests.lib.outputs.compliance.fixtures import NIST_800_53_REVISION_4_AWS @@ -129,3 +134,67 @@ class TestAWSGenericCompliance: expected_csv = f"PROVIDER;DESCRIPTION;ACCOUNTID;REGION;ASSESSMENTDATE;REQUIREMENTS_ID;REQUIREMENTS_DESCRIPTION;REQUIREMENTS_ATTRIBUTES_SECTION;REQUIREMENTS_ATTRIBUTES_SUBSECTION;REQUIREMENTS_ATTRIBUTES_SUBGROUP;REQUIREMENTS_ATTRIBUTES_SERVICE;REQUIREMENTS_ATTRIBUTES_TYPE;STATUS;STATUSEXTENDED;RESOURCEID;CHECKID;MUTED;RESOURCENAME;FRAMEWORK;NAME;REQUIREMENTS_ATTRIBUTES_COMMENT\r\naws;NIST 800-53 is a regulatory standard that defines the minimum baseline of security controls for all U.S. federal information systems except those related to national security. The controls defined in this standard are customizable and address a diverse set of security and privacy requirements.;123456789012;eu-west-1;{datetime.now()};ac_2_4;Account Management;Access Control (AC);Account Management (AC-2);;aws;;PASS;;;service_test_check_id;False;;NIST-800-53-Revision-4;National Institute of Standards and Technology (NIST) 800-53 Revision 4;\r\naws;NIST 800-53 is a regulatory standard that defines the minimum baseline of security controls for all U.S. federal information systems except those related to national security. The controls defined in this standard are customizable and address a diverse set of security and privacy requirements.;;;{datetime.now()};ac_2_5;Account Management;Access Control (AC);Account Management (AC-2);;aws;;MANUAL;Manual check;manual_check;manual;False;Manual check;NIST-800-53-Revision-4;National Institute of Standards and Technology (NIST) 800-53 Revision 4;\r\n" assert content == expected_csv + + def test_csv_row_count_matches_framework_checks_not_stored_compliance(self): + """Regression test for PROWLER-1763. + + Ensures CSV emission is driven by the framework JSON's Requirements[].Checks + (the same source the UI uses) and not by the per-finding `finding.compliance` + snapshot stored at scan time. If `finding.check_id` is in a requirement's + Checks list, the row must be emitted regardless of what was stored in + `finding.compliance`. Conversely, a stale `finding.compliance` entry pointing + to a requirement whose Checks list no longer contains the finding's check_id + must not produce a row. + """ + framework_name = "Test-Framework-1763" + compliance = Compliance( + Framework=framework_name, + Name=framework_name, + Provider="AWS", + Version="", + Description="Regression fixture for PROWLER-1763", + Requirements=[ + Compliance_Requirement( + Id="req_in_framework", + Description="Requirement currently in framework", + Attributes=[ + Generic_Compliance_Requirement_Attribute( + Section="Section A", Service="aws" + ) + ], + Checks=["service_check_in_framework"], + ), + Compliance_Requirement( + Id="req_no_longer_in_framework", + Description="Requirement whose Checks list no longer includes the finding's check_id", + Attributes=[ + Generic_Compliance_Requirement_Attribute( + Section="Section B", Service="aws" + ) + ], + Checks=["service_different_check"], + ), + ], + ) + + # Snapshot drift case: finding.compliance maps to a requirement whose + # current Checks list no longer includes the finding's check_id, AND + # the finding belongs to a requirement that is NOT in the snapshot. + findings = [ + generate_finding_output( + check_id="service_check_in_framework", + compliance={framework_name: ["req_no_longer_in_framework"]}, + ) + ] + + output = GenericCompliance(findings, compliance) + rows = [ + row + for row in output.data + if row.Status != "MANUAL" and row.ResourceName != "Manual check" + ] + assert ( + len(rows) == 1 + ), f"Expected 1 row driven by framework JSON, got {len(rows)}" + assert rows[0].Requirements_Id == "req_in_framework" + assert rows[0].CheckId == "service_check_in_framework" diff --git a/tests/lib/outputs/finding_test.py b/tests/lib/outputs/finding_test.py index 2a57cc40bb..fb75a1f705 100644 --- a/tests/lib/outputs/finding_test.py +++ b/tests/lib/outputs/finding_test.py @@ -331,8 +331,8 @@ class TestFinding: assert finding_output.auth_method == "mock_identity_type: mock_identity_id" assert finding_output.account_organization_uid == "mock_tenant_id_1" assert finding_output.account_organization_name == "mock_tenant_domain" - assert finding_output.account_uid == "mock_subscription_name" - assert finding_output.account_name == "mock_subscription_id" + assert finding_output.account_uid == "mock_subscription_id" + assert finding_output.account_name == "mock_subscription_name" assert finding_output.resource_name == "test_resource_name" assert finding_output.resource_uid == "test_resource_id" assert finding_output.region == "us-west-1" diff --git a/tests/lib/outputs/summary_table_test.py b/tests/lib/outputs/summary_table_test.py new file mode 100644 index 0000000000..c1e5322c33 --- /dev/null +++ b/tests/lib/outputs/summary_table_test.py @@ -0,0 +1,42 @@ +from types import SimpleNamespace + +from prowler.lib.outputs.summary_table import display_summary_table + + +class TestDisplaySummaryTable: + def test_azure_summary_shows_display_name_and_subscription_id(self, capsys): + provider = SimpleNamespace( + type="azure", + identity=SimpleNamespace( + tenant_domain="tenant.example.com", + tenant_ids=["tenant-id"], + subscriptions={ + "subscription-id-1": "Duplicate Subscription", + "subscription-id-2": "Duplicate Subscription", + }, + ), + ) + output_options = SimpleNamespace( + output_directory="out", + output_filename="report", + output_modes=[], + ) + findings = [ + SimpleNamespace( + status="PASS", + muted=False, + check_metadata=SimpleNamespace( + ServiceName="network", + Provider="azure", + Severity="low", + ), + ) + ] + + display_summary_table(findings, provider, output_options) + + captured = capsys.readouterr() + + assert "Subscriptions scanned:" in captured.out + assert "Duplicate Subscription (subscription-id-1)" in captured.out + assert "Duplicate Subscription (subscription-id-2)" in captured.out diff --git a/tests/lib/utils/test_vulnerability_references.py b/tests/lib/utils/test_vulnerability_references.py new file mode 100644 index 0000000000..8610980b51 --- /dev/null +++ b/tests/lib/utils/test_vulnerability_references.py @@ -0,0 +1,91 @@ +from prowler.lib.utils.vulnerability_references import ( + build_finding_reference_url, + resolve_vulnerability_reference_urls, +) + + +class TestBuildFindingReferenceUrl: + def test_cve_id_returns_cve_org_url(self): + assert ( + build_finding_reference_url("CVE-2023-1234") + == "https://www.cve.org/CVERecord?id=CVE-2023-1234" + ) + + def test_lowercase_cve_id_is_normalized(self): + assert ( + build_finding_reference_url("cve-2024-9999") + == "https://www.cve.org/CVERecord?id=CVE-2024-9999" + ) + + def test_ghsa_id_returns_github_advisory_url(self): + assert ( + build_finding_reference_url("GHSA-abcd-1234-efgh") + == "https://github.com/advisories/GHSA-ABCD-1234-EFGH" + ) + + def test_avd_prefixed_id_strips_prefix_for_hub(self): + assert ( + build_finding_reference_url("AVD-AWS-0001") + == "https://hub.prowler.com/check/AWS-0001" + ) + + def test_clean_trivy_id_uses_hub_directly(self): + assert ( + build_finding_reference_url("AWS-0104") + == "https://hub.prowler.com/check/AWS-0104" + ) + + def test_kubernetes_id_uses_hub(self): + assert ( + build_finding_reference_url("AVD-K8S-0001") + == "https://hub.prowler.com/check/K8S-0001" + ) + + def test_dockerfile_id_uses_hub(self): + assert ( + build_finding_reference_url("AVD-DOCKER-0001") + == "https://hub.prowler.com/check/DOCKER-0001" + ) + + def test_whitespace_is_trimmed(self): + assert ( + build_finding_reference_url(" AZU-0013 ") + == "https://hub.prowler.com/check/AZU-0013" + ) + + +class TestResolveVulnerabilityReferenceUrls: + def test_cve_with_cve_org_reference_uses_it(self): + recommendation_url, additional_urls = resolve_vulnerability_reference_urls( + vulnerability_id="CVE-2023-1234", + references=[ + "https://avd.aquasec.com/nvd/cve-2023-1234", + "https://www.cve.org/CVERecord?id=CVE-2023-1234", + "https://nvd.nist.gov/vuln/detail/CVE-2023-1234", + ], + primary_url="https://avd.aquasec.com/nvd/cve-2023-1234", + ) + + assert recommendation_url == "https://www.cve.org/CVERecord?id=CVE-2023-1234" + assert additional_urls == ["https://www.cve.org/CVERecord?id=CVE-2023-1234"] + + def test_cve_without_cve_org_reference_builds_url(self): + recommendation_url, additional_urls = resolve_vulnerability_reference_urls( + vulnerability_id="CVE-2023-5678", + references=["https://nvd.nist.gov/vuln/detail/CVE-2023-5678"], + ) + + assert recommendation_url == "https://www.cve.org/CVERecord?id=CVE-2023-5678" + assert additional_urls == ["https://www.cve.org/CVERecord?id=CVE-2023-5678"] + + def test_non_cve_id_returns_filtered_references(self): + recommendation_url, additional_urls = resolve_vulnerability_reference_urls( + vulnerability_id="GHSA-abcd-1234-efgh", + references=[ + "https://avd.aquasec.com/nvd/ghsa-abcd-1234-efgh", + "https://github.com/advisories/GHSA-abcd-1234-efgh", + ], + ) + + assert recommendation_url == "" + assert additional_urls == ["https://github.com/advisories/GHSA-abcd-1234-efgh"] diff --git a/tests/providers/aws/services/bedrock/bedrock_prompt_encrypted_with_cmk/bedrock_prompt_encrypted_with_cmk_test.py b/tests/providers/aws/services/bedrock/bedrock_prompt_encrypted_with_cmk/bedrock_prompt_encrypted_with_cmk_test.py new file mode 100644 index 0000000000..6a009983a0 --- /dev/null +++ b/tests/providers/aws/services/bedrock/bedrock_prompt_encrypted_with_cmk/bedrock_prompt_encrypted_with_cmk_test.py @@ -0,0 +1,174 @@ +from unittest import mock + +import botocore + +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_EU_WEST_1, + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) + +make_api_call = botocore.client.BaseClient._make_api_call + +PROMPT_ARN = ( + f"arn:aws:bedrock:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:prompt/test-prompt-id" +) +PROMPT_ID = "test-prompt-id" +PROMPT_NAME = "test-prompt" +KMS_KEY_ARN = ( + f"arn:aws:kms:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:key/" + "12345678-1234-1234-1234-123456789012" +) + + +def mock_make_api_call_with_cmk(self, operation_name, kwarg): + """Mock API call returning a prompt encrypted with a customer-managed KMS key.""" + if operation_name == "ListPrompts": + return { + "promptSummaries": [ + { + "id": PROMPT_ID, + "name": PROMPT_NAME, + "arn": PROMPT_ARN, + } + ] + } + elif operation_name == "GetPrompt": + return { + "id": PROMPT_ID, + "name": PROMPT_NAME, + "arn": PROMPT_ARN, + "customerEncryptionKeyArn": KMS_KEY_ARN, + } + return make_api_call(self, operation_name, kwarg) + + +def mock_make_api_call_without_cmk(self, operation_name, kwarg): + """Mock API call returning a prompt without a customer-managed KMS key.""" + if operation_name == "ListPrompts": + return { + "promptSummaries": [ + { + "id": PROMPT_ID, + "name": PROMPT_NAME, + "arn": PROMPT_ARN, + } + ] + } + elif operation_name == "GetPrompt": + return { + "id": PROMPT_ID, + "name": PROMPT_NAME, + "arn": PROMPT_ARN, + } + return make_api_call(self, operation_name, kwarg) + + +class Test_bedrock_prompt_encrypted_with_cmk: + """Test suite for the bedrock_prompt_encrypted_with_cmk check.""" + + @mock.patch( + "botocore.client.BaseClient._make_api_call", + new=lambda self, op, kwarg: make_api_call(self, op, kwarg), + ) + def test_no_prompts(self): + """Test when no prompts exist.""" + from prowler.providers.aws.services.bedrock.bedrock_service import BedrockAgent + + aws_provider = set_mocked_aws_provider( + [AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1] + ) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.bedrock.bedrock_prompt_encrypted_with_cmk.bedrock_prompt_encrypted_with_cmk.bedrock_agent_client", + new=BedrockAgent(aws_provider), + ), + ): + from prowler.providers.aws.services.bedrock.bedrock_prompt_encrypted_with_cmk.bedrock_prompt_encrypted_with_cmk import ( + bedrock_prompt_encrypted_with_cmk, + ) + + check = bedrock_prompt_encrypted_with_cmk() + result = check.execute() + + assert len(result) == 0 + + @mock.patch( + "botocore.client.BaseClient._make_api_call", + new=mock_make_api_call_with_cmk, + ) + def test_prompt_encrypted_with_cmk(self): + """Test when a prompt is encrypted with a customer-managed KMS key.""" + from prowler.providers.aws.services.bedrock.bedrock_service import BedrockAgent + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.bedrock.bedrock_prompt_encrypted_with_cmk.bedrock_prompt_encrypted_with_cmk.bedrock_agent_client", + new=BedrockAgent(aws_provider), + ), + ): + from prowler.providers.aws.services.bedrock.bedrock_prompt_encrypted_with_cmk.bedrock_prompt_encrypted_with_cmk import ( + bedrock_prompt_encrypted_with_cmk, + ) + + check = bedrock_prompt_encrypted_with_cmk() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Bedrock Prompt {PROMPT_NAME} is encrypted with a customer-managed KMS key." + ) + assert result[0].resource_id == PROMPT_ID + assert result[0].resource_arn == PROMPT_ARN + assert result[0].region == AWS_REGION_US_EAST_1 + + @mock.patch( + "botocore.client.BaseClient._make_api_call", + new=mock_make_api_call_without_cmk, + ) + def test_prompt_not_encrypted_with_cmk(self): + """Test when a prompt is not encrypted with a customer-managed KMS key.""" + from prowler.providers.aws.services.bedrock.bedrock_service import BedrockAgent + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.bedrock.bedrock_prompt_encrypted_with_cmk.bedrock_prompt_encrypted_with_cmk.bedrock_agent_client", + new=BedrockAgent(aws_provider), + ), + ): + from prowler.providers.aws.services.bedrock.bedrock_prompt_encrypted_with_cmk.bedrock_prompt_encrypted_with_cmk import ( + bedrock_prompt_encrypted_with_cmk, + ) + + check = bedrock_prompt_encrypted_with_cmk() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Bedrock Prompt {PROMPT_NAME} is not encrypted with a customer-managed KMS key." + ) + assert result[0].resource_id == PROMPT_ID + assert result[0].resource_arn == PROMPT_ARN + assert result[0].region == AWS_REGION_US_EAST_1 diff --git a/tests/providers/aws/services/bedrock/bedrock_service_test.py b/tests/providers/aws/services/bedrock/bedrock_service_test.py index 5f7ddf0cf2..f8a1fc8996 100644 --- a/tests/providers/aws/services/bedrock/bedrock_service_test.py +++ b/tests/providers/aws/services/bedrock/bedrock_service_test.py @@ -406,12 +406,14 @@ class TestBedrockPromptPagination: regional_client.get_paginator.assert_called_once_with("list_prompts") paginator.paginate.assert_called_once() - def test_list_prompts_ignores_audit_resources_filter(self): - """Prompt collection is region-scoped and must ignore audit_resources.""" + def test_list_prompts_filters_audit_resources(self): + """Prompt collection must honor audit_resources when resource ARNs are scoped.""" audit_info = MagicMock() audit_info.audited_partition = "aws" audit_info.audited_account = "123456789012" - audit_info.audit_resources = ["arn:aws:s3:::unrelated-resource"] + audit_info.audit_resources = [ + "arn:aws:bedrock:us-east-1:123456789012:prompt/prompt-1" + ] regional_client = MagicMock() regional_client.region = "us-east-1" @@ -424,7 +426,12 @@ class TestBedrockPromptPagination: "id": "prompt-1", "name": "prompt-name-1", "arn": "arn:aws:bedrock:us-east-1:123456789012:prompt/prompt-1", - } + }, + { + "id": "prompt-2", + "name": "prompt-name-2", + "arn": "arn:aws:bedrock:us-east-1:123456789012:prompt/prompt-2", + }, ] } ] @@ -438,6 +445,14 @@ class TestBedrockPromptPagination: bedrock_agent_service._list_prompts(regional_client) assert len(bedrock_agent_service.prompts) == 1 + assert ( + "arn:aws:bedrock:us-east-1:123456789012:prompt/prompt-1" + in bedrock_agent_service.prompts + ) + assert ( + "arn:aws:bedrock:us-east-1:123456789012:prompt/prompt-2" + not in bedrock_agent_service.prompts + ) assert "us-east-1" in bedrock_agent_service.prompt_scanned_regions def test_list_prompts_error_does_not_mark_region_scanned(self): diff --git a/tests/providers/aws/services/cloudtrail/cloudtrail_bedrock_logging_enabled/cloudtrail_bedrock_logging_enabled_test.py b/tests/providers/aws/services/cloudtrail/cloudtrail_bedrock_logging_enabled/cloudtrail_bedrock_logging_enabled_test.py new file mode 100644 index 0000000000..7b0cfa6a07 --- /dev/null +++ b/tests/providers/aws/services/cloudtrail/cloudtrail_bedrock_logging_enabled/cloudtrail_bedrock_logging_enabled_test.py @@ -0,0 +1,1020 @@ +from unittest import mock + +import pytest +from boto3 import client +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) + +CHECK_MODULE_PATH = "prowler.providers.aws.services.cloudtrail.cloudtrail_bedrock_logging_enabled.cloudtrail_bedrock_logging_enabled" + + +class Test_cloudtrail_bedrock_logging_enabled: + @mock_aws + def test_no_trails(self): + """Test when there are no CloudTrail trails configured.""" + from prowler.providers.aws.services.cloudtrail.cloudtrail_service import ( + Cloudtrail, + ) + + aws_provider = set_mocked_aws_provider() + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + f"{CHECK_MODULE_PATH}.cloudtrail_client", + new=Cloudtrail(aws_provider), + ), + ): + from prowler.providers.aws.services.cloudtrail.cloudtrail_bedrock_logging_enabled.cloudtrail_bedrock_logging_enabled import ( + cloudtrail_bedrock_logging_enabled, + ) + + check = cloudtrail_bedrock_logging_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No CloudTrail trails are configured to log Amazon Bedrock API calls." + ) + assert result[0].resource_id == AWS_ACCOUNT_NUMBER + assert ( + result[0].resource_arn + == f"arn:aws:cloudtrail:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:trail" + ) + assert result[0].region == AWS_REGION_US_EAST_1 + + @mock_aws + def test_trail_not_logging(self): + """Test when a trail exists but is not actively logging.""" + cloudtrail_client_us = client("cloudtrail", region_name=AWS_REGION_US_EAST_1) + s3_client_us = client("s3", region_name=AWS_REGION_US_EAST_1) + trail_name = "trail_test" + bucket_name = "bucket_test" + s3_client_us.create_bucket(Bucket=bucket_name) + cloudtrail_client_us.create_trail( + Name=trail_name, S3BucketName=bucket_name, IsMultiRegionTrail=False + ) + cloudtrail_client_us.put_event_selectors( + TrailName=trail_name, + EventSelectors=[ + { + "ReadWriteType": "All", + "IncludeManagementEvents": True, + "DataResources": [ + {"Type": "AWS::S3::Object", "Values": ["arn:aws:s3"]} + ], + } + ], + ) + # Trail is not started, so is_logging remains False + + from prowler.providers.aws.services.cloudtrail.cloudtrail_service import ( + Cloudtrail, + ) + + aws_provider = set_mocked_aws_provider() + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + f"{CHECK_MODULE_PATH}.cloudtrail_client", + new=Cloudtrail(aws_provider), + ), + ): + from prowler.providers.aws.services.cloudtrail.cloudtrail_bedrock_logging_enabled.cloudtrail_bedrock_logging_enabled import ( + cloudtrail_bedrock_logging_enabled, + ) + + check = cloudtrail_bedrock_logging_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No CloudTrail trails are configured to log Amazon Bedrock API calls." + ) + + @mock_aws + def test_trail_without_management_events(self): + """Test when a trail has data events but no management events enabled.""" + cloudtrail_client_us = client("cloudtrail", region_name=AWS_REGION_US_EAST_1) + s3_client_us = client("s3", region_name=AWS_REGION_US_EAST_1) + trail_name = "trail_test" + bucket_name = "bucket_test" + s3_client_us.create_bucket(Bucket=bucket_name) + cloudtrail_client_us.create_trail( + Name=trail_name, S3BucketName=bucket_name, IsMultiRegionTrail=False + ) + cloudtrail_client_us.start_logging(Name=trail_name) + cloudtrail_client_us.put_event_selectors( + TrailName=trail_name, + EventSelectors=[ + { + "ReadWriteType": "All", + "IncludeManagementEvents": False, + "DataResources": [ + {"Type": "AWS::S3::Object", "Values": ["arn:aws:s3"]} + ], + } + ], + ) + + from prowler.providers.aws.services.cloudtrail.cloudtrail_service import ( + Cloudtrail, + ) + + aws_provider = set_mocked_aws_provider() + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + f"{CHECK_MODULE_PATH}.cloudtrail_client", + new=Cloudtrail(aws_provider), + ), + ): + from prowler.providers.aws.services.cloudtrail.cloudtrail_bedrock_logging_enabled.cloudtrail_bedrock_logging_enabled import ( + cloudtrail_bedrock_logging_enabled, + ) + + check = cloudtrail_bedrock_logging_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No CloudTrail trails are configured to log Amazon Bedrock API calls." + ) + + @mock_aws + def test_trail_with_classic_management_events(self): + """Test PASS when a trail has classic management events enabled.""" + cloudtrail_client_us = client("cloudtrail", region_name=AWS_REGION_US_EAST_1) + s3_client_us = client("s3", region_name=AWS_REGION_US_EAST_1) + trail_name = "trail_test" + bucket_name = "bucket_test" + s3_client_us.create_bucket(Bucket=bucket_name) + trail = cloudtrail_client_us.create_trail( + Name=trail_name, S3BucketName=bucket_name, IsMultiRegionTrail=False + ) + cloudtrail_client_us.start_logging(Name=trail_name) + cloudtrail_client_us.put_event_selectors( + TrailName=trail_name, + EventSelectors=[ + { + "ReadWriteType": "All", + "IncludeManagementEvents": True, + "DataResources": [ + {"Type": "AWS::S3::Object", "Values": ["arn:aws:s3"]} + ], + } + ], + ) + + from prowler.providers.aws.services.cloudtrail.cloudtrail_service import ( + Cloudtrail, + ) + + aws_provider = set_mocked_aws_provider() + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + f"{CHECK_MODULE_PATH}.cloudtrail_client", + new=Cloudtrail(aws_provider), + ), + ): + from prowler.providers.aws.services.cloudtrail.cloudtrail_bedrock_logging_enabled.cloudtrail_bedrock_logging_enabled import ( + cloudtrail_bedrock_logging_enabled, + ) + + check = cloudtrail_bedrock_logging_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Trail {trail_name} from home region {AWS_REGION_US_EAST_1} has management events enabled to log Amazon Bedrock control-plane API calls." + ) + assert result[0].resource_id == trail_name + assert result[0].resource_arn == trail["TrailARN"] + assert result[0].region == AWS_REGION_US_EAST_1 + + @mock_aws + def test_trail_with_classic_management_events_read_only(self): + """Test FAIL when a trail has management events but ReadWriteType is ReadOnly.""" + cloudtrail_client_us = client("cloudtrail", region_name=AWS_REGION_US_EAST_1) + s3_client_us = client("s3", region_name=AWS_REGION_US_EAST_1) + trail_name = "trail_test" + bucket_name = "bucket_test" + s3_client_us.create_bucket(Bucket=bucket_name) + cloudtrail_client_us.create_trail( + Name=trail_name, S3BucketName=bucket_name, IsMultiRegionTrail=False + ) + cloudtrail_client_us.start_logging(Name=trail_name) + cloudtrail_client_us.put_event_selectors( + TrailName=trail_name, + EventSelectors=[ + { + "ReadWriteType": "ReadOnly", + "IncludeManagementEvents": True, + "DataResources": [ + {"Type": "AWS::S3::Object", "Values": ["arn:aws:s3"]} + ], + } + ], + ) + + from prowler.providers.aws.services.cloudtrail.cloudtrail_service import ( + Cloudtrail, + ) + + aws_provider = set_mocked_aws_provider() + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + f"{CHECK_MODULE_PATH}.cloudtrail_client", + new=Cloudtrail(aws_provider), + ), + ): + from prowler.providers.aws.services.cloudtrail.cloudtrail_bedrock_logging_enabled.cloudtrail_bedrock_logging_enabled import ( + cloudtrail_bedrock_logging_enabled, + ) + + check = cloudtrail_bedrock_logging_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No CloudTrail trails are configured to log Amazon Bedrock API calls." + ) + + @mock_aws + def test_trail_with_advanced_management_events(self): + """Test PASS when a trail has unrestricted advanced management selectors.""" + cloudtrail_client_us = client("cloudtrail", region_name=AWS_REGION_US_EAST_1) + s3_client_us = client("s3", region_name=AWS_REGION_US_EAST_1) + trail_name = "trail_test" + bucket_name = "bucket_test" + s3_client_us.create_bucket(Bucket=bucket_name) + trail = cloudtrail_client_us.create_trail( + Name=trail_name, S3BucketName=bucket_name, IsMultiRegionTrail=False + ) + cloudtrail_client_us.start_logging(Name=trail_name) + cloudtrail_client_us.put_event_selectors( + TrailName=trail_name, + AdvancedEventSelectors=[ + { + "Name": "Management events selector", + "FieldSelectors": [ + {"Field": "eventCategory", "Equals": ["Management"]}, + ], + }, + ], + ) + + from prowler.providers.aws.services.cloudtrail.cloudtrail_service import ( + Cloudtrail, + ) + + aws_provider = set_mocked_aws_provider() + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + f"{CHECK_MODULE_PATH}.cloudtrail_client", + new=Cloudtrail(aws_provider), + ), + ): + from prowler.providers.aws.services.cloudtrail.cloudtrail_bedrock_logging_enabled.cloudtrail_bedrock_logging_enabled import ( + cloudtrail_bedrock_logging_enabled, + ) + + check = cloudtrail_bedrock_logging_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Trail {trail_name} from home region {AWS_REGION_US_EAST_1} has an advanced management event selector to log Amazon Bedrock control-plane API calls." + ) + assert result[0].resource_id == trail_name + assert result[0].resource_arn == trail["TrailARN"] + assert result[0].region == AWS_REGION_US_EAST_1 + + @mock_aws + @pytest.mark.parametrize( + "event_source", + [ + pytest.param("bedrock.amazonaws.com", id="bedrock"), + pytest.param("bedrock-agent.amazonaws.com", id="bedrock-agent"), + pytest.param("bedrock-runtime.amazonaws.com", id="bedrock-runtime"), + pytest.param( + "bedrock-agent-runtime.amazonaws.com", + id="bedrock-agent-runtime", + ), + pytest.param( + "bedrock-data-automation.amazonaws.com", + id="bedrock-data-automation", + ), + pytest.param( + "bedrock-data-automation-runtime.amazonaws.com", + id="bedrock-data-automation-runtime", + ), + ], + ) + def test_trail_with_advanced_management_events_bedrock_event_sources( + self, event_source + ): + """Test PASS when advanced management events are scoped to Bedrock family sources.""" + from prowler.providers.aws.services.cloudtrail.cloudtrail_service import ( + Cloudtrail, + Event_Selector, + ) + + cloudtrail_client_us = client("cloudtrail", region_name=AWS_REGION_US_EAST_1) + s3_client_us = client("s3", region_name=AWS_REGION_US_EAST_1) + trail_name = "trail_test" + bucket_name = "bucket_test" + s3_client_us.create_bucket(Bucket=bucket_name) + trail = cloudtrail_client_us.create_trail( + Name=trail_name, S3BucketName=bucket_name, IsMultiRegionTrail=False + ) + cloudtrail_client_us.start_logging(Name=trail_name) + + aws_provider = set_mocked_aws_provider() + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + f"{CHECK_MODULE_PATH}.cloudtrail_client", + new=Cloudtrail(aws_provider), + ) as mock_cloudtrail_client, + ): + trail_arn = trail["TrailARN"] + mock_cloudtrail_client.trails[trail_arn].data_events = [ + Event_Selector( + is_advanced=True, + event_selector={ + "Name": "Bedrock management events selector", + "FieldSelectors": [ + {"Field": "eventCategory", "Equals": ["Management"]}, + {"Field": "eventSource", "Equals": [event_source]}, + ], + }, + ) + ] + + from prowler.providers.aws.services.cloudtrail.cloudtrail_bedrock_logging_enabled.cloudtrail_bedrock_logging_enabled import ( + cloudtrail_bedrock_logging_enabled, + ) + + check = cloudtrail_bedrock_logging_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Trail {trail_name} from home region {AWS_REGION_US_EAST_1} has an advanced management event selector to log Amazon Bedrock control-plane API calls." + ) + assert result[0].resource_id == trail_name + assert result[0].resource_arn == trail["TrailARN"] + assert result[0].region == AWS_REGION_US_EAST_1 + + @mock_aws + def test_trail_with_advanced_bedrock_data_events(self): + """Test PASS when a trail has advanced event selectors for Bedrock resources.""" + from prowler.providers.aws.services.cloudtrail.cloudtrail_service import ( + Cloudtrail, + Event_Selector, + ) + + cloudtrail_client_us = client("cloudtrail", region_name=AWS_REGION_US_EAST_1) + s3_client_us = client("s3", region_name=AWS_REGION_US_EAST_1) + trail_name = "trail_test" + bucket_name = "bucket_test" + s3_client_us.create_bucket(Bucket=bucket_name) + trail = cloudtrail_client_us.create_trail( + Name=trail_name, S3BucketName=bucket_name, IsMultiRegionTrail=False + ) + cloudtrail_client_us.start_logging(Name=trail_name) + + aws_provider = set_mocked_aws_provider() + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + f"{CHECK_MODULE_PATH}.cloudtrail_client", + new=Cloudtrail(aws_provider), + ) as mock_cloudtrail_client, + ): + # Manually inject the Bedrock advanced event selector since moto + # does not support Bedrock resource types. + trail_arn = trail["TrailARN"] + mock_cloudtrail_client.trails[trail_arn].data_events = [ + Event_Selector( + is_advanced=True, + event_selector={ + "Name": "Bedrock data events", + "FieldSelectors": [ + {"Field": "eventCategory", "Equals": ["Data"]}, + { + "Field": "resources.type", + "Equals": ["AWS::Bedrock::Model"], + }, + ], + }, + ) + ] + + from prowler.providers.aws.services.cloudtrail.cloudtrail_bedrock_logging_enabled.cloudtrail_bedrock_logging_enabled import ( + cloudtrail_bedrock_logging_enabled, + ) + + check = cloudtrail_bedrock_logging_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Trail {trail_name} from home region {AWS_REGION_US_EAST_1} has an advanced data event selector to log Amazon Bedrock API calls." + ) + assert result[0].resource_id == trail_name + assert result[0].resource_arn == trail["TrailARN"] + assert result[0].region == AWS_REGION_US_EAST_1 + + @mock_aws + def test_trail_with_advanced_bedrock_guardrail_events(self): + """Test PASS when a trail has advanced event selectors for Bedrock Guardrail resources.""" + from prowler.providers.aws.services.cloudtrail.cloudtrail_service import ( + Cloudtrail, + Event_Selector, + ) + + cloudtrail_client_us = client("cloudtrail", region_name=AWS_REGION_US_EAST_1) + s3_client_us = client("s3", region_name=AWS_REGION_US_EAST_1) + trail_name = "trail_test" + bucket_name = "bucket_test" + s3_client_us.create_bucket(Bucket=bucket_name) + trail = cloudtrail_client_us.create_trail( + Name=trail_name, S3BucketName=bucket_name, IsMultiRegionTrail=False + ) + cloudtrail_client_us.start_logging(Name=trail_name) + + aws_provider = set_mocked_aws_provider() + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + f"{CHECK_MODULE_PATH}.cloudtrail_client", + new=Cloudtrail(aws_provider), + ) as mock_cloudtrail_client, + ): + # Manually inject the Bedrock Guardrail advanced event selector + # since moto does not support Bedrock resource types. + trail_arn = trail["TrailARN"] + mock_cloudtrail_client.trails[trail_arn].data_events = [ + Event_Selector( + is_advanced=True, + event_selector={ + "Name": "Bedrock guardrail events", + "FieldSelectors": [ + {"Field": "eventCategory", "Equals": ["Data"]}, + { + "Field": "resources.type", + "Equals": ["AWS::Bedrock::Guardrail"], + }, + ], + }, + ) + ] + + from prowler.providers.aws.services.cloudtrail.cloudtrail_bedrock_logging_enabled.cloudtrail_bedrock_logging_enabled import ( + cloudtrail_bedrock_logging_enabled, + ) + + check = cloudtrail_bedrock_logging_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Trail {trail_name} from home region {AWS_REGION_US_EAST_1} has an advanced data event selector to log Amazon Bedrock API calls." + ) + assert result[0].resource_id == trail_name + assert result[0].resource_arn == trail["TrailARN"] + assert result[0].region == AWS_REGION_US_EAST_1 + + @mock_aws + def test_trail_with_advanced_non_bedrock_data_events(self): + """Test FAIL when a trail has advanced event selectors for non-Bedrock resources.""" + cloudtrail_client_us = client("cloudtrail", region_name=AWS_REGION_US_EAST_1) + s3_client_us = client("s3", region_name=AWS_REGION_US_EAST_1) + trail_name = "trail_test" + bucket_name = "bucket_test" + s3_client_us.create_bucket(Bucket=bucket_name) + cloudtrail_client_us.create_trail( + Name=trail_name, S3BucketName=bucket_name, IsMultiRegionTrail=False + ) + cloudtrail_client_us.start_logging(Name=trail_name) + cloudtrail_client_us.put_event_selectors( + TrailName=trail_name, + AdvancedEventSelectors=[ + { + "Name": "S3 data events", + "FieldSelectors": [ + {"Field": "eventCategory", "Equals": ["Data"]}, + { + "Field": "resources.type", + "Equals": ["AWS::S3::Object"], + }, + ], + }, + ], + ) + + from prowler.providers.aws.services.cloudtrail.cloudtrail_service import ( + Cloudtrail, + ) + + aws_provider = set_mocked_aws_provider() + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + f"{CHECK_MODULE_PATH}.cloudtrail_client", + new=Cloudtrail(aws_provider), + ), + ): + from prowler.providers.aws.services.cloudtrail.cloudtrail_bedrock_logging_enabled.cloudtrail_bedrock_logging_enabled import ( + cloudtrail_bedrock_logging_enabled, + ) + + check = cloudtrail_bedrock_logging_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No CloudTrail trails are configured to log Amazon Bedrock API calls." + ) + + @mock_aws + def test_trail_with_classic_management_events_write_only(self): + """Test PASS when a trail has management events with ReadWriteType WriteOnly.""" + cloudtrail_client_us = client("cloudtrail", region_name=AWS_REGION_US_EAST_1) + s3_client_us = client("s3", region_name=AWS_REGION_US_EAST_1) + trail_name = "trail_test" + bucket_name = "bucket_test" + s3_client_us.create_bucket(Bucket=bucket_name) + trail = cloudtrail_client_us.create_trail( + Name=trail_name, S3BucketName=bucket_name, IsMultiRegionTrail=False + ) + cloudtrail_client_us.start_logging(Name=trail_name) + cloudtrail_client_us.put_event_selectors( + TrailName=trail_name, + EventSelectors=[ + { + "ReadWriteType": "WriteOnly", + "IncludeManagementEvents": True, + "DataResources": [], + } + ], + ) + + from prowler.providers.aws.services.cloudtrail.cloudtrail_service import ( + Cloudtrail, + ) + + aws_provider = set_mocked_aws_provider() + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + f"{CHECK_MODULE_PATH}.cloudtrail_client", + new=Cloudtrail(aws_provider), + ), + ): + from prowler.providers.aws.services.cloudtrail.cloudtrail_bedrock_logging_enabled.cloudtrail_bedrock_logging_enabled import ( + cloudtrail_bedrock_logging_enabled, + ) + + check = cloudtrail_bedrock_logging_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Trail {trail_name} from home region {AWS_REGION_US_EAST_1} has management events enabled to log Amazon Bedrock control-plane API calls." + ) + assert result[0].resource_id == trail_name + assert result[0].resource_arn == trail["TrailARN"] + assert result[0].region == AWS_REGION_US_EAST_1 + + @mock_aws + def test_trail_with_classic_management_events_default_read_write_type(self): + """Test PASS when a classic selector omits ReadWriteType and uses the AWS default.""" + cloudtrail_client_us = client("cloudtrail", region_name=AWS_REGION_US_EAST_1) + s3_client_us = client("s3", region_name=AWS_REGION_US_EAST_1) + trail_name = "trail_test" + bucket_name = "bucket_test" + s3_client_us.create_bucket(Bucket=bucket_name) + trail = cloudtrail_client_us.create_trail( + Name=trail_name, S3BucketName=bucket_name, IsMultiRegionTrail=False + ) + cloudtrail_client_us.start_logging(Name=trail_name) + cloudtrail_client_us.put_event_selectors( + TrailName=trail_name, + EventSelectors=[ + { + "IncludeManagementEvents": True, + "DataResources": [], + } + ], + ) + + from prowler.providers.aws.services.cloudtrail.cloudtrail_service import ( + Cloudtrail, + ) + + aws_provider = set_mocked_aws_provider() + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + f"{CHECK_MODULE_PATH}.cloudtrail_client", + new=Cloudtrail(aws_provider), + ), + ): + from prowler.providers.aws.services.cloudtrail.cloudtrail_bedrock_logging_enabled.cloudtrail_bedrock_logging_enabled import ( + cloudtrail_bedrock_logging_enabled, + ) + + check = cloudtrail_bedrock_logging_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Trail {trail_name} from home region {AWS_REGION_US_EAST_1} has management events enabled to log Amazon Bedrock control-plane API calls." + ) + assert result[0].resource_id == trail_name + assert result[0].resource_arn == trail["TrailARN"] + assert result[0].region == AWS_REGION_US_EAST_1 + + @mock_aws + def test_trail_with_advanced_management_events_read_only(self): + """Test FAIL when advanced management event selector has readOnly=true restriction.""" + from prowler.providers.aws.services.cloudtrail.cloudtrail_service import ( + Cloudtrail, + Event_Selector, + ) + + cloudtrail_client_us = client("cloudtrail", region_name=AWS_REGION_US_EAST_1) + s3_client_us = client("s3", region_name=AWS_REGION_US_EAST_1) + trail_name = "trail_test" + bucket_name = "bucket_test" + s3_client_us.create_bucket(Bucket=bucket_name) + trail = cloudtrail_client_us.create_trail( + Name=trail_name, S3BucketName=bucket_name, IsMultiRegionTrail=False + ) + cloudtrail_client_us.start_logging(Name=trail_name) + + aws_provider = set_mocked_aws_provider() + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + f"{CHECK_MODULE_PATH}.cloudtrail_client", + new=Cloudtrail(aws_provider), + ) as mock_cloudtrail_client, + ): + trail_arn = trail["TrailARN"] + mock_cloudtrail_client.trails[trail_arn].data_events = [ + Event_Selector( + is_advanced=True, + event_selector={ + "Name": "Management events selector", + "FieldSelectors": [ + {"Field": "eventCategory", "Equals": ["Management"]}, + {"Field": "readOnly", "Equals": ["true"]}, + ], + }, + ) + ] + + from prowler.providers.aws.services.cloudtrail.cloudtrail_bedrock_logging_enabled.cloudtrail_bedrock_logging_enabled import ( + cloudtrail_bedrock_logging_enabled, + ) + + check = cloudtrail_bedrock_logging_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No CloudTrail trails are configured to log Amazon Bedrock API calls." + ) + + @mock_aws + def test_trail_with_advanced_management_events_read_only_not_equals_false(self): + """Test FAIL when advanced management selector restricts events with NotEquals false.""" + from prowler.providers.aws.services.cloudtrail.cloudtrail_service import ( + Cloudtrail, + Event_Selector, + ) + + cloudtrail_client_us = client("cloudtrail", region_name=AWS_REGION_US_EAST_1) + s3_client_us = client("s3", region_name=AWS_REGION_US_EAST_1) + trail_name = "trail_test" + bucket_name = "bucket_test" + s3_client_us.create_bucket(Bucket=bucket_name) + trail = cloudtrail_client_us.create_trail( + Name=trail_name, S3BucketName=bucket_name, IsMultiRegionTrail=False + ) + cloudtrail_client_us.start_logging(Name=trail_name) + + aws_provider = set_mocked_aws_provider() + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + f"{CHECK_MODULE_PATH}.cloudtrail_client", + new=Cloudtrail(aws_provider), + ) as mock_cloudtrail_client, + ): + trail_arn = trail["TrailARN"] + mock_cloudtrail_client.trails[trail_arn].data_events = [ + Event_Selector( + is_advanced=True, + event_selector={ + "Name": "Management events selector", + "FieldSelectors": [ + {"Field": "eventCategory", "Equals": ["Management"]}, + {"Field": "readOnly", "NotEquals": ["false"]}, + ], + }, + ) + ] + + from prowler.providers.aws.services.cloudtrail.cloudtrail_bedrock_logging_enabled.cloudtrail_bedrock_logging_enabled import ( + cloudtrail_bedrock_logging_enabled, + ) + + check = cloudtrail_bedrock_logging_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No CloudTrail trails are configured to log Amazon Bedrock API calls." + ) + + @mock_aws + def test_trail_with_advanced_management_events_other_service_event_source(self): + """Test FAIL when advanced management events are scoped to a non-Bedrock event source.""" + from prowler.providers.aws.services.cloudtrail.cloudtrail_service import ( + Cloudtrail, + Event_Selector, + ) + + cloudtrail_client_us = client("cloudtrail", region_name=AWS_REGION_US_EAST_1) + s3_client_us = client("s3", region_name=AWS_REGION_US_EAST_1) + trail_name = "trail_test" + bucket_name = "bucket_test" + s3_client_us.create_bucket(Bucket=bucket_name) + trail = cloudtrail_client_us.create_trail( + Name=trail_name, S3BucketName=bucket_name, IsMultiRegionTrail=False + ) + cloudtrail_client_us.start_logging(Name=trail_name) + + aws_provider = set_mocked_aws_provider() + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + f"{CHECK_MODULE_PATH}.cloudtrail_client", + new=Cloudtrail(aws_provider), + ) as mock_cloudtrail_client, + ): + trail_arn = trail["TrailARN"] + mock_cloudtrail_client.trails[trail_arn].data_events = [ + Event_Selector( + is_advanced=True, + event_selector={ + "Name": "EC2 management events selector", + "FieldSelectors": [ + {"Field": "eventCategory", "Equals": ["Management"]}, + { + "Field": "eventSource", + "Equals": ["ec2.amazonaws.com"], + }, + ], + }, + ) + ] + + from prowler.providers.aws.services.cloudtrail.cloudtrail_bedrock_logging_enabled.cloudtrail_bedrock_logging_enabled import ( + cloudtrail_bedrock_logging_enabled, + ) + + check = cloudtrail_bedrock_logging_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No CloudTrail trails are configured to log Amazon Bedrock API calls." + ) + + @mock_aws + def test_access_denied(self): + """Test when trails are None due to access denied.""" + from prowler.providers.aws.services.cloudtrail.cloudtrail_service import ( + Cloudtrail, + ) + + aws_provider = set_mocked_aws_provider() + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + f"{CHECK_MODULE_PATH}.cloudtrail_client", + new=Cloudtrail(aws_provider), + ) as mock_cloudtrail_client, + ): + from prowler.providers.aws.services.cloudtrail.cloudtrail_bedrock_logging_enabled.cloudtrail_bedrock_logging_enabled import ( + cloudtrail_bedrock_logging_enabled, + ) + + mock_cloudtrail_client.trails = None + check = cloudtrail_bedrock_logging_enabled() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + @pytest.mark.parametrize( + ("selector", "expected"), + [ + pytest.param( + {"Equals": ["bedrock.amazonaws.com"]}, + True, + id="equals-match", + ), + pytest.param( + {"Equals": ["ec2.amazonaws.com"]}, + False, + id="equals-mismatch", + ), + pytest.param( + {"NotEquals": ["ec2.amazonaws.com"]}, + True, + id="not-equals-match", + ), + pytest.param( + {"NotEquals": ["bedrock.amazonaws.com"]}, + False, + id="not-equals-mismatch", + ), + pytest.param( + {"StartsWith": ["bedrock."]}, + True, + id="starts-with-match", + ), + pytest.param( + {"StartsWith": ["ec2."]}, + False, + id="starts-with-mismatch", + ), + pytest.param( + {"NotStartsWith": ["ec2."]}, + True, + id="not-starts-with-match", + ), + pytest.param( + {"NotStartsWith": ["bedrock."]}, + False, + id="not-starts-with-mismatch", + ), + pytest.param( + {"EndsWith": [".amazonaws.com"]}, + True, + id="ends-with-match", + ), + pytest.param( + {"EndsWith": [".amazonaws.org"]}, + False, + id="ends-with-mismatch", + ), + pytest.param( + {"NotEndsWith": [".amazonaws.org"]}, + True, + id="not-ends-with-match", + ), + pytest.param( + {"NotEndsWith": [".amazonaws.com"]}, + False, + id="not-ends-with-mismatch", + ), + pytest.param({}, True, id="no-conditions"), + ], + ) + def test_field_selector_matches_value(self, selector, expected): + """Test advanced field selector operators against the Bedrock event source.""" + from prowler.providers.aws.services.cloudtrail.cloudtrail_service import ( + Cloudtrail, + ) + + aws_provider = set_mocked_aws_provider() + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + f"{CHECK_MODULE_PATH}.cloudtrail_client", + new=Cloudtrail(aws_provider), + ), + ): + from prowler.providers.aws.services.cloudtrail.cloudtrail_bedrock_logging_enabled.cloudtrail_bedrock_logging_enabled import ( + cloudtrail_bedrock_logging_enabled, + ) + + assert ( + cloudtrail_bedrock_logging_enabled._field_selector_matches_value( + "bedrock.amazonaws.com", selector + ) + is expected + ) diff --git a/tests/providers/aws/services/iam/iam_no_custom_policy_permissive_role_assumption/iam_no_custom_policy_permissive_role_assumption_test.py b/tests/providers/aws/services/iam/iam_no_custom_policy_permissive_role_assumption/iam_no_custom_policy_permissive_role_assumption_test.py index 6a9576050c..1fbebe6f63 100644 --- a/tests/providers/aws/services/iam/iam_no_custom_policy_permissive_role_assumption/iam_no_custom_policy_permissive_role_assumption_test.py +++ b/tests/providers/aws/services/iam/iam_no_custom_policy_permissive_role_assumption/iam_no_custom_policy_permissive_role_assumption_test.py @@ -408,3 +408,83 @@ class Test_iam_no_custom_policy_permissive_role_assumption: assert search( "allows permissive STS Role assumption", result[0].status_extended ) + + @mock_aws + def test_unattached_policy_skipped_when_scan_unused_services_disabled(self): + iam_client = client("iam") + policy_name = "unattached_permissive_assume_role" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + {"Effect": "Allow", "Action": "sts:AssumeRole", "Resource": "*"}, + ], + } + iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document) + ) + + from prowler.providers.aws.services.iam.iam_service import IAM + + aws_provider = set_mocked_aws_provider( + [AWS_REGION_US_EAST_1], scan_unused_services=False + ) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.iam.iam_no_custom_policy_permissive_role_assumption.iam_no_custom_policy_permissive_role_assumption.iam_client", + new=IAM(aws_provider), + ): + from prowler.providers.aws.services.iam.iam_no_custom_policy_permissive_role_assumption.iam_no_custom_policy_permissive_role_assumption import ( + iam_no_custom_policy_permissive_role_assumption, + ) + + check = iam_no_custom_policy_permissive_role_assumption() + result = check.execute() + assert result == [] + + @mock_aws + def test_attached_policy_fails_when_scan_unused_services_disabled(self): + iam_client = client("iam") + user_name = "test_user_assume_role" + policy_name = "attached_permissive_assume_role" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + {"Effect": "Allow", "Action": "sts:AssumeRole", "Resource": "*"}, + ], + } + arn = iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document) + )["Policy"]["Arn"] + iam_client.create_user(UserName=user_name) + iam_client.attach_user_policy(UserName=user_name, PolicyArn=arn) + + from prowler.providers.aws.services.iam.iam_service import IAM + + aws_provider = set_mocked_aws_provider( + [AWS_REGION_US_EAST_1], scan_unused_services=False + ) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.iam.iam_no_custom_policy_permissive_role_assumption.iam_no_custom_policy_permissive_role_assumption.iam_client", + new=IAM(aws_provider), + ): + from prowler.providers.aws.services.iam.iam_no_custom_policy_permissive_role_assumption.iam_no_custom_policy_permissive_role_assumption import ( + iam_no_custom_policy_permissive_role_assumption, + ) + + check = iam_no_custom_policy_permissive_role_assumption() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].resource_arn == arn + assert search( + "allows permissive STS Role assumption", result[0].status_extended + ) diff --git a/tests/providers/aws/services/iam/iam_policy_allows_privilege_escalation/iam_policy_allows_privilege_escalation_test.py b/tests/providers/aws/services/iam/iam_policy_allows_privilege_escalation/iam_policy_allows_privilege_escalation_test.py index 79e1b25955..850a5ba2cb 100644 --- a/tests/providers/aws/services/iam/iam_policy_allows_privilege_escalation/iam_policy_allows_privilege_escalation_test.py +++ b/tests/providers/aws/services/iam/iam_policy_allows_privilege_escalation/iam_policy_allows_privilege_escalation_test.py @@ -1261,3 +1261,86 @@ class Test_iam_policy_allows_privilege_escalation: permissions ]: assert search(permission, finding.status_extended) + + @mock_aws + def test_unattached_policy_skipped_when_scan_unused_services_disabled(self): + iam_client = client("iam", region_name=AWS_REGION_US_EAST_1) + policy_name = "unattached_privilege_escalation" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + {"Effect": "Allow", "Action": "iam:CreateAccessKey", "Resource": "*"}, + ], + } + iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document) + ) + + aws_provider = set_mocked_aws_provider( + [AWS_REGION_US_EAST_1], scan_unused_services=False + ) + from prowler.providers.aws.services.iam.iam_service import IAM + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.iam.iam_policy_allows_privilege_escalation.iam_policy_allows_privilege_escalation.iam_client", + new=IAM(aws_provider), + ), + ): + from prowler.providers.aws.services.iam.iam_policy_allows_privilege_escalation.iam_policy_allows_privilege_escalation import ( + iam_policy_allows_privilege_escalation, + ) + + check = iam_policy_allows_privilege_escalation() + result = check.execute() + assert result == [] + + @mock_aws + def test_attached_policy_fails_when_scan_unused_services_disabled(self): + iam_client = client("iam", region_name=AWS_REGION_US_EAST_1) + user_name = "test_user_privesc" + policy_name = "attached_privilege_escalation" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + {"Effect": "Allow", "Action": "iam:CreateAccessKey", "Resource": "*"}, + ], + } + policy_arn = iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document) + )["Policy"]["Arn"] + iam_client.create_user(UserName=user_name) + iam_client.attach_user_policy(UserName=user_name, PolicyArn=policy_arn) + + aws_provider = set_mocked_aws_provider( + [AWS_REGION_US_EAST_1], scan_unused_services=False + ) + from prowler.providers.aws.services.iam.iam_service import IAM + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.iam.iam_policy_allows_privilege_escalation.iam_policy_allows_privilege_escalation.iam_client", + new=IAM(aws_provider), + ), + ): + from prowler.providers.aws.services.iam.iam_policy_allows_privilege_escalation.iam_policy_allows_privilege_escalation import ( + iam_policy_allows_privilege_escalation, + ) + + check = iam_policy_allows_privilege_escalation() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].resource_arn == policy_arn + assert search( + f"Custom Policy {policy_arn} allows privilege escalation", + result[0].status_extended, + ) diff --git a/tests/providers/aws/services/iam/iam_policy_no_full_access_to_cloudtrail/iam_policy_no_full_access_to_cloudtrail_test.py b/tests/providers/aws/services/iam/iam_policy_no_full_access_to_cloudtrail/iam_policy_no_full_access_to_cloudtrail_test.py index a29b130f27..38f5a77e19 100644 --- a/tests/providers/aws/services/iam/iam_policy_no_full_access_to_cloudtrail/iam_policy_no_full_access_to_cloudtrail_test.py +++ b/tests/providers/aws/services/iam/iam_policy_no_full_access_to_cloudtrail/iam_policy_no_full_access_to_cloudtrail_test.py @@ -207,3 +207,78 @@ class Test_iam_policy_no_full_access_to_cloudtrail: assert result[0].resource_id == "policy_no_cloudtrail_full_no_actions" assert result[0].resource_arn == arn assert result[0].region == "us-east-1" + + @mock_aws + def test_unattached_policy_skipped_when_scan_unused_services_disabled(self): + aws_provider = set_mocked_aws_provider( + [AWS_REGION_US_EAST_1], scan_unused_services=False + ) + iam_client = client("iam", region_name=AWS_REGION_US_EAST_1) + policy_name = "unattached_cloudtrail_full" + policy_document_full_access = { + "Version": "2012-10-17", + "Statement": [ + {"Effect": "Allow", "Action": "cloudtrail:*", "Resource": "*"}, + ], + } + iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document_full_access) + ) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.iam.iam_policy_no_full_access_to_cloudtrail.iam_policy_no_full_access_to_cloudtrail.iam_client", + new=IAM(aws_provider), + ): + from prowler.providers.aws.services.iam.iam_policy_no_full_access_to_cloudtrail.iam_policy_no_full_access_to_cloudtrail import ( + iam_policy_no_full_access_to_cloudtrail, + ) + + check = iam_policy_no_full_access_to_cloudtrail() + result = check.execute() + assert result == [] + + @mock_aws + def test_attached_policy_fails_when_scan_unused_services_disabled(self): + aws_provider = set_mocked_aws_provider( + [AWS_REGION_US_EAST_1], scan_unused_services=False + ) + iam_client = client("iam", region_name=AWS_REGION_US_EAST_1) + user_name = "test_user_cloudtrail" + policy_name = "attached_cloudtrail_full" + policy_document_full_access = { + "Version": "2012-10-17", + "Statement": [ + {"Effect": "Allow", "Action": "cloudtrail:*", "Resource": "*"}, + ], + } + arn = iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document_full_access) + )["Policy"]["Arn"] + iam_client.create_user(UserName=user_name) + iam_client.attach_user_policy(UserName=user_name, PolicyArn=arn) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.iam.iam_policy_no_full_access_to_cloudtrail.iam_policy_no_full_access_to_cloudtrail.iam_client", + new=IAM(aws_provider), + ): + from prowler.providers.aws.services.iam.iam_policy_no_full_access_to_cloudtrail.iam_policy_no_full_access_to_cloudtrail import ( + iam_policy_no_full_access_to_cloudtrail, + ) + + check = iam_policy_no_full_access_to_cloudtrail() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Custom Policy {policy_name} allows 'cloudtrail:*' privileges." + ) + assert result[0].resource_arn == arn diff --git a/tests/providers/aws/services/iam/iam_policy_no_full_access_to_kms/iam_policy_no_full_access_to_kms_test.py b/tests/providers/aws/services/iam/iam_policy_no_full_access_to_kms/iam_policy_no_full_access_to_kms_test.py index 514a83b935..15ff9f80ae 100644 --- a/tests/providers/aws/services/iam/iam_policy_no_full_access_to_kms/iam_policy_no_full_access_to_kms_test.py +++ b/tests/providers/aws/services/iam/iam_policy_no_full_access_to_kms/iam_policy_no_full_access_to_kms_test.py @@ -329,6 +329,81 @@ class Test_iam_policy_no_full_access_to_kms_with_unicode: assert result[0].resource_arn == arn assert result[0].region == "us-east-1" + @mock_aws + def test_unattached_policy_skipped_when_scan_unused_services_disabled(self): + aws_provider = set_mocked_aws_provider( + [AWS_REGION_US_EAST_1], scan_unused_services=False + ) + iam_client = client("iam") + policy_name = "unattached_kms_full" + policy_document_full_access = { + "Version": "2012-10-17", + "Statement": [ + {"Effect": "Allow", "Action": "kms:*", "Resource": "*"}, + ], + } + iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document_full_access) + ) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.iam.iam_policy_no_full_access_to_kms.iam_policy_no_full_access_to_kms.iam_client", + new=IAM(aws_provider), + ): + from prowler.providers.aws.services.iam.iam_policy_no_full_access_to_kms.iam_policy_no_full_access_to_kms import ( + iam_policy_no_full_access_to_kms, + ) + + check = iam_policy_no_full_access_to_kms() + result = check.execute() + assert result == [] + + @mock_aws + def test_attached_policy_fails_when_scan_unused_services_disabled(self): + aws_provider = set_mocked_aws_provider( + [AWS_REGION_US_EAST_1], scan_unused_services=False + ) + iam_client = client("iam") + user_name = "test_user_kms" + policy_name = "attached_kms_full" + policy_document_full_access = { + "Version": "2012-10-17", + "Statement": [ + {"Effect": "Allow", "Action": "kms:*", "Resource": "*"}, + ], + } + arn = iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document_full_access) + )["Policy"]["Arn"] + iam_client.create_user(UserName=user_name) + iam_client.attach_user_policy(UserName=user_name, PolicyArn=arn) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.iam.iam_policy_no_full_access_to_kms.iam_policy_no_full_access_to_kms.iam_client", + new=IAM(aws_provider), + ): + from prowler.providers.aws.services.iam.iam_policy_no_full_access_to_kms.iam_policy_no_full_access_to_kms import ( + iam_policy_no_full_access_to_kms, + ) + + check = iam_policy_no_full_access_to_kms() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Custom Policy {policy_name} allows 'kms:*' privileges." + ) + assert result[0].resource_arn == arn + @mock_aws def test_policy_full_access_and_full_deny_to_kms(self): aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) diff --git a/tests/providers/aws/services/iam/iam_policy_no_wildcard_marketplace_subscribe/iam_policy_no_wildcard_marketplace_subscribe_test.py b/tests/providers/aws/services/iam/iam_policy_no_wildcard_marketplace_subscribe/iam_policy_no_wildcard_marketplace_subscribe_test.py index b667e7dbfb..b3b41f5e7a 100644 --- a/tests/providers/aws/services/iam/iam_policy_no_wildcard_marketplace_subscribe/iam_policy_no_wildcard_marketplace_subscribe_test.py +++ b/tests/providers/aws/services/iam/iam_policy_no_wildcard_marketplace_subscribe/iam_policy_no_wildcard_marketplace_subscribe_test.py @@ -507,3 +507,84 @@ class Test_iam_policy_no_wildcard_marketplace_subscribe: check = iam_policy_no_wildcard_marketplace_subscribe() result = check.execute() assert len(result) == 0 + + @mock_aws + def test_unattached_policy_skipped_when_scan_unused_services_disabled(self): + """No FAIL for an unattached risky policy when --scan-unused-services is off.""" + aws_provider = set_mocked_aws_provider( + [AWS_REGION_US_EAST_1], scan_unused_services=False + ) + iam_client = client("iam") + policy_name = "unattached_marketplace_subscribe" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "aws-marketplace:Subscribe", + "Resource": "*", + }, + ], + } + iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document) + ) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + f"{CHECK_MODULE_PATH}.iam_client", + new=IAM(aws_provider), + ): + from prowler.providers.aws.services.iam.iam_policy_no_wildcard_marketplace_subscribe.iam_policy_no_wildcard_marketplace_subscribe import ( + iam_policy_no_wildcard_marketplace_subscribe, + ) + + check = iam_policy_no_wildcard_marketplace_subscribe() + result = check.execute() + assert result == [] + + @mock_aws + def test_attached_policy_fails_when_scan_unused_services_disabled(self): + """Attached risky policy still FAILs when --scan-unused-services is off.""" + aws_provider = set_mocked_aws_provider( + [AWS_REGION_US_EAST_1], scan_unused_services=False + ) + iam_client = client("iam") + user_name = "test_user_marketplace" + policy_name = "attached_marketplace_subscribe" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "aws-marketplace:Subscribe", + "Resource": "*", + }, + ], + } + arn = iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document) + )["Policy"]["Arn"] + iam_client.create_user(UserName=user_name) + iam_client.attach_user_policy(UserName=user_name, PolicyArn=arn) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + f"{CHECK_MODULE_PATH}.iam_client", + new=IAM(aws_provider), + ): + from prowler.providers.aws.services.iam.iam_policy_no_wildcard_marketplace_subscribe.iam_policy_no_wildcard_marketplace_subscribe import ( + iam_policy_no_wildcard_marketplace_subscribe, + ) + + check = iam_policy_no_wildcard_marketplace_subscribe() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].resource_arn == arn diff --git a/tests/providers/aws/services/iam/iam_role_access_not_stale_to_bedrock/iam_role_access_not_stale_to_bedrock_test.py b/tests/providers/aws/services/iam/iam_role_access_not_stale_to_bedrock/iam_role_access_not_stale_to_bedrock_test.py index ecff542b28..06e1f27db7 100644 --- a/tests/providers/aws/services/iam/iam_role_access_not_stale_to_bedrock/iam_role_access_not_stale_to_bedrock_test.py +++ b/tests/providers/aws/services/iam/iam_role_access_not_stale_to_bedrock/iam_role_access_not_stale_to_bedrock_test.py @@ -190,6 +190,258 @@ class Test_iam_role_access_not_stale_to_bedrock: assert "has never used them" in result[0].status_extended assert "Role" in result[0].status_extended + @mock_aws + def test_no_roles_listed(self): + """No findings when iam.roles is None (short-circuit).""" + from prowler.providers.aws.services.iam.iam_service import IAM + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + iam.roles = None + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch(f"{CHECK_MODULE}.iam_client", new=iam), + ): + from prowler.providers.aws.services.iam.iam_role_access_not_stale_to_bedrock.iam_role_access_not_stale_to_bedrock import ( + iam_role_access_not_stale_to_bedrock, + ) + + check = iam_role_access_not_stale_to_bedrock() + assert check.execute() == [] + + @mock_aws + def test_role_without_bedrock_permissions(self): + """Role with non-Bedrock services is skipped.""" + from prowler.providers.aws.services.iam.iam_service import IAM, Role + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + + mock_role = Role( + name=IAM_ROLE_NAME, + arn=IAM_ROLE_ARN, + assume_role_policy={}, + is_service_role=False, + attached_policies=[], + inline_policies=[], + ) + iam.roles = [mock_role] + iam.role_last_accessed_services = { + ROLE_DATA: [ + {"ServiceNamespace": "iam", "ServiceName": "IAM"}, + {"ServiceNamespace": "s3", "ServiceName": "S3"}, + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch(f"{CHECK_MODULE}.iam_client", new=iam), + ): + from prowler.providers.aws.services.iam.iam_role_access_not_stale_to_bedrock.iam_role_access_not_stale_to_bedrock import ( + iam_role_access_not_stale_to_bedrock, + ) + + check = iam_role_access_not_stale_to_bedrock() + assert len(check.execute()) == 0 + + @mock_aws + def test_role_bedrock_access_with_string_date(self): + """PASS when LastAuthenticated is an ISO string instead of a datetime object.""" + from prowler.providers.aws.services.iam.iam_service import IAM, Role + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + + mock_role = Role( + name=IAM_ROLE_NAME, + arn=IAM_ROLE_ARN, + assume_role_policy={}, + is_service_role=False, + attached_policies=[], + inline_policies=[], + ) + iam.roles = [mock_role] + + last_access_date = datetime.now(timezone.utc) - timedelta(days=5) + iam.role_last_accessed_services = { + ROLE_DATA: [ + { + "ServiceNamespace": "bedrock", + "ServiceName": "Amazon Bedrock", + "LastAuthenticated": last_access_date.isoformat(), + }, + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch(f"{CHECK_MODULE}.iam_client", new=iam), + ): + from prowler.providers.aws.services.iam.iam_role_access_not_stale_to_bedrock.iam_role_access_not_stale_to_bedrock import ( + iam_role_access_not_stale_to_bedrock, + ) + + check = iam_role_access_not_stale_to_bedrock() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + + @mock_aws + def test_role_bedrock_access_at_exact_threshold(self): + """PASS when role accessed Bedrock exactly at the 60-day boundary.""" + from prowler.providers.aws.services.iam.iam_service import IAM, Role + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + + mock_role = Role( + name=IAM_ROLE_NAME, + arn=IAM_ROLE_ARN, + assume_role_policy={}, + is_service_role=False, + attached_policies=[], + inline_policies=[], + ) + iam.roles = [mock_role] + + last_authenticated = datetime.now(timezone.utc) - timedelta(days=60) + iam.role_last_accessed_services = { + ROLE_DATA: [ + { + "ServiceNamespace": "bedrock", + "ServiceName": "Amazon Bedrock", + "LastAuthenticated": last_authenticated, + }, + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch(f"{CHECK_MODULE}.iam_client", new=iam), + ): + from prowler.providers.aws.services.iam.iam_role_access_not_stale_to_bedrock.iam_role_access_not_stale_to_bedrock import ( + iam_role_access_not_stale_to_bedrock, + ) + + check = iam_role_access_not_stale_to_bedrock() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert "60 days ago" in result[0].status_extended + assert "threshold: 60 days" in result[0].status_extended + + @mock_aws + def test_role_bedrock_access_one_day_over_threshold(self): + """FAIL when role accessed Bedrock 61 days ago.""" + from prowler.providers.aws.services.iam.iam_service import IAM, Role + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + + mock_role = Role( + name=IAM_ROLE_NAME, + arn=IAM_ROLE_ARN, + assume_role_policy={}, + is_service_role=False, + attached_policies=[], + inline_policies=[], + ) + iam.roles = [mock_role] + + last_authenticated = datetime.now(timezone.utc) - timedelta(days=61) + iam.role_last_accessed_services = { + ROLE_DATA: [ + { + "ServiceNamespace": "bedrock", + "ServiceName": "Amazon Bedrock", + "LastAuthenticated": last_authenticated, + }, + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch(f"{CHECK_MODULE}.iam_client", new=iam), + ): + from prowler.providers.aws.services.iam.iam_role_access_not_stale_to_bedrock.iam_role_access_not_stale_to_bedrock import ( + iam_role_access_not_stale_to_bedrock, + ) + + check = iam_role_access_not_stale_to_bedrock() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "61 days" in result[0].status_extended + assert "threshold: 60 days" in result[0].status_extended + + @mock_aws + def test_custom_threshold_via_audit_config(self): + """Custom threshold from audit_config is respected for roles.""" + from prowler.providers.aws.services.iam.iam_service import IAM, Role + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + iam.audit_config = {"max_unused_bedrock_access_days": 30} + + mock_role = Role( + name=IAM_ROLE_NAME, + arn=IAM_ROLE_ARN, + assume_role_policy={}, + is_service_role=False, + attached_policies=[], + inline_policies=[], + ) + iam.roles = [mock_role] + + last_authenticated = datetime.now(timezone.utc) - timedelta(days=45) + iam.role_last_accessed_services = { + ROLE_DATA: [ + { + "ServiceNamespace": "bedrock", + "ServiceName": "Amazon Bedrock", + "LastAuthenticated": last_authenticated, + }, + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch(f"{CHECK_MODULE}.iam_client", new=iam), + ): + from prowler.providers.aws.services.iam.iam_role_access_not_stale_to_bedrock.iam_role_access_not_stale_to_bedrock import ( + iam_role_access_not_stale_to_bedrock, + ) + + check = iam_role_access_not_stale_to_bedrock() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "45 days" in result[0].status_extended + assert "threshold: 30 days" in result[0].status_extended + @mock_aws def test_role_tags_are_populated(self): """Verify resource_tags are populated from the role object.""" diff --git a/tests/providers/aws/services/iam/iam_user_access_not_stale_to_sagemaker/iam_user_access_not_stale_to_sagemaker_test.py b/tests/providers/aws/services/iam/iam_user_access_not_stale_to_sagemaker/iam_user_access_not_stale_to_sagemaker_test.py new file mode 100644 index 0000000000..0119ac7154 --- /dev/null +++ b/tests/providers/aws/services/iam/iam_user_access_not_stale_to_sagemaker/iam_user_access_not_stale_to_sagemaker_test.py @@ -0,0 +1,623 @@ +from datetime import datetime, timedelta, timezone +from unittest import mock + +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) + +IAM_USER_NAME = "test-user" +IAM_USER_ARN = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:user/{IAM_USER_NAME}" +USER_DATA = (IAM_USER_NAME, IAM_USER_ARN) + +CHECK_MODULE = ( + "prowler.providers.aws.services.iam." + "iam_user_access_not_stale_to_sagemaker.iam_user_access_not_stale_to_sagemaker" +) + + +class Test_iam_user_access_not_stale_to_sagemaker: + @mock_aws + def test_no_users_with_sagemaker_permissions(self): + """No findings when no users have SageMaker in last accessed services.""" + from prowler.providers.aws.services.iam.iam_service import IAM + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + iam.last_accessed_services = {} + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch(f"{CHECK_MODULE}.iam_client", new=iam), + ): + from prowler.providers.aws.services.iam.iam_user_access_not_stale_to_sagemaker.iam_user_access_not_stale_to_sagemaker import ( + iam_user_access_not_stale_to_sagemaker, + ) + + check = iam_user_access_not_stale_to_sagemaker() + assert len(check.execute()) == 0 + + @mock_aws + def test_user_without_sagemaker_permissions(self): + """User with non-SageMaker services is skipped.""" + from prowler.providers.aws.services.iam.iam_service import IAM, User + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + + mock_user = User( + name=IAM_USER_NAME, + arn=IAM_USER_ARN, + attached_policies=[], + inline_policies=[], + ) + iam.users = [mock_user] + iam.last_accessed_services = { + USER_DATA: [ + {"ServiceNamespace": "iam", "ServiceName": "IAM"}, + {"ServiceNamespace": "s3", "ServiceName": "S3"}, + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch(f"{CHECK_MODULE}.iam_client", new=iam), + ): + from prowler.providers.aws.services.iam.iam_user_access_not_stale_to_sagemaker.iam_user_access_not_stale_to_sagemaker import ( + iam_user_access_not_stale_to_sagemaker, + ) + + check = iam_user_access_not_stale_to_sagemaker() + assert len(check.execute()) == 0 + + @mock_aws + def test_user_sagemaker_access_recent(self): + """PASS when user accessed SageMaker within the threshold.""" + from prowler.providers.aws.services.iam.iam_service import IAM, User + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + + mock_user = User( + name=IAM_USER_NAME, + arn=IAM_USER_ARN, + attached_policies=[], + inline_policies=[], + ) + iam.users = [mock_user] + + last_authenticated = datetime.now(timezone.utc) - timedelta(days=10) + iam.last_accessed_services = { + USER_DATA: [ + { + "ServiceNamespace": "sagemaker", + "ServiceName": "Amazon SageMaker", + "LastAuthenticated": last_authenticated, + }, + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch(f"{CHECK_MODULE}.iam_client", new=iam), + ): + from prowler.providers.aws.services.iam.iam_user_access_not_stale_to_sagemaker.iam_user_access_not_stale_to_sagemaker import ( + iam_user_access_not_stale_to_sagemaker, + ) + + check = iam_user_access_not_stale_to_sagemaker() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert "accessed SageMaker" in result[0].status_extended + assert IAM_USER_NAME in result[0].status_extended + assert result[0].resource_id == IAM_USER_NAME + assert result[0].resource_arn == IAM_USER_ARN + assert result[0].region == AWS_REGION_US_EAST_1 + + @mock_aws + def test_user_sagemaker_access_stale(self): + """FAIL when user last accessed SageMaker more than 90 days ago.""" + from prowler.providers.aws.services.iam.iam_service import IAM, User + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + + mock_user = User( + name=IAM_USER_NAME, + arn=IAM_USER_ARN, + attached_policies=[], + inline_policies=[], + ) + iam.users = [mock_user] + + last_authenticated = datetime.now(timezone.utc) - timedelta(days=120) + iam.last_accessed_services = { + USER_DATA: [ + { + "ServiceNamespace": "sagemaker", + "ServiceName": "Amazon SageMaker", + "LastAuthenticated": last_authenticated, + }, + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch(f"{CHECK_MODULE}.iam_client", new=iam), + ): + from prowler.providers.aws.services.iam.iam_user_access_not_stale_to_sagemaker.iam_user_access_not_stale_to_sagemaker import ( + iam_user_access_not_stale_to_sagemaker, + ) + + check = iam_user_access_not_stale_to_sagemaker() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "has not accessed SageMaker" in result[0].status_extended + assert "120 days" in result[0].status_extended + assert IAM_USER_NAME in result[0].status_extended + + @mock_aws + def test_user_sagemaker_never_accessed(self): + """FAIL when user has SageMaker permissions but has never used them.""" + from prowler.providers.aws.services.iam.iam_service import IAM, User + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + + mock_user = User( + name=IAM_USER_NAME, + arn=IAM_USER_ARN, + attached_policies=[], + inline_policies=[], + ) + iam.users = [mock_user] + + iam.last_accessed_services = { + USER_DATA: [ + { + "ServiceNamespace": "sagemaker", + "ServiceName": "Amazon SageMaker", + }, + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch(f"{CHECK_MODULE}.iam_client", new=iam), + ): + from prowler.providers.aws.services.iam.iam_user_access_not_stale_to_sagemaker.iam_user_access_not_stale_to_sagemaker import ( + iam_user_access_not_stale_to_sagemaker, + ) + + check = iam_user_access_not_stale_to_sagemaker() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "has never used them" in result[0].status_extended + + @mock_aws + def test_custom_threshold_via_audit_config(self): + """Custom threshold from audit_config is respected.""" + from prowler.providers.aws.services.iam.iam_service import IAM, User + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + iam.audit_config = {"max_unused_sagemaker_access_days": 30} + + mock_user = User( + name=IAM_USER_NAME, + arn=IAM_USER_ARN, + attached_policies=[], + inline_policies=[], + ) + iam.users = [mock_user] + + last_authenticated = datetime.now(timezone.utc) - timedelta(days=45) + iam.last_accessed_services = { + USER_DATA: [ + { + "ServiceNamespace": "sagemaker", + "ServiceName": "Amazon SageMaker", + "LastAuthenticated": last_authenticated, + }, + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch(f"{CHECK_MODULE}.iam_client", new=iam), + ): + from prowler.providers.aws.services.iam.iam_user_access_not_stale_to_sagemaker.iam_user_access_not_stale_to_sagemaker import ( + iam_user_access_not_stale_to_sagemaker, + ) + + check = iam_user_access_not_stale_to_sagemaker() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "45 days" in result[0].status_extended + assert "threshold: 30 days" in result[0].status_extended + + @mock_aws + def test_user_sagemaker_access_at_exact_threshold(self): + """PASS when user accessed SageMaker exactly at the 90-day boundary.""" + from prowler.providers.aws.services.iam.iam_service import IAM, User + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + + mock_user = User( + name=IAM_USER_NAME, + arn=IAM_USER_ARN, + attached_policies=[], + inline_policies=[], + ) + iam.users = [mock_user] + + last_authenticated = datetime.now(timezone.utc) - timedelta(days=90) + iam.last_accessed_services = { + USER_DATA: [ + { + "ServiceNamespace": "sagemaker", + "ServiceName": "Amazon SageMaker", + "LastAuthenticated": last_authenticated, + }, + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch(f"{CHECK_MODULE}.iam_client", new=iam), + ): + from prowler.providers.aws.services.iam.iam_user_access_not_stale_to_sagemaker.iam_user_access_not_stale_to_sagemaker import ( + iam_user_access_not_stale_to_sagemaker, + ) + + check = iam_user_access_not_stale_to_sagemaker() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert "90 days ago" in result[0].status_extended + assert "threshold: 90 days" in result[0].status_extended + + @mock_aws + def test_user_sagemaker_access_one_day_over_threshold(self): + """FAIL when user accessed SageMaker 91 days ago.""" + from prowler.providers.aws.services.iam.iam_service import IAM, User + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + + mock_user = User( + name=IAM_USER_NAME, + arn=IAM_USER_ARN, + attached_policies=[], + inline_policies=[], + ) + iam.users = [mock_user] + + last_authenticated = datetime.now(timezone.utc) - timedelta(days=91) + iam.last_accessed_services = { + USER_DATA: [ + { + "ServiceNamespace": "sagemaker", + "ServiceName": "Amazon SageMaker", + "LastAuthenticated": last_authenticated, + }, + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch(f"{CHECK_MODULE}.iam_client", new=iam), + ): + from prowler.providers.aws.services.iam.iam_user_access_not_stale_to_sagemaker.iam_user_access_not_stale_to_sagemaker import ( + iam_user_access_not_stale_to_sagemaker, + ) + + check = iam_user_access_not_stale_to_sagemaker() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "91 days" in result[0].status_extended + assert "threshold: 90 days" in result[0].status_extended + + @mock_aws + def test_user_sagemaker_access_with_string_date(self): + """PASS when LastAuthenticated is an ISO string instead of a datetime object.""" + from prowler.providers.aws.services.iam.iam_service import IAM, User + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + + mock_user = User( + name=IAM_USER_NAME, + arn=IAM_USER_ARN, + attached_policies=[], + inline_policies=[], + ) + iam.users = [mock_user] + + last_access_date = datetime.now(timezone.utc) - timedelta(days=5) + iam.last_accessed_services = { + USER_DATA: [ + { + "ServiceNamespace": "sagemaker", + "ServiceName": "Amazon SageMaker", + "LastAuthenticated": last_access_date.isoformat(), + }, + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch(f"{CHECK_MODULE}.iam_client", new=iam), + ): + from prowler.providers.aws.services.iam.iam_user_access_not_stale_to_sagemaker.iam_user_access_not_stale_to_sagemaker import ( + iam_user_access_not_stale_to_sagemaker, + ) + + check = iam_user_access_not_stale_to_sagemaker() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + + @mock_aws + def test_user_tags_are_populated(self): + """Verify resource_tags are populated from the user object.""" + from prowler.providers.aws.services.iam.iam_service import IAM, User + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + + user_tags = [{"Key": "Environment", "Value": "production"}] + mock_user = User( + name=IAM_USER_NAME, + arn=IAM_USER_ARN, + attached_policies=[], + inline_policies=[], + tags=user_tags, + ) + iam.users = [mock_user] + + last_authenticated = datetime.now(timezone.utc) - timedelta(days=10) + iam.last_accessed_services = { + USER_DATA: [ + { + "ServiceNamespace": "sagemaker", + "ServiceName": "Amazon SageMaker", + "LastAuthenticated": last_authenticated, + }, + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch(f"{CHECK_MODULE}.iam_client", new=iam), + ): + from prowler.providers.aws.services.iam.iam_user_access_not_stale_to_sagemaker.iam_user_access_not_stale_to_sagemaker import ( + iam_user_access_not_stale_to_sagemaker, + ) + + check = iam_user_access_not_stale_to_sagemaker() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].resource_tags == user_tags + + @mock_aws + def test_multiple_users_mixed_results(self): + """Multiple users: one recent (PASS), one stale (FAIL), one without SageMaker (skipped).""" + from prowler.providers.aws.services.iam.iam_service import IAM, User + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + + recent_user_name = "recent-user" + recent_user_arn = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:user/{recent_user_name}" + stale_user_name = "stale-user" + stale_user_arn = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:user/{stale_user_name}" + no_sagemaker_user_name = "no-sagemaker-user" + no_sagemaker_user_arn = ( + f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:user/{no_sagemaker_user_name}" + ) + + iam.users = [ + User( + name=recent_user_name, + arn=recent_user_arn, + attached_policies=[], + inline_policies=[], + ), + User( + name=stale_user_name, + arn=stale_user_arn, + attached_policies=[], + inline_policies=[], + ), + User( + name=no_sagemaker_user_name, + arn=no_sagemaker_user_arn, + attached_policies=[], + inline_policies=[], + ), + ] + + recent_access = datetime.now(timezone.utc) - timedelta(days=10) + stale_access = datetime.now(timezone.utc) - timedelta(days=120) + iam.last_accessed_services = { + (recent_user_name, recent_user_arn): [ + { + "ServiceNamespace": "sagemaker", + "ServiceName": "Amazon SageMaker", + "LastAuthenticated": recent_access, + }, + ], + (stale_user_name, stale_user_arn): [ + { + "ServiceNamespace": "sagemaker", + "ServiceName": "Amazon SageMaker", + "LastAuthenticated": stale_access, + }, + ], + (no_sagemaker_user_name, no_sagemaker_user_arn): [ + {"ServiceNamespace": "s3", "ServiceName": "S3"}, + ], + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch(f"{CHECK_MODULE}.iam_client", new=iam), + ): + from prowler.providers.aws.services.iam.iam_user_access_not_stale_to_sagemaker.iam_user_access_not_stale_to_sagemaker import ( + iam_user_access_not_stale_to_sagemaker, + ) + + check = iam_user_access_not_stale_to_sagemaker() + result = check.execute() + + assert len(result) == 2 + results_by_id = {r.resource_id: r for r in result} + + assert results_by_id[recent_user_name].status == "PASS" + assert ( + "accessed SageMaker" in results_by_id[recent_user_name].status_extended + ) + + assert results_by_id[stale_user_name].status == "FAIL" + assert ( + "has not accessed SageMaker" + in results_by_id[stale_user_name].status_extended + ) + assert "120 days" in results_by_id[stale_user_name].status_extended + + assert no_sagemaker_user_name not in results_by_id + + @mock_aws + def test_user_arn_not_in_users_list(self): + """No findings when last_accessed_services entries do not match any iam.users.""" + from prowler.providers.aws.services.iam.iam_service import IAM + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + iam.users = [] + + last_authenticated = datetime.now(timezone.utc) - timedelta(days=10) + iam.last_accessed_services = { + USER_DATA: [ + { + "ServiceNamespace": "sagemaker", + "ServiceName": "Amazon SageMaker", + "LastAuthenticated": last_authenticated, + }, + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch(f"{CHECK_MODULE}.iam_client", new=iam), + ): + from prowler.providers.aws.services.iam.iam_user_access_not_stale_to_sagemaker.iam_user_access_not_stale_to_sagemaker import ( + iam_user_access_not_stale_to_sagemaker, + ) + + check = iam_user_access_not_stale_to_sagemaker() + assert check.execute() == [] + + @mock_aws + def test_sagemaker_among_multiple_services(self): + """SageMaker entry is correctly found when mixed with other services.""" + from prowler.providers.aws.services.iam.iam_service import IAM, User + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + + mock_user = User( + name=IAM_USER_NAME, + arn=IAM_USER_ARN, + attached_policies=[], + inline_policies=[], + ) + iam.users = [mock_user] + + last_authenticated = datetime.now(timezone.utc) - timedelta(days=15) + iam.last_accessed_services = { + USER_DATA: [ + {"ServiceNamespace": "iam", "ServiceName": "IAM"}, + {"ServiceNamespace": "s3", "ServiceName": "S3"}, + { + "ServiceNamespace": "sagemaker", + "ServiceName": "Amazon SageMaker", + "LastAuthenticated": last_authenticated, + }, + {"ServiceNamespace": "ec2", "ServiceName": "EC2"}, + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch(f"{CHECK_MODULE}.iam_client", new=iam), + ): + from prowler.providers.aws.services.iam.iam_user_access_not_stale_to_sagemaker.iam_user_access_not_stale_to_sagemaker import ( + iam_user_access_not_stale_to_sagemaker, + ) + + check = iam_user_access_not_stale_to_sagemaker() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert "accessed SageMaker" in result[0].status_extended + assert "15 days ago" in result[0].status_extended diff --git a/tests/providers/aws/services/s3/s3_bucket_shadow_resource_vulnerability/s3_bucket_shadow_resource_vulnerability_test.py b/tests/providers/aws/services/s3/s3_bucket_shadow_resource_vulnerability/s3_bucket_shadow_resource_vulnerability_test.py index 9b80d6db35..4b435f0f59 100644 --- a/tests/providers/aws/services/s3/s3_bucket_shadow_resource_vulnerability/s3_bucket_shadow_resource_vulnerability_test.py +++ b/tests/providers/aws/services/s3/s3_bucket_shadow_resource_vulnerability/s3_bucket_shadow_resource_vulnerability_test.py @@ -93,6 +93,8 @@ class Test_s3_bucket_shadow_resource_vulnerability: @mock_aws def test_bucket_not_predictable(self): + # A bucket whose name does not match any predictable service pattern + # is not a shadow resource and must not produce any finding. aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) aws_provider.identity.identity_arn = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root" @@ -113,6 +115,55 @@ class Test_s3_bucket_shadow_resource_vulnerability: s3_client.provider = aws_provider s3_client._head_bucket = mock.MagicMock(return_value=False) + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_shadow_resource_vulnerability.s3_bucket_shadow_resource_vulnerability.s3_client", + new=s3_client, + ), + ): + from prowler.providers.aws.services.s3.s3_bucket_shadow_resource_vulnerability.s3_bucket_shadow_resource_vulnerability import ( + s3_bucket_shadow_resource_vulnerability, + ) + + check = s3_bucket_shadow_resource_vulnerability() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_only_predictable_bucket_reported_among_many(self): + # With a mix of buckets, only the one matching a predictable pattern + # must produce a finding; the rest must be silent. + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + aws_provider.identity.identity_arn = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root" + + predictable_bucket = f"sagemaker-{AWS_REGION_US_EAST_1}-{AWS_ACCOUNT_NUMBER}" + plain_buckets = [ + "config-bucket-data", + "my-app-data-bucket", + "guardduty-findings-store", + ] + + s3_client = mock.MagicMock() + s3_client.audited_canonical_id = AWS_ACCOUNT_NUMBER + s3_client.audited_partition = "aws" + s3_client.buckets = { + name: Bucket( + name=name, + arn=f"arn:aws:s3:::{name}", + region=AWS_REGION_US_EAST_1, + owner_id=AWS_ACCOUNT_NUMBER, + tags=[], + ) + for name in [predictable_bucket, *plain_buckets] + } + s3_client.provider = aws_provider + s3_client._head_bucket = mock.MagicMock(return_value=False) + with ( mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -132,14 +183,10 @@ class Test_s3_bucket_shadow_resource_vulnerability: assert len(result) == 1 report = result[0] - - # Test all report attributes assert report.status == "PASS" - assert report.region == AWS_REGION_US_EAST_1 - assert report.resource_id == bucket_name - assert report.resource_arn == f"arn:aws:s3:::{bucket_name}" - assert report.resource_tags == [{"Key": "Project", "Value": "test-project"}] - assert "is not a known shadow resource" in report.status_extended + assert report.resource_id == predictable_bucket + assert "SageMaker" in report.status_extended + assert "is correctly owned by the audited account" in report.status_extended @mock_aws def test_shadow_resource_in_other_account(self): diff --git a/tests/providers/aws/services/sagemaker/sagemaker_domain_sso_configured/sagemaker_domain_sso_configured_test.py b/tests/providers/aws/services/sagemaker/sagemaker_domain_sso_configured/sagemaker_domain_sso_configured_test.py new file mode 100644 index 0000000000..0026a568c0 --- /dev/null +++ b/tests/providers/aws/services/sagemaker/sagemaker_domain_sso_configured/sagemaker_domain_sso_configured_test.py @@ -0,0 +1,234 @@ +from unittest import mock + +from prowler.providers.aws.services.sagemaker.sagemaker_service import Domain +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_EU_WEST_1, + set_mocked_aws_provider, +) + +test_domain_name = "test-domain" +test_domain_id = "d-testdomain123" +domain_arn = f"arn:aws:sagemaker:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:domain/{test_domain_id}" +test_sso_instance_id = "app-test-instance-id" +test_sso_application_arn = ( + f"arn:aws:sso::{AWS_ACCOUNT_NUMBER}:application/sagemaker/apl-test" +) + + +class Test_sagemaker_domain_sso_configured: + def test_no_domains(self): + sagemaker_client = mock.MagicMock + sagemaker_client.sagemaker_domains = [] + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_domain_sso_configured.sagemaker_domain_sso_configured.sagemaker_client", + sagemaker_client, + ), + ): + from prowler.providers.aws.services.sagemaker.sagemaker_domain_sso_configured.sagemaker_domain_sso_configured import ( + sagemaker_domain_sso_configured, + ) + + check = sagemaker_domain_sso_configured() + result = check.execute() + assert len(result) == 0 + + def test_domain_sso_configured_with_instance_id(self): + sagemaker_client = mock.MagicMock + sagemaker_client.sagemaker_domains = [ + Domain( + domain_id=test_domain_id, + name=test_domain_name, + arn=domain_arn, + region=AWS_REGION_EU_WEST_1, + auth_mode="SSO", + single_sign_on_managed_application_instance_id=test_sso_instance_id, + ) + ] + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_domain_sso_configured.sagemaker_domain_sso_configured.sagemaker_client", + sagemaker_client, + ), + ): + from prowler.providers.aws.services.sagemaker.sagemaker_domain_sso_configured.sagemaker_domain_sso_configured import ( + sagemaker_domain_sso_configured, + ) + + check = sagemaker_domain_sso_configured() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"SageMaker domain {test_domain_name} is configured with SSO authentication and is associated with an IAM Identity Center instance." + ) + assert result[0].resource_id == test_domain_name + assert result[0].resource_arn == domain_arn + + def test_domain_sso_configured_with_application_arn(self): + sagemaker_client = mock.MagicMock + sagemaker_client.sagemaker_domains = [ + Domain( + domain_id=test_domain_id, + name=test_domain_name, + arn=domain_arn, + region=AWS_REGION_EU_WEST_1, + auth_mode="SSO", + single_sign_on_application_arn=test_sso_application_arn, + ) + ] + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_domain_sso_configured.sagemaker_domain_sso_configured.sagemaker_client", + sagemaker_client, + ), + ): + from prowler.providers.aws.services.sagemaker.sagemaker_domain_sso_configured.sagemaker_domain_sso_configured import ( + sagemaker_domain_sso_configured, + ) + + check = sagemaker_domain_sso_configured() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"SageMaker domain {test_domain_name} is configured with SSO authentication and is associated with an IAM Identity Center instance." + ) + + def test_domain_sso_without_identity_center(self): + sagemaker_client = mock.MagicMock + sagemaker_client.sagemaker_domains = [ + Domain( + domain_id=test_domain_id, + name=test_domain_name, + arn=domain_arn, + region=AWS_REGION_EU_WEST_1, + auth_mode="SSO", + ) + ] + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_domain_sso_configured.sagemaker_domain_sso_configured.sagemaker_client", + sagemaker_client, + ), + ): + from prowler.providers.aws.services.sagemaker.sagemaker_domain_sso_configured.sagemaker_domain_sso_configured import ( + sagemaker_domain_sso_configured, + ) + + check = sagemaker_domain_sso_configured() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"SageMaker domain {test_domain_name} is configured with SSO authentication but is not associated with an IAM Identity Center instance." + ) + assert result[0].resource_id == test_domain_name + assert result[0].resource_arn == domain_arn + + def test_domain_iam_mode(self): + sagemaker_client = mock.MagicMock + sagemaker_client.sagemaker_domains = [ + Domain( + domain_id=test_domain_id, + name=test_domain_name, + arn=domain_arn, + region=AWS_REGION_EU_WEST_1, + auth_mode="IAM", + ) + ] + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_domain_sso_configured.sagemaker_domain_sso_configured.sagemaker_client", + sagemaker_client, + ), + ): + from prowler.providers.aws.services.sagemaker.sagemaker_domain_sso_configured.sagemaker_domain_sso_configured import ( + sagemaker_domain_sso_configured, + ) + + check = sagemaker_domain_sso_configured() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"SageMaker domain {test_domain_name} is not configured with SSO authentication, current mode is IAM." + ) + assert result[0].resource_id == test_domain_name + assert result[0].resource_arn == domain_arn + + def test_domain_auth_mode_unknown(self): + sagemaker_client = mock.MagicMock + sagemaker_client.sagemaker_domains = [ + Domain( + domain_id=test_domain_id, + name=test_domain_name, + arn=domain_arn, + region=AWS_REGION_EU_WEST_1, + ) + ] + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_domain_sso_configured.sagemaker_domain_sso_configured.sagemaker_client", + sagemaker_client, + ), + ): + from prowler.providers.aws.services.sagemaker.sagemaker_domain_sso_configured.sagemaker_domain_sso_configured import ( + sagemaker_domain_sso_configured, + ) + + check = sagemaker_domain_sso_configured() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"SageMaker domain {test_domain_name} is not configured with SSO authentication, current mode is unknown." + ) diff --git a/tests/providers/aws/services/sagemaker/sagemaker_models_registry_in_use/sagemaker_models_registry_in_use_test.py b/tests/providers/aws/services/sagemaker/sagemaker_models_registry_in_use/sagemaker_models_registry_in_use_test.py new file mode 100644 index 0000000000..c192217de3 --- /dev/null +++ b/tests/providers/aws/services/sagemaker/sagemaker_models_registry_in_use/sagemaker_models_registry_in_use_test.py @@ -0,0 +1,156 @@ +from unittest import mock + +from prowler.providers.aws.services.sagemaker.sagemaker_service import ModelRegistry +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_EU_WEST_1, + set_mocked_aws_provider, +) + +registry_arn = f"arn:aws:sagemaker:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:model-registry/unknown" + + +class Test_sagemaker_models_registry_in_use: + def test_no_registries(self): + sagemaker_client = mock.MagicMock + sagemaker_client.sagemaker_model_registries = [] + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_models_registry_in_use.sagemaker_models_registry_in_use.sagemaker_client", + sagemaker_client, + ), + ): + from prowler.providers.aws.services.sagemaker.sagemaker_models_registry_in_use.sagemaker_models_registry_in_use import ( + sagemaker_models_registry_in_use, + ) + + check = sagemaker_models_registry_in_use() + result = check.execute() + assert len(result) == 0 + + def test_registry_no_groups(self): + sagemaker_client = mock.MagicMock + sagemaker_client.sagemaker_model_registries = [ + ModelRegistry( + name="SageMaker Model Registry", + arn=registry_arn, + region=AWS_REGION_EU_WEST_1, + has_groups=False, + has_approved_packages=False, + ) + ] + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_models_registry_in_use.sagemaker_models_registry_in_use.sagemaker_client", + sagemaker_client, + ), + ): + from prowler.providers.aws.services.sagemaker.sagemaker_models_registry_in_use.sagemaker_models_registry_in_use import ( + sagemaker_models_registry_in_use, + ) + + check = sagemaker_models_registry_in_use() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"SageMaker Model Registry in region {AWS_REGION_EU_WEST_1} has no Model Package Groups." + ) + assert result[0].resource_id == "SageMaker Model Registry" + assert result[0].resource_arn == registry_arn + assert result[0].region == AWS_REGION_EU_WEST_1 + + def test_registry_groups_no_approved_packages(self): + sagemaker_client = mock.MagicMock + sagemaker_client.sagemaker_model_registries = [ + ModelRegistry( + name="SageMaker Model Registry", + arn=registry_arn, + region=AWS_REGION_EU_WEST_1, + has_groups=True, + has_approved_packages=False, + ) + ] + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_models_registry_in_use.sagemaker_models_registry_in_use.sagemaker_client", + sagemaker_client, + ), + ): + from prowler.providers.aws.services.sagemaker.sagemaker_models_registry_in_use.sagemaker_models_registry_in_use import ( + sagemaker_models_registry_in_use, + ) + + check = sagemaker_models_registry_in_use() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"SageMaker Model Registry in region {AWS_REGION_EU_WEST_1} has Model Package Groups but no approved model packages." + ) + assert result[0].resource_id == "SageMaker Model Registry" + assert result[0].resource_arn == registry_arn + assert result[0].region == AWS_REGION_EU_WEST_1 + + def test_registry_with_approved_packages(self): + sagemaker_client = mock.MagicMock + sagemaker_client.sagemaker_model_registries = [ + ModelRegistry( + name="SageMaker Model Registry", + arn=registry_arn, + region=AWS_REGION_EU_WEST_1, + has_groups=True, + has_approved_packages=True, + ) + ] + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_models_registry_in_use.sagemaker_models_registry_in_use.sagemaker_client", + sagemaker_client, + ), + ): + from prowler.providers.aws.services.sagemaker.sagemaker_models_registry_in_use.sagemaker_models_registry_in_use import ( + sagemaker_models_registry_in_use, + ) + + check = sagemaker_models_registry_in_use() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"SageMaker Model Registry in region {AWS_REGION_EU_WEST_1} has at least one approved model package." + ) + assert result[0].resource_id == "SageMaker Model Registry" + assert result[0].resource_arn == registry_arn + assert result[0].region == AWS_REGION_EU_WEST_1 diff --git a/tests/providers/aws/services/sagemaker/sagemaker_service_test.py b/tests/providers/aws/services/sagemaker/sagemaker_service_test.py index 4213abd650..d49a506fd9 100644 --- a/tests/providers/aws/services/sagemaker/sagemaker_service_test.py +++ b/tests/providers/aws/services/sagemaker/sagemaker_service_test.py @@ -13,6 +13,11 @@ from tests.providers.aws.utils import ( set_mocked_aws_provider, ) +test_model_package_group_name = "test-model-package-group" +test_model_package_group_arn = f"arn:aws:sagemaker:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:model-package-group/{test_model_package_group_name}" +test_model_package_name = "test-model-package" +test_model_package_arn = f"arn:aws:sagemaker:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:model-package/{test_model_package_name}/1" + test_notebook_instance = "test-notebook-instance" notebook_instance_arn = f"arn:aws:sagemaker:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:notebook-instance/{test_notebook_instance}" test_model = "test-model" @@ -26,6 +31,13 @@ kms_key_id = str(uuid4()) endpoint_config_name = "endpoint-config-test" endpoint_config_arn = f"arn:aws:sagemaker:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:endpoint-config/{endpoint_config_name}" prod_variant_name = "Variant1" +test_domain_name = "test-domain" +test_domain_id = "d-testdomain123" +test_domain_arn = f"arn:aws:sagemaker:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:domain/{test_domain_id}" +test_sso_instance_id = "app-test-instance-id" +test_sso_application_arn = ( + f"arn:aws:sso::{AWS_ACCOUNT_NUMBER}:application/sagemaker/apl-test" +) make_api_call = botocore.client.BaseClient._make_api_call @@ -87,6 +99,25 @@ def mock_make_api_call(self, operation_name, kwarg): "EnableNetworkIsolation": True, "EnableInterContainerTrafficEncryption": True, } + if operation_name == "ListModelPackageGroups": + return { + "ModelPackageGroupSummaryList": [ + { + "ModelPackageGroupName": test_model_package_group_name, + "ModelPackageGroupArn": test_model_package_group_arn, + }, + ] + } + if operation_name == "ListModelPackages": + return { + "ModelPackageSummaryList": [ + { + "ModelPackageName": test_model_package_name, + "ModelPackageArn": test_model_package_arn, + "ModelApprovalStatus": "Approved", + }, + ] + } if operation_name == "ListTags": return { "Tags": [ @@ -115,6 +146,25 @@ def mock_make_api_call(self, operation_name, kwarg): }, ] } + if operation_name == "ListDomains": + return { + "Domains": [ + { + "DomainId": test_domain_id, + "DomainName": test_domain_name, + "DomainArn": test_domain_arn, + }, + ], + } + if operation_name == "DescribeDomain": + return { + "DomainId": test_domain_id, + "DomainName": test_domain_name, + "DomainArn": test_domain_arn, + "AuthMode": "SSO", + "SingleSignOnManagedApplicationInstanceId": test_sso_instance_id, + "SingleSignOnApplicationArn": test_sso_application_arn, + } return make_api_call(self, operation_name, kwarg) @@ -249,6 +299,33 @@ class Test_SageMaker_Service: else: assert prod_variant.initial_instance_count == 2 + # Test SageMaker list domains + def test_list_domains(self): + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + sagemaker = SageMaker(aws_provider) + assert len(sagemaker.sagemaker_domains) == 1 + assert sagemaker.sagemaker_domains[0].domain_id == test_domain_id + assert sagemaker.sagemaker_domains[0].name == test_domain_name + assert sagemaker.sagemaker_domains[0].arn == test_domain_arn + assert sagemaker.sagemaker_domains[0].region == AWS_REGION_EU_WEST_1 + + # Test SageMaker describe domain + def test_describe_domain(self): + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + sagemaker = SageMaker(aws_provider) + assert len(sagemaker.sagemaker_domains) == 1 + assert sagemaker.sagemaker_domains[0].auth_mode == "SSO" + assert ( + sagemaker.sagemaker_domains[ + 0 + ].single_sign_on_managed_application_instance_id + == test_sso_instance_id + ) + assert ( + sagemaker.sagemaker_domains[0].single_sign_on_application_arn + == test_sso_application_arn + ) + # Test SageMaker _list_tags_for_resource def test_list_tags_for_resource_calls_client(self): """Test that _list_tags_for_resource calls the correct AWS client and updates the resource.""" @@ -312,14 +389,47 @@ class Test_SageMaker_Service: patch( "prowler.providers.aws.services.sagemaker.sagemaker_service.SageMaker._list_endpoint_configs" ), + patch( + "prowler.providers.aws.services.sagemaker.sagemaker_service.SageMaker._list_domains" + ), ): sagemaker_service = SageMaker(audit_info) # Check that __threading_call__ was called for _list_tags_for_resource - # (at least 4 calls expected, one for each resource type) + # (one for each resource type: models, notebooks, training jobs, endpoint configs, domains) tag_calls = [ c for c in mock_threading_call.call_args_list if c[0][0] == sagemaker_service._list_tags_for_resource ] - assert len(tag_calls) == 4 + assert len(tag_calls) == 5 + + # Test SageMaker list model package groups + def test_list_model_package_groups(self): + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + sagemaker = SageMaker(aws_provider) + assert len(sagemaker.sagemaker_model_registries) == 1 + registry = sagemaker.sagemaker_model_registries[0] + assert registry.region == AWS_REGION_EU_WEST_1 + assert registry.has_groups is True + assert registry.has_approved_packages is True + + def test_list_model_package_groups_access_denied(self): + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + def mock_access_denied(self, operation_name, kwarg): + if operation_name == "ListModelPackageGroups": + raise botocore.exceptions.ClientError( + { + "Error": { + "Code": "AccessDeniedException", + "Message": "User is not authorized to perform sagemaker:ListModelPackageGroups", + } + }, + "ListModelPackageGroups", + ) + return make_api_call(self, operation_name, kwarg) + + with patch("botocore.client.BaseClient._make_api_call", new=mock_access_denied): + sagemaker = SageMaker(aws_provider) + assert sagemaker.sagemaker_model_registries == [] diff --git a/tests/providers/aws/services/ses/ses_identity_dkim_enabled/ses_identity_dkim_enabled_test.py b/tests/providers/aws/services/ses/ses_identity_dkim_enabled/ses_identity_dkim_enabled_test.py new file mode 100644 index 0000000000..be4fda3cf4 --- /dev/null +++ b/tests/providers/aws/services/ses/ses_identity_dkim_enabled/ses_identity_dkim_enabled_test.py @@ -0,0 +1,328 @@ +from unittest import mock + +import botocore +from boto3 import client +from moto import mock_aws + +from prowler.providers.aws.services.ses.ses_service import SES +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_EU_WEST_1, + set_mocked_aws_provider, +) + +make_api_call = botocore.client.BaseClient._make_api_call + + +def mock_make_api_call_dkim_pass(self, operation_name, kwarg): + if operation_name == "ListEmailIdentities": + return { + "EmailIdentities": [ + { + "IdentityType": "DOMAIN", + "IdentityName": "test-domain-dkim-pass", + } + ], + } + elif operation_name == "GetEmailIdentity": + return { + "Policies": {}, + "Tags": [], + "DkimAttributes": { + "Status": "SUCCESS", + "SigningEnabled": True, + "SigningAttributesOrigin": "AWS_SES", + }, + } + return make_api_call(self, operation_name, kwarg) + + +def mock_make_api_call_dkim_fail_not_started(self, operation_name, kwarg): + if operation_name == "ListEmailIdentities": + return { + "EmailIdentities": [ + { + "IdentityType": "DOMAIN", + "IdentityName": "test-domain-dkim-not-started", + } + ], + } + elif operation_name == "GetEmailIdentity": + return { + "Policies": {}, + "Tags": [], + "DkimAttributes": { + "Status": "NOT_STARTED", + "SigningEnabled": False, + "SigningAttributesOrigin": "AWS_SES", + }, + } + return make_api_call(self, operation_name, kwarg) + + +def mock_make_api_call_dkim_fail_failed(self, operation_name, kwarg): + if operation_name == "ListEmailIdentities": + return { + "EmailIdentities": [ + { + "IdentityType": "DOMAIN", + "IdentityName": "test-domain-dkim-failed", + } + ], + } + elif operation_name == "GetEmailIdentity": + return { + "Policies": {}, + "Tags": [], + "DkimAttributes": { + "Status": "FAILED", + "SigningEnabled": False, + "SigningAttributesOrigin": "AWS_SES", + }, + } + return make_api_call(self, operation_name, kwarg) + + +def mock_make_api_call_dkim_pending(self, operation_name, kwarg): + if operation_name == "ListEmailIdentities": + return { + "EmailIdentities": [ + { + "IdentityType": "DOMAIN", + "IdentityName": "test-domain-dkim-pending", + } + ], + } + elif operation_name == "GetEmailIdentity": + return { + "Policies": {}, + "Tags": [], + "DkimAttributes": { + "Status": "PENDING", + "SigningEnabled": False, + "SigningAttributesOrigin": "AWS_SES", + }, + } + return make_api_call(self, operation_name, kwarg) + + +def mock_make_api_call_dkim_success_not_enabled(self, operation_name, kwarg): + if operation_name == "ListEmailIdentities": + return { + "EmailIdentities": [ + { + "IdentityType": "DOMAIN", + "IdentityName": "test-domain-dkim-verified-not-signed", + } + ], + } + elif operation_name == "GetEmailIdentity": + return { + "Policies": {}, + "Tags": [], + "DkimAttributes": { + "Status": "SUCCESS", + "SigningEnabled": False, + "SigningAttributesOrigin": "AWS_SES", + }, + } + return make_api_call(self, operation_name, kwarg) + + +class Test_ses_identity_dkim_enabled: + @mock_aws + def test_no_identities(self): + client("sesv2", region_name=AWS_REGION_EU_WEST_1) + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.ses.ses_identity_dkim_enabled.ses_identity_dkim_enabled.ses_client", + new=SES(aws_provider), + ), + ): + from prowler.providers.aws.services.ses.ses_identity_dkim_enabled.ses_identity_dkim_enabled import ( + ses_identity_dkim_enabled, + ) + + check = ses_identity_dkim_enabled() + result = check.execute() + assert len(result) == 0 + + @mock_aws + @mock.patch( + "botocore.client.BaseClient._make_api_call", + new=mock_make_api_call_dkim_pass, + ) + def test_identity_dkim_enabled_and_verified(self): + client("sesv2", region_name=AWS_REGION_EU_WEST_1) + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.ses.ses_identity_dkim_enabled.ses_identity_dkim_enabled.ses_client", + new=SES(aws_provider), + ), + ): + from prowler.providers.aws.services.ses.ses_identity_dkim_enabled.ses_identity_dkim_enabled import ( + ses_identity_dkim_enabled, + ) + + check = ses_identity_dkim_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "SES identity test-domain-dkim-pass has DKIM signing enabled and verified." + ) + assert result[0].resource_id == "test-domain-dkim-pass" + assert ( + result[0].resource_arn + == f"arn:aws:ses:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:identity/test-domain-dkim-pass" + ) + assert result[0].region == AWS_REGION_EU_WEST_1 + + @mock_aws + @mock.patch( + "botocore.client.BaseClient._make_api_call", + new=mock_make_api_call_dkim_fail_not_started, + ) + def test_identity_dkim_not_started(self): + client("sesv2", region_name=AWS_REGION_EU_WEST_1) + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.ses.ses_identity_dkim_enabled.ses_identity_dkim_enabled.ses_client", + new=SES(aws_provider), + ), + ): + from prowler.providers.aws.services.ses.ses_identity_dkim_enabled.ses_identity_dkim_enabled import ( + ses_identity_dkim_enabled, + ) + + check = ses_identity_dkim_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "SES identity test-domain-dkim-not-started has DKIM signing not verified (status: NOT_STARTED)." + ) + assert result[0].resource_id == "test-domain-dkim-not-started" + assert result[0].region == AWS_REGION_EU_WEST_1 + + @mock_aws + @mock.patch( + "botocore.client.BaseClient._make_api_call", + new=mock_make_api_call_dkim_fail_failed, + ) + def test_identity_dkim_failed(self): + client("sesv2", region_name=AWS_REGION_EU_WEST_1) + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.ses.ses_identity_dkim_enabled.ses_identity_dkim_enabled.ses_client", + new=SES(aws_provider), + ), + ): + from prowler.providers.aws.services.ses.ses_identity_dkim_enabled.ses_identity_dkim_enabled import ( + ses_identity_dkim_enabled, + ) + + check = ses_identity_dkim_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "SES identity test-domain-dkim-failed has DKIM signing failed verification." + ) + assert result[0].resource_id == "test-domain-dkim-failed" + assert result[0].region == AWS_REGION_EU_WEST_1 + + @mock_aws + @mock.patch( + "botocore.client.BaseClient._make_api_call", + new=mock_make_api_call_dkim_pending, + ) + def test_identity_dkim_pending(self): + client("sesv2", region_name=AWS_REGION_EU_WEST_1) + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.ses.ses_identity_dkim_enabled.ses_identity_dkim_enabled.ses_client", + new=SES(aws_provider), + ), + ): + from prowler.providers.aws.services.ses.ses_identity_dkim_enabled.ses_identity_dkim_enabled import ( + ses_identity_dkim_enabled, + ) + + check = ses_identity_dkim_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "SES identity test-domain-dkim-pending has DKIM signing not verified (status: PENDING)." + ) + assert result[0].resource_id == "test-domain-dkim-pending" + assert result[0].region == AWS_REGION_EU_WEST_1 + + @mock_aws + @mock.patch( + "botocore.client.BaseClient._make_api_call", + new=mock_make_api_call_dkim_success_not_enabled, + ) + def test_identity_dkim_verified_but_not_enabled(self): + client("sesv2", region_name=AWS_REGION_EU_WEST_1) + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.ses.ses_identity_dkim_enabled.ses_identity_dkim_enabled.ses_client", + new=SES(aws_provider), + ), + ): + from prowler.providers.aws.services.ses.ses_identity_dkim_enabled.ses_identity_dkim_enabled import ( + ses_identity_dkim_enabled, + ) + + check = ses_identity_dkim_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "SES identity test-domain-dkim-verified-not-signed has DKIM verified but signing is disabled." + ) + assert result[0].resource_id == "test-domain-dkim-verified-not-signed" + assert result[0].region == AWS_REGION_EU_WEST_1 diff --git a/tests/providers/aws/services/ses/ses_service_test.py b/tests/providers/aws/services/ses/ses_service_test.py index dc1e6eb710..223ed94365 100644 --- a/tests/providers/aws/services/ses/ses_service_test.py +++ b/tests/providers/aws/services/ses/ses_service_test.py @@ -29,6 +29,11 @@ def mock_make_api_call(self, operation_name, kwarg): "policy1": '{"policy1": "value1"}', }, "Tags": {"tag1": "value1", "tag2": "value2"}, + "DkimAttributes": { + "Status": "SUCCESS", + "SigningEnabled": True, + "SigningAttributesOrigin": "AWS_SES", + }, } return make_api_call(self, operation_name, kwarg) @@ -78,3 +83,6 @@ class Test_SES_Service: assert ses.email_identities[arn].region == AWS_REGION_EU_WEST_1 assert ses.email_identities[arn].policy == {"policy1": "value1"} assert ses.email_identities[arn].tags == {"tag1": "value1", "tag2": "value2"} + assert ses.email_identities[arn].dkim_status == "SUCCESS" + assert ses.email_identities[arn].dkim_signing_attributes_origin == "AWS_SES" + assert ses.email_identities[arn].dkim_signing_enabled is True diff --git a/tests/providers/azure/azure_fixtures.py b/tests/providers/azure/azure_fixtures.py index 51b919e0d6..84d43fd2c3 100644 --- a/tests/providers/azure/azure_fixtures.py +++ b/tests/providers/azure/azure_fixtures.py @@ -8,6 +8,7 @@ from prowler.providers.azure.models import AzureIdentityInfo, AzureRegionConfig AZURE_SUBSCRIPTION_ID = str(uuid4()) AZURE_SUBSCRIPTION_NAME = "Subscription Name" +AZURE_SUBSCRIPTION_DISPLAY = f"{AZURE_SUBSCRIPTION_NAME} ({AZURE_SUBSCRIPTION_ID})" # Azure Identity IDENTITY_ID = "00000000-0000-0000-0000-000000000000" diff --git a/tests/providers/azure/azure_provider_test.py b/tests/providers/azure/azure_provider_test.py index a0c8a5e5d4..0846fdc540 100644 --- a/tests/providers/azure/azure_provider_test.py +++ b/tests/providers/azure/azure_provider_test.py @@ -1,8 +1,10 @@ -from unittest.mock import patch +import asyncio +from unittest.mock import AsyncMock, patch from uuid import uuid4 import pytest from azure.core.credentials import AccessToken +from azure.core.exceptions import HttpResponseError from azure.identity import DefaultAzureCredential from mock import MagicMock @@ -85,6 +87,7 @@ class TestAzureProvider: "python_latest_version": "3.12", "java_latest_version": "17", "recommended_minimal_tls_versions": ["1.2", "1.3"], + "recommended_smb_channel_encryption_algorithms": ["AES-256-GCM"], "vm_backup_min_daily_retention_days": 7, "desired_vm_sku_sizes": [ "Standard_A8_v2", @@ -432,8 +435,29 @@ class TestAzureProvider: ) def test_test_connection_with_ClientAuthenticationError(self): - with pytest.raises(AzureHTTPResponseError) as exception: - tenant_id = str(uuid4()) + tenant_id = str(uuid4()) + error_message = ( + "Authentication failed: Unable to get authority configuration for " + f"https://login.microsoftonline.com/{tenant_id}." + ) + + with ( + patch( + "prowler.providers.azure.azure_provider.AzureProvider.setup_session" + ) as mock_setup_session, + patch( + "prowler.providers.azure.azure_provider.SubscriptionClient" + ) as mock_subscription_client, + pytest.raises(AzureHTTPResponseError) as exception, + ): + mock_setup_session.return_value = MagicMock() + mock_client = MagicMock() + mock_client.subscriptions = MagicMock() + mock_client.subscriptions.list.side_effect = HttpResponseError( + message=error_message + ) + mock_subscription_client.return_value = mock_client + AzureProvider.test_connection( browser_auth=True, tenant_id=tenant_id, @@ -441,9 +465,8 @@ class TestAzureProvider: ) assert exception.type == AzureHTTPResponseError - assert ( - exception.value.args[0] - == f"[2010] Error in HTTP response from Azure - Authentication failed: Unable to get authority configuration for https://login.microsoftonline.com/{tenant_id}. Authority would typically be in a format of https://login.microsoftonline.com/your_tenant or https://tenant_name.ciamlogin.com or https://tenant_name.b2clogin.com/tenant.onmicrosoft.com/policy. Also please double check your tenant name or GUID is correct." + assert exception.value.args[0] == ( + f"[2010] Error in HTTP response from Azure - {error_message}" ) def test_test_connection_without_any_method(self): @@ -527,3 +550,261 @@ class TestAzureProvider: regions = azure_provider.get_regions(subscription_ids=subscription_ids) assert regions == expected_regions + + +class TestAzureProviderSetupIdentitySubscriptions: + """Regression tests ensuring identity.subscriptions preserves every + subscription even when multiple Azure subscriptions share the same + display_name (which is permitted by Azure).""" + + @staticmethod + def _mock_subscription(display_name, subscription_id): + mock_subscription = MagicMock() + mock_subscription.display_name = display_name + mock_subscription.subscription_id = subscription_id + return mock_subscription + + @staticmethod + def _build_subscriptions_client_mock(list_result=None, get_map=None): + """Construct a fully explicit SubscriptionClient mock so the tests do + not depend on MagicMock auto-attribute behavior, which makes the suite + sensitive to shared state across test files.""" + subscriptions_operations = MagicMock() + subscriptions_operations.list = MagicMock(return_value=list_result or []) + if get_map is not None: + subscriptions_operations.get = MagicMock( + side_effect=lambda subscription_id: get_map[subscription_id] + ) + else: + subscriptions_operations.get = MagicMock() + + tenants_operations = MagicMock() + tenants_operations.list = MagicMock(return_value=[]) + + client_instance = MagicMock() + client_instance.subscriptions = subscriptions_operations + client_instance.tenants = tenants_operations + + client_class = MagicMock(return_value=client_instance) + return client_class + + @staticmethod + def _build_provider(): + """Create an AzureProvider instance ready to invoke setup_identity + with auth flags left False so the AAD lookup branches are skipped and + the test focuses on the subscription resolution logic.""" + with patch.object(AzureProvider, "__init__", return_value=None): + azure_provider = AzureProvider() + azure_provider._session = MagicMock() + azure_provider._region_config = AzureRegionConfig( + name="AzureCloud", + authority=None, + base_url="https://management.azure.com", + credential_scopes=["https://management.azure.com/.default"], + ) + return azure_provider + + def test_setup_identity_auto_discovery_preserves_unique_display_names(self): + first_id = str(uuid4()) + second_id = str(uuid4()) + client_class = self._build_subscriptions_client_mock( + list_result=[ + self._mock_subscription("Unique Name One", first_id), + self._mock_subscription("Unique Name Two", second_id), + ] + ) + with patch( + "prowler.providers.azure.azure_provider.SubscriptionClient", + client_class, + ): + azure_provider = self._build_provider() + + identity = azure_provider.setup_identity( + az_cli_auth=False, + sp_env_auth=False, + browser_auth=False, + managed_identity_auth=False, + subscription_ids=[], + client_id=None, + ) + + assert identity.subscriptions == { + first_id: "Unique Name One", + second_id: "Unique Name Two", + } + + def test_setup_identity_auto_discovery_preserves_duplicate_display_names( + self, + ): + shared_name = "Shared Display Name" + first_id = str(uuid4()) + second_id = str(uuid4()) + client_class = self._build_subscriptions_client_mock( + list_result=[ + self._mock_subscription(shared_name, first_id), + self._mock_subscription(shared_name, second_id), + ] + ) + with patch( + "prowler.providers.azure.azure_provider.SubscriptionClient", + client_class, + ): + azure_provider = self._build_provider() + + identity = azure_provider.setup_identity( + az_cli_auth=False, + sp_env_auth=False, + browser_auth=False, + managed_identity_auth=False, + subscription_ids=[], + client_id=None, + ) + + assert identity.subscriptions == { + first_id: shared_name, + second_id: shared_name, + } + + def test_setup_identity_filtered_preserves_unique_display_names(self): + first_id = str(uuid4()) + second_id = str(uuid4()) + client_class = self._build_subscriptions_client_mock( + get_map={ + first_id: self._mock_subscription("Unique Name One", first_id), + second_id: self._mock_subscription("Unique Name Two", second_id), + } + ) + with patch( + "prowler.providers.azure.azure_provider.SubscriptionClient", + client_class, + ): + azure_provider = self._build_provider() + + identity = azure_provider.setup_identity( + az_cli_auth=False, + sp_env_auth=False, + browser_auth=False, + managed_identity_auth=False, + subscription_ids=[first_id, second_id], + client_id=None, + ) + + assert identity.subscriptions == { + first_id: "Unique Name One", + second_id: "Unique Name Two", + } + + def test_setup_identity_filtered_preserves_duplicate_display_names(self): + shared_name = "Shared Display Name" + first_id = str(uuid4()) + second_id = str(uuid4()) + client_class = self._build_subscriptions_client_mock( + get_map={ + first_id: self._mock_subscription(shared_name, first_id), + second_id: self._mock_subscription(shared_name, second_id), + } + ) + with patch( + "prowler.providers.azure.azure_provider.SubscriptionClient", + client_class, + ): + azure_provider = self._build_provider() + + identity = azure_provider.setup_identity( + az_cli_auth=False, + sp_env_auth=False, + browser_auth=False, + managed_identity_auth=False, + subscription_ids=[first_id, second_id], + client_id=None, + ) + + assert identity.subscriptions == { + first_id: shared_name, + second_id: shared_name, + } + + +class TestAzureProviderSetupIdentityEventLoop: + """Regression for the Celery worker scenario where + asyncio.get_event_loop() raised "There is no current event loop in + thread 'MainThread'." on Python 3.12. setup_identity now uses + asyncio.run(), which creates its own loop and must work without a + pre-existing one in the current thread.""" + + @staticmethod + def _mock_subscription(display_name, subscription_id): + mock_subscription = MagicMock() + mock_subscription.display_name = display_name + mock_subscription.subscription_id = subscription_id + return mock_subscription + + @staticmethod + def _build_subscriptions_client_mock(subscriptions): + subscriptions_operations = MagicMock() + subscriptions_operations.list = MagicMock(return_value=subscriptions) + subscriptions_operations.get = MagicMock() + + tenants_operations = MagicMock() + tenants_operations.list = MagicMock(return_value=[]) + + client_instance = MagicMock() + client_instance.subscriptions = subscriptions_operations + client_instance.tenants = tenants_operations + return MagicMock(return_value=client_instance) + + @staticmethod + def _build_provider(): + with patch.object(AzureProvider, "__init__", return_value=None): + azure_provider = AzureProvider() + azure_provider._session = MagicMock() + azure_provider._region_config = AzureRegionConfig( + name="AzureCloud", + authority=None, + base_url="https://management.azure.com", + credential_scopes=["https://management.azure.com/.default"], + ) + return azure_provider + + def test_setup_identity_succeeds_without_active_event_loop(self): + sub_id = str(uuid4()) + subscriptions_client = self._build_subscriptions_client_mock( + [self._mock_subscription("Sub", sub_id)] + ) + + graph_client = MagicMock() + graph_client.domains.get = AsyncMock(return_value=MagicMock(value=[])) + graph_client.me.get = AsyncMock(return_value=None) + + # Simulate the Celery worker state: no event loop registered for the + # current thread. Before the fix this combination triggered + # `RuntimeError: There is no current event loop in thread 'MainThread'.` + # on Python 3.12 from asyncio.get_event_loop(). + asyncio.set_event_loop(None) + try: + with ( + patch( + "prowler.providers.azure.azure_provider.GraphServiceClient", + return_value=graph_client, + ), + patch( + "prowler.providers.azure.azure_provider.SubscriptionClient", + subscriptions_client, + ), + ): + azure_provider = self._build_provider() + identity = azure_provider.setup_identity( + az_cli_auth=False, + sp_env_auth=True, + browser_auth=False, + managed_identity_auth=False, + subscription_ids=[], + client_id="00000000-0000-0000-0000-000000000000", + ) + finally: + # Re-arm a loop for sibling tests that may rely on the default. + asyncio.set_event_loop(asyncio.new_event_loop()) + + assert isinstance(identity, AzureIdentityInfo) + assert identity.subscriptions == {sub_id: "Sub"} + graph_client.domains.get.assert_awaited_once() diff --git a/tests/providers/azure/lib/mutelist/azure_mutelist_test.py b/tests/providers/azure/lib/mutelist/azure_mutelist_test.py index d15faa83ed..24f6d5ef1e 100644 --- a/tests/providers/azure/lib/mutelist/azure_mutelist_test.py +++ b/tests/providers/azure/lib/mutelist/azure_mutelist_test.py @@ -64,10 +64,12 @@ class TestAzureMutelist: finding.status = "FAIL" finding.resource_name = "test_resource" finding.resource_tags = {} - finding.subscription = "subscription_1" + finding.subscription = "12345678-1234-1234-1234-123456789012" assert mutelist.is_finding_muted( - finding, "12345678-1234-1234-1234-123456789012" + finding, + "12345678-1234-1234-1234-123456789012", + "subscription_1", ) def test_is_finding_muted_subscription_id(self): diff --git a/tests/providers/azure/services/aisearch/aisearch_service_public_access_level_is_disabled/aisearch_service_public_access_level_is_disabled_test.py b/tests/providers/azure/services/aisearch/aisearch_service_public_access_level_is_disabled/aisearch_service_public_access_level_is_disabled_test.py index 7e35a2ca36..6f3aebcc00 100644 --- a/tests/providers/azure/services/aisearch/aisearch_service_public_access_level_is_disabled/aisearch_service_public_access_level_is_disabled_test.py +++ b/tests/providers/azure/services/aisearch/aisearch_service_public_access_level_is_disabled/aisearch_service_public_access_level_is_disabled_test.py @@ -3,7 +3,9 @@ from uuid import uuid4 from prowler.providers.azure.services.aisearch.aisearch_service import AISearchService from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_AISearch_service_not_publicly_accessible: def test_aisearch_sevice_no_aisearch_services(self): aisearch_client = mock.MagicMock + aisearch_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} aisearch_client.aisearch_services = {} with ( @@ -35,6 +38,7 @@ class Test_AISearch_service_not_publicly_accessible: aisearch_service_id = str(uuid4()) aisearch_service_name = "Test AISearch Service" aisearch_client = mock.MagicMock + aisearch_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} aisearch_client.aisearch_services = { AZURE_SUBSCRIPTION_ID: { aisearch_service_id: AISearchService( @@ -66,7 +70,7 @@ class Test_AISearch_service_not_publicly_accessible: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"AISearch Service {aisearch_service_name} from subscription {AZURE_SUBSCRIPTION_ID} allows public access." + == f"AISearch Service {aisearch_service_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} allows public access." ) assert result[0].resource_id == aisearch_service_id assert result[0].subscription == AZURE_SUBSCRIPTION_ID @@ -77,6 +81,7 @@ class Test_AISearch_service_not_publicly_accessible: aisearch_service_id = str(uuid4()) aisearch_service_name = "Test Search Service" aisearch_client = mock.MagicMock + aisearch_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} aisearch_client.aisearch_services = { AZURE_SUBSCRIPTION_ID: { aisearch_service_id: AISearchService( @@ -108,7 +113,7 @@ class Test_AISearch_service_not_publicly_accessible: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"AISearch Service {aisearch_service_name} from subscription {AZURE_SUBSCRIPTION_ID} does not allows public access." + == f"AISearch Service {aisearch_service_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} does not allows public access." ) assert result[0].resource_id == aisearch_service_id assert result[0].subscription == AZURE_SUBSCRIPTION_ID diff --git a/tests/providers/azure/services/aks/aks_cluster_rbac_enabled/aks_cluster_rbac_enabled_test.py b/tests/providers/azure/services/aks/aks_cluster_rbac_enabled/aks_cluster_rbac_enabled_test.py index a4706f013a..5cbe7d67b5 100644 --- a/tests/providers/azure/services/aks/aks_cluster_rbac_enabled/aks_cluster_rbac_enabled_test.py +++ b/tests/providers/azure/services/aks/aks_cluster_rbac_enabled/aks_cluster_rbac_enabled_test.py @@ -3,7 +3,9 @@ from uuid import uuid4 from prowler.providers.azure.services.aks.aks_service import Cluster from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_aks_cluster_rbac_enabled: def test_aks_no_subscriptions(self): aks_client = mock.MagicMock + aks_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} aks_client.clusters = {} with ( @@ -33,6 +36,7 @@ class Test_aks_cluster_rbac_enabled: def test_aks_subscription_empty(self): aks_client = mock.MagicMock + aks_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} aks_client.clusters = {AZURE_SUBSCRIPTION_ID: {}} with ( @@ -55,6 +59,7 @@ class Test_aks_cluster_rbac_enabled: def test_aks_cluster_rbac_enabled(self): aks_client = mock.MagicMock + aks_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} cluster_id = str(uuid4()) aks_client.clusters = { AZURE_SUBSCRIPTION_ID: { @@ -91,7 +96,7 @@ class Test_aks_cluster_rbac_enabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"RBAC is enabled for cluster 'cluster_name' in subscription '{AZURE_SUBSCRIPTION_ID}'." + == f"RBAC is enabled for cluster 'cluster_name' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'." ) assert result[0].resource_name == "cluster_name" assert result[0].resource_id == cluster_id @@ -100,6 +105,7 @@ class Test_aks_cluster_rbac_enabled: def test_aks_rbac_not_enabled(self): aks_client = mock.MagicMock + aks_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} cluster_id = str(uuid4()) aks_client.clusters = { AZURE_SUBSCRIPTION_ID: { @@ -136,7 +142,7 @@ class Test_aks_cluster_rbac_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"RBAC is not enabled for cluster 'cluster_name' in subscription '{AZURE_SUBSCRIPTION_ID}'." + == f"RBAC is not enabled for cluster 'cluster_name' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'." ) assert result[0].resource_name == "cluster_name" assert result[0].resource_id == cluster_id diff --git a/tests/providers/azure/services/aks/aks_clusters_created_with_private_nodes/aks_clusters_created_with_private_nodes_test.py b/tests/providers/azure/services/aks/aks_clusters_created_with_private_nodes/aks_clusters_created_with_private_nodes_test.py index 74ee1e7206..bf5a32660d 100644 --- a/tests/providers/azure/services/aks/aks_clusters_created_with_private_nodes/aks_clusters_created_with_private_nodes_test.py +++ b/tests/providers/azure/services/aks/aks_clusters_created_with_private_nodes/aks_clusters_created_with_private_nodes_test.py @@ -3,7 +3,9 @@ from uuid import uuid4 from prowler.providers.azure.services.aks.aks_service import Cluster from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_aks_clusters_created_with_private_nodes: def test_aks_no_subscriptions(self): aks_client = mock.MagicMock + aks_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} aks_client.clusters = {} with ( @@ -33,6 +36,7 @@ class Test_aks_clusters_created_with_private_nodes: def test_aks_subscription_empty(self): aks_client = mock.MagicMock + aks_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} aks_client.clusters = {AZURE_SUBSCRIPTION_ID: {}} with ( @@ -55,6 +59,7 @@ class Test_aks_clusters_created_with_private_nodes: def test_aks_cluster_no_private_nodes(self): aks_client = mock.MagicMock + aks_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} cluster_id = str(uuid4()) aks_client.clusters = { AZURE_SUBSCRIPTION_ID: { @@ -91,7 +96,7 @@ class Test_aks_clusters_created_with_private_nodes: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Cluster 'cluster_name' was not created with private nodes in subscription '{AZURE_SUBSCRIPTION_ID}'" + == f"Cluster 'cluster_name' was not created with private nodes in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'" ) assert result[0].resource_id == cluster_id assert result[0].resource_name == "cluster_name" @@ -100,6 +105,7 @@ class Test_aks_clusters_created_with_private_nodes: def test_aks_cluster_private_nodes(self): aks_client = mock.MagicMock + aks_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} cluster_id = str(uuid4()) aks_client.clusters = { AZURE_SUBSCRIPTION_ID: { @@ -136,7 +142,7 @@ class Test_aks_clusters_created_with_private_nodes: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Cluster 'cluster_name' was created with private nodes in subscription '{AZURE_SUBSCRIPTION_ID}'" + == f"Cluster 'cluster_name' was created with private nodes in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'" ) assert result[0].resource_id == cluster_id assert result[0].resource_name == "cluster_name" @@ -145,6 +151,7 @@ class Test_aks_clusters_created_with_private_nodes: def test_aks_cluster_public_and_private_nodes(self): aks_client = mock.MagicMock + aks_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} cluster_id = str(uuid4()) aks_client.clusters = { AZURE_SUBSCRIPTION_ID: { @@ -185,7 +192,7 @@ class Test_aks_clusters_created_with_private_nodes: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Cluster 'cluster_name' was not created with private nodes in subscription '{AZURE_SUBSCRIPTION_ID}'" + == f"Cluster 'cluster_name' was not created with private nodes in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'" ) assert result[0].resource_id == cluster_id assert result[0].resource_name == "cluster_name" diff --git a/tests/providers/azure/services/aks/aks_clusters_public_access_disabled/aks_clusters_public_access_disabled_test.py b/tests/providers/azure/services/aks/aks_clusters_public_access_disabled/aks_clusters_public_access_disabled_test.py index 6d012c39e5..dfb000a096 100644 --- a/tests/providers/azure/services/aks/aks_clusters_public_access_disabled/aks_clusters_public_access_disabled_test.py +++ b/tests/providers/azure/services/aks/aks_clusters_public_access_disabled/aks_clusters_public_access_disabled_test.py @@ -3,7 +3,9 @@ from uuid import uuid4 from prowler.providers.azure.services.aks.aks_service import Cluster from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_aks_clusters_public_access_disabled: def test_aks_no_subscriptions(self): aks_client = mock.MagicMock + aks_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} aks_client.clusters = {} with ( @@ -33,6 +36,7 @@ class Test_aks_clusters_public_access_disabled: def test_aks_subscription_empty(self): aks_client = mock.MagicMock + aks_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} aks_client.clusters = {AZURE_SUBSCRIPTION_ID: {}} with ( @@ -55,6 +59,7 @@ class Test_aks_clusters_public_access_disabled: def test_aks_cluster_public_fqdn(self): aks_client = mock.MagicMock + aks_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} cluster_id = str(uuid4()) aks_client.clusters = { AZURE_SUBSCRIPTION_ID: { @@ -91,7 +96,7 @@ class Test_aks_clusters_public_access_disabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Public access to nodes is enabled for cluster 'cluster_name' in subscription '{AZURE_SUBSCRIPTION_ID}'" + == f"Public access to nodes is enabled for cluster 'cluster_name' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'" ) assert result[0].resource_id == cluster_id assert result[0].resource_name == "cluster_name" @@ -100,6 +105,7 @@ class Test_aks_clusters_public_access_disabled: def test_aks_cluster_private_fqdn(self): aks_client = mock.MagicMock + aks_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} cluster_id = str(uuid4()) aks_client.clusters = { AZURE_SUBSCRIPTION_ID: { @@ -136,7 +142,7 @@ class Test_aks_clusters_public_access_disabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Public access to nodes is disabled for cluster 'cluster_name' in subscription '{AZURE_SUBSCRIPTION_ID}'" + == f"Public access to nodes is disabled for cluster 'cluster_name' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'" ) assert result[0].resource_id == cluster_id assert result[0].resource_name == "cluster_name" @@ -145,6 +151,7 @@ class Test_aks_clusters_public_access_disabled: def test_aks_cluster_private_fqdn_with_public_ip(self): aks_client = mock.MagicMock + aks_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} cluster_id = str(uuid4()) aks_client.clusters = { AZURE_SUBSCRIPTION_ID: { @@ -181,7 +188,7 @@ class Test_aks_clusters_public_access_disabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Public access to nodes is enabled for cluster 'cluster_name' in subscription '{AZURE_SUBSCRIPTION_ID}'" + == f"Public access to nodes is enabled for cluster 'cluster_name' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'" ) assert result[0].resource_id == cluster_id assert result[0].resource_name == "cluster_name" diff --git a/tests/providers/azure/services/aks/aks_network_policy_enabled/aks_network_policy_enabled_test.py b/tests/providers/azure/services/aks/aks_network_policy_enabled/aks_network_policy_enabled_test.py index a172e94151..896fa44405 100644 --- a/tests/providers/azure/services/aks/aks_network_policy_enabled/aks_network_policy_enabled_test.py +++ b/tests/providers/azure/services/aks/aks_network_policy_enabled/aks_network_policy_enabled_test.py @@ -3,7 +3,9 @@ from uuid import uuid4 from prowler.providers.azure.services.aks.aks_service import Cluster from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_aks_network_policy_enabled: def test_aks_no_subscriptions(self): aks_client = mock.MagicMock + aks_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} aks_client.clusters = {} with ( @@ -33,6 +36,7 @@ class Test_aks_network_policy_enabled: def test_aks_subscription_empty(self): aks_client = mock.MagicMock + aks_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} aks_client.clusters = {AZURE_SUBSCRIPTION_ID: {}} with ( @@ -55,6 +59,7 @@ class Test_aks_network_policy_enabled: def test_aks_network_policy_enabled(self): aks_client = mock.MagicMock + aks_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} cluster_id = str(uuid4()) aks_client.clusters = { AZURE_SUBSCRIPTION_ID: { @@ -91,7 +96,7 @@ class Test_aks_network_policy_enabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Network policy is enabled for cluster 'cluster_name' in subscription '{AZURE_SUBSCRIPTION_ID}'." + == f"Network policy is enabled for cluster 'cluster_name' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'." ) assert result[0].resource_name == "cluster_name" assert result[0].resource_id == cluster_id @@ -100,6 +105,7 @@ class Test_aks_network_policy_enabled: def test_aks_network_policy_disabled(self): aks_client = mock.MagicMock + aks_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} cluster_id = str(uuid4()) aks_client.clusters = { AZURE_SUBSCRIPTION_ID: { @@ -136,7 +142,7 @@ class Test_aks_network_policy_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Network policy is not enabled for cluster 'cluster_name' in subscription '{AZURE_SUBSCRIPTION_ID}'." + == f"Network policy is not enabled for cluster 'cluster_name' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'." ) assert result[0].resource_name == "cluster_name" assert result[0].resource_id == cluster_id diff --git a/tests/providers/azure/services/apim/apim_service_test.py b/tests/providers/azure/services/apim/apim_service_test.py index 3b1a061293..f2141aee6b 100644 --- a/tests/providers/azure/services/apim/apim_service_test.py +++ b/tests/providers/azure/services/apim/apim_service_test.py @@ -8,6 +8,7 @@ from azure.monitor.query import LogsQueryResult from tests.providers.azure.azure_fixtures import ( AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -193,6 +194,9 @@ class Test_APIM_Service(TestCase): # Properly mock the nested client structure mock_client = mock.MagicMock() + mock_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } mock_workspaces = mock.MagicMock() mock_workspaces.get.return_value = mock_workspace mock_client.workspaces = mock_workspaces @@ -246,6 +250,9 @@ class Test_APIM_Service(TestCase): # Properly mock the nested client structure mock_client = mock.MagicMock() + mock_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } mock_client.query_workspace.return_value = mock_response mock_logsquery_client.clients = {AZURE_SUBSCRIPTION_ID: mock_client} diff --git a/tests/providers/azure/services/apim/apim_threat_detection_llm_jacking/apim_threat_detection_llm_jacking_test.py b/tests/providers/azure/services/apim/apim_threat_detection_llm_jacking/apim_threat_detection_llm_jacking_test.py index a551b33c7e..997c2392fa 100644 --- a/tests/providers/azure/services/apim/apim_threat_detection_llm_jacking/apim_threat_detection_llm_jacking_test.py +++ b/tests/providers/azure/services/apim/apim_threat_detection_llm_jacking/apim_threat_detection_llm_jacking_test.py @@ -2,7 +2,9 @@ from datetime import datetime from unittest import mock from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -139,10 +141,18 @@ def mock_get_llm_operations_logs_no_workspace(subscription, instance, minutes): return [] +def mock_get_llm_operations_logs_by_subscription(subscription, instance, minutes): + """Return different logs per subscription to validate isolation.""" + if subscription == AZURE_SUBSCRIPTION_ID: + return mock_get_llm_operations_logs_attacker(subscription, instance, minutes) + return mock_get_llm_operations_logs_2_operations(subscription, instance, minutes) + + class Test_apim_threat_detection_llm_jacking: def test_no_apim_instances(self): """Test when there are no APIM instances""" apim_client = mock.MagicMock() + apim_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} apim_client.instances = {} apim_client.audit_config = { "apim_threat_detection_llm_jacking_threshold": 0.1, @@ -175,6 +185,7 @@ class Test_apim_threat_detection_llm_jacking: def test_no_potential_llm_jacking(self): """Test when no potential LLM jacking is detected""" apim_client = mock.MagicMock() + apim_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} apim_client.instances = { AZURE_SUBSCRIPTION_ID: [ mock.MagicMock( @@ -184,7 +195,7 @@ class Test_apim_threat_detection_llm_jacking: ) ] } - apim_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_ID} + apim_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} apim_client.audit_config = { "apim_threat_detection_llm_jacking_threshold": 0.9, "apim_threat_detection_llm_jacking_minutes": 1440, @@ -240,6 +251,7 @@ class Test_apim_threat_detection_llm_jacking: def test_potential_llm_jacking_detected(self): """Test when potential LLM jacking is detected""" apim_client = mock.MagicMock() + apim_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} apim_client.instances = { AZURE_SUBSCRIPTION_ID: [ mock.MagicMock( @@ -286,6 +298,7 @@ class Test_apim_threat_detection_llm_jacking: "Potential LLM Jacking attack detected from IP address 10.0.0.50" in result[0].status_extended ) + assert AZURE_SUBSCRIPTION_DISPLAY in result[0].status_extended assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource["name"] == "10.0.0.50" assert result[0].resource["id"] == "10.0.0.50" @@ -293,6 +306,7 @@ class Test_apim_threat_detection_llm_jacking: def test_higher_threshold_no_detection(self): """Test when threshold is higher and no attack is detected""" apim_client = mock.MagicMock() + apim_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} apim_client.instances = { AZURE_SUBSCRIPTION_ID: [ mock.MagicMock( @@ -302,7 +316,7 @@ class Test_apim_threat_detection_llm_jacking: ) ] } - apim_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_ID} + apim_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} apim_client.audit_config = { "apim_threat_detection_llm_jacking_threshold": 0.9, "apim_threat_detection_llm_jacking_minutes": 1440, @@ -367,7 +381,7 @@ class Test_apim_threat_detection_llm_jacking: ) ] } - apim_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_ID} + apim_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} apim_client.audit_config = { "apim_threat_detection_llm_jacking_threshold": 0.9, "apim_threat_detection_llm_jacking_minutes": 1440, @@ -423,6 +437,7 @@ class Test_apim_threat_detection_llm_jacking: def test_multiple_subscriptions(self): """Test with multiple subscriptions""" apim_client = mock.MagicMock() + apim_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} apim_client.instances = { AZURE_SUBSCRIPTION_ID: [ mock.MagicMock( @@ -440,7 +455,7 @@ class Test_apim_threat_detection_llm_jacking: ], } apim_client.subscriptions = { - AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME, "another-subscription": "another-subscription-id", } apim_client.audit_config = { @@ -496,3 +511,90 @@ class Test_apim_threat_detection_llm_jacking: "No potential LLM Jacking attacks detected" in report.status_extended ) + + def test_multiple_subscriptions_keep_findings_isolated(self): + """Ensure findings from one subscription do not leak into another.""" + apim_client = mock.MagicMock() + second_subscription_id = "another-subscription" + second_subscription_name = "another-subscription-id" + apim_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME, + second_subscription_id: second_subscription_name, + } + apim_client.instances = { + AZURE_SUBSCRIPTION_ID: [ + mock.MagicMock( + id="/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.ApiManagement/service/test-apim", + name="test-apim", + log_analytics_workspace_id="/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.OperationalInsights/workspaces/test-workspace", + ) + ], + second_subscription_id: [ + mock.MagicMock( + id="/subscriptions/another-sub/resourceGroups/test-rg/providers/Microsoft.ApiManagement/service/another-apim", + name="another-apim", + log_analytics_workspace_id="/subscriptions/another-sub/resourceGroups/test-rg/providers/Microsoft.OperationalInsights/workspaces/another-workspace", + ) + ], + } + apim_client.audit_config = { + "apim_threat_detection_llm_jacking_threshold": 0.2, + "apim_threat_detection_llm_jacking_minutes": 1440, + "apim_threat_detection_llm_jacking_actions": [ + "ChatCompletions_Create", + "ImageGenerations_Create", + "Completions_Create", + "Embeddings_Create", + "FineTuning_Jobs_Create", + "Models_List", + "Deployments_List", + "Deployments_Get", + "Deployments_Create", + "Deployments_Delete", + "Messages_Create", + "Claude_Create", + "GenerateContent", + "GenerateText", + "GenerateImage", + "Llama_Create", + "CodeLlama_Create", + "Gemini_Generate", + "Claude_Generate", + "Llama_Generate", + ], + } + apim_client.get_llm_operations_logs = ( + mock_get_llm_operations_logs_by_subscription + ) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.apim.apim_threat_detection_llm_jacking.apim_threat_detection_llm_jacking.apim_client", + new=apim_client, + ), + ): + from prowler.providers.azure.services.apim.apim_threat_detection_llm_jacking.apim_threat_detection_llm_jacking import ( + apim_threat_detection_llm_jacking, + ) + + check = apim_threat_detection_llm_jacking() + result = check.execute() + + assert len(result) == 2 + + report_by_subscription = {report.subscription: report for report in result} + + assert report_by_subscription[AZURE_SUBSCRIPTION_ID].status == "FAIL" + assert ( + AZURE_SUBSCRIPTION_DISPLAY + in report_by_subscription[AZURE_SUBSCRIPTION_ID].status_extended + ) + assert report_by_subscription[second_subscription_id].status == "PASS" + assert ( + f"{second_subscription_name} ({second_subscription_id})" + in report_by_subscription[second_subscription_id].status_extended + ) diff --git a/tests/providers/azure/services/app/app_client_certificates_on/app_client_certificates_on_test.py b/tests/providers/azure/services/app/app_client_certificates_on/app_client_certificates_on_test.py index 581b3be994..b140223890 100644 --- a/tests/providers/azure/services/app/app_client_certificates_on/app_client_certificates_on_test.py +++ b/tests/providers/azure/services/app/app_client_certificates_on/app_client_certificates_on_test.py @@ -2,7 +2,9 @@ from unittest import mock from uuid import uuid4 from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -10,6 +12,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_app_client_certificates_on: def test_app_no_subscriptions(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} app_client.apps = {} with ( @@ -32,6 +35,7 @@ class Test_app_client_certificates_on: def test_app_subscription_empty(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} app_client.apps = {AZURE_SUBSCRIPTION_ID: {}} with ( @@ -55,6 +59,7 @@ class Test_app_client_certificates_on: def test_app_client_certificates_on(self): resource_id = f"/subscriptions/{uuid4()}" app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -91,7 +96,7 @@ class Test_app_client_certificates_on: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Clients are required to present a certificate for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_ID}'." + == f"Clients are required to present a certificate for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'." ) assert result[0].resource_id == resource_id assert result[0].resource_name == "app_id-1" @@ -101,6 +106,7 @@ class Test_app_client_certificates_on: def test_app_client_certificates_off(self): resource_id = f"/subscriptions/{uuid4()}" app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -137,7 +143,7 @@ class Test_app_client_certificates_on: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Clients are not required to present a certificate for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_ID}'." + == f"Clients are not required to present a certificate for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'." ) assert result[0].resource_id == resource_id assert result[0].resource_name == "app_id-1" diff --git a/tests/providers/azure/services/app/app_ensure_auth_is_set_up/app_ensure_auth_is_set_up_test.py b/tests/providers/azure/services/app/app_ensure_auth_is_set_up/app_ensure_auth_is_set_up_test.py index 79bf195d80..b8f0d12ed2 100644 --- a/tests/providers/azure/services/app/app_ensure_auth_is_set_up/app_ensure_auth_is_set_up_test.py +++ b/tests/providers/azure/services/app/app_ensure_auth_is_set_up/app_ensure_auth_is_set_up_test.py @@ -2,7 +2,9 @@ from unittest import mock from uuid import uuid4 from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -10,6 +12,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_app_ensure_auth_is_set_up: def test_app_no_subscriptions(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} app_client.apps = {} with ( @@ -32,6 +35,7 @@ class Test_app_ensure_auth_is_set_up: def test_app_subscription_empty(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} app_client.apps = {AZURE_SUBSCRIPTION_ID: {}} with ( @@ -55,6 +59,7 @@ class Test_app_ensure_auth_is_set_up: def test_app_auth_enabled(self): resource_id = f"/subscriptions/{uuid4()}" app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -91,7 +96,7 @@ class Test_app_ensure_auth_is_set_up: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Authentication is set up for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_ID}'." + == f"Authentication is set up for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'." ) assert result[0].resource_name == "app_id-1" assert result[0].resource_id == resource_id @@ -101,6 +106,7 @@ class Test_app_ensure_auth_is_set_up: def test_app_auth_disabled(self): resource_id = f"/subscriptions/{uuid4()}" app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -137,7 +143,7 @@ class Test_app_ensure_auth_is_set_up: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Authentication is not set up for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_ID}'." + == f"Authentication is not set up for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'." ) assert result[0].resource_name == "app_id-1" assert result[0].resource_id == resource_id diff --git a/tests/providers/azure/services/app/app_ensure_http_is_redirected_to_https/app_ensure_http_is_redirected_to_https_test.py b/tests/providers/azure/services/app/app_ensure_http_is_redirected_to_https/app_ensure_http_is_redirected_to_https_test.py index 08743bbd79..4d959f3e3f 100644 --- a/tests/providers/azure/services/app/app_ensure_http_is_redirected_to_https/app_ensure_http_is_redirected_to_https_test.py +++ b/tests/providers/azure/services/app/app_ensure_http_is_redirected_to_https/app_ensure_http_is_redirected_to_https_test.py @@ -2,7 +2,9 @@ from unittest import mock from uuid import uuid4 from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -10,6 +12,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_app_ensure_http_is_redirected_to_https: def test_app_no_subscriptions(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} app_client.apps = {} with ( @@ -32,6 +35,7 @@ class Test_app_ensure_http_is_redirected_to_https: def test_app_subscriptions_empty_empty(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} app_client.apps = {AZURE_SUBSCRIPTION_ID: {}} with ( @@ -55,6 +59,7 @@ class Test_app_ensure_http_is_redirected_to_https: def test_app_http_to_https(self): resource_id = f"/subscriptions/{uuid4()}" app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -91,7 +96,7 @@ class Test_app_ensure_http_is_redirected_to_https: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"HTTP is not redirected to HTTPS for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_ID}'." + == f"HTTP is not redirected to HTTPS for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'." ) assert result[0].resource_name == "app_id-1" assert result[0].resource_id == resource_id @@ -101,6 +106,7 @@ class Test_app_ensure_http_is_redirected_to_https: def test_app_http_to_https_enabled(self): resource_id = f"/subscriptions/{uuid4()}" app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -137,7 +143,7 @@ class Test_app_ensure_http_is_redirected_to_https: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"HTTP is redirected to HTTPS for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_ID}'." + == f"HTTP is redirected to HTTPS for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'." ) assert result[0].resource_name == "app_id-1" assert result[0].resource_id == resource_id diff --git a/tests/providers/azure/services/app/app_ensure_java_version_is_latest/app_ensure_java_version_is_latest_test.py b/tests/providers/azure/services/app/app_ensure_java_version_is_latest/app_ensure_java_version_is_latest_test.py index 22a9d3771a..2e0d847b97 100644 --- a/tests/providers/azure/services/app/app_ensure_java_version_is_latest/app_ensure_java_version_is_latest_test.py +++ b/tests/providers/azure/services/app/app_ensure_java_version_is_latest/app_ensure_java_version_is_latest_test.py @@ -2,7 +2,9 @@ from unittest import mock from uuid import uuid4 from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -10,6 +12,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_app_ensure_java_version_is_latest: def test_app_no_subscriptions(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} app_client.apps = {} with ( @@ -32,6 +35,7 @@ class Test_app_ensure_java_version_is_latest: def test_app_subscriptions_empty(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} app_client.apps = {AZURE_SUBSCRIPTION_ID: {}} with ( @@ -55,6 +59,7 @@ class Test_app_ensure_java_version_is_latest: def test_app_configurations_none(self): resource_id = f"/subscriptions/{uuid4()}" app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -92,6 +97,7 @@ class Test_app_ensure_java_version_is_latest: def test_app_linux_java_version_latest(self): resource_id = f"/subscriptions/{uuid4()}" app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} app_client.audit_config = {"java_latest_version": "17"} @@ -132,7 +138,7 @@ class Test_app_ensure_java_version_is_latest: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Java version is set to 'java 17' for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_ID}'." + == f"Java version is set to 'java 17' for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'." ) assert result[0].resource_id == resource_id assert result[0].resource_name == "app_id-1" @@ -142,6 +148,7 @@ class Test_app_ensure_java_version_is_latest: def test_app_linux_java_version_not_latest(self): resource_id = f"/subscriptions/{uuid4()}" app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} app_client.audit_config = {"java_latest_version": "17"} @@ -182,7 +189,7 @@ class Test_app_ensure_java_version_is_latest: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Java version is set to 'Tomcat|9.0-java11', but should be set to 'java 17' for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_ID}'." + == f"Java version is set to 'Tomcat|9.0-java11', but should be set to 'java 17' for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'." ) assert result[0].resource_id == resource_id assert result[0].resource_name == "app_id-1" @@ -192,6 +199,7 @@ class Test_app_ensure_java_version_is_latest: def test_app_windows_java_version_latest(self): resource_id = f"/subscriptions/{uuid4()}" app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} app_client.audit_config = {"java_latest_version": "17"} @@ -232,7 +240,7 @@ class Test_app_ensure_java_version_is_latest: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Java version is set to 'java 17' for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_ID}'." + == f"Java version is set to 'java 17' for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'." ) assert result[0].resource_id == resource_id assert result[0].resource_name == "app_id-1" @@ -242,6 +250,7 @@ class Test_app_ensure_java_version_is_latest: def test_app_windows_java_version_not_latest(self): resource_id = f"/subscriptions/{uuid4()}" app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} app_client.audit_config = {"java_latest_version": "17"} with ( @@ -281,7 +290,7 @@ class Test_app_ensure_java_version_is_latest: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Java version is set to 'java11', but should be set to 'java 17' for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_ID}'." + == f"Java version is set to 'java11', but should be set to 'java 17' for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'." ) assert result[0].resource_id == resource_id assert result[0].resource_name == "app_id-1" @@ -291,6 +300,7 @@ class Test_app_ensure_java_version_is_latest: def test_app_linux_php_version_latest(self): resource_id = f"/subscriptions/{uuid4()}" app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} app_client.audit_config = {"java_latest_version": "17"} diff --git a/tests/providers/azure/services/app/app_ensure_php_version_is_latest/app_ensure_php_version_is_latest_test.py b/tests/providers/azure/services/app/app_ensure_php_version_is_latest/app_ensure_php_version_is_latest_test.py index 3db89439a3..2e192501ef 100644 --- a/tests/providers/azure/services/app/app_ensure_php_version_is_latest/app_ensure_php_version_is_latest_test.py +++ b/tests/providers/azure/services/app/app_ensure_php_version_is_latest/app_ensure_php_version_is_latest_test.py @@ -2,7 +2,9 @@ from unittest import mock from uuid import uuid4 from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -10,6 +12,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_app_ensure_php_version_is_latest: def test_app_no_subscriptions(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} app_client.apps = {} with ( @@ -32,6 +35,7 @@ class Test_app_ensure_php_version_is_latest: def test_app_subscription_empty(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} app_client.apps = {AZURE_SUBSCRIPTION_ID: {}} with ( @@ -55,6 +59,7 @@ class Test_app_ensure_php_version_is_latest: def test_app_configurations_none(self): resource_id = f"/subscriptions/{uuid4()}" app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -92,6 +97,7 @@ class Test_app_ensure_php_version_is_latest: def test_app_php_version_not_latest(self): resource_id = f"/subscriptions/{uuid4()}" app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} app_client.audit_config = {"php_latest_version": "8.2"} @@ -130,7 +136,7 @@ class Test_app_ensure_php_version_is_latest: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"PHP version is set to 'php|8.0', the latest version that you could use is the '8.2' version, for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_ID}'." + == f"PHP version is set to 'php|8.0', the latest version that you could use is the '8.2' version, for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'." ) assert result[0].resource_id == resource_id assert result[0].resource_name == "app_id-1" @@ -140,6 +146,7 @@ class Test_app_ensure_php_version_is_latest: def test_app_php_version_latest(self): resource_id = f"/subscriptions/{uuid4()}" app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} app_client.audit_config = {"php_latest_version": "8.2"} @@ -178,7 +185,7 @@ class Test_app_ensure_php_version_is_latest: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"PHP version is set to '8.2' for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_ID}'." + == f"PHP version is set to '8.2' for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'." ) assert result[0].resource_id == resource_id assert result[0].resource_name == "app_id-1" diff --git a/tests/providers/azure/services/app/app_ensure_python_version_is_latest/app_ensure_python_version_is_latest_test.py b/tests/providers/azure/services/app/app_ensure_python_version_is_latest/app_ensure_python_version_is_latest_test.py index 899d3615b5..7f2eaf6693 100644 --- a/tests/providers/azure/services/app/app_ensure_python_version_is_latest/app_ensure_python_version_is_latest_test.py +++ b/tests/providers/azure/services/app/app_ensure_python_version_is_latest/app_ensure_python_version_is_latest_test.py @@ -2,7 +2,9 @@ from unittest import mock from uuid import uuid4 from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -10,6 +12,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_app_ensure_python_version_is_latest: def test_app_no_subscriptions(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} app_client.apps = {} with ( @@ -32,6 +35,7 @@ class Test_app_ensure_python_version_is_latest: def test_app_subscriptions_empty(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} app_client.apps = {AZURE_SUBSCRIPTION_ID: {}} with ( @@ -55,6 +59,7 @@ class Test_app_ensure_python_version_is_latest: def test_app_configurations_none(self): resource_id = f"/subscriptions/{uuid4()}" app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -91,6 +96,7 @@ class Test_app_ensure_python_version_is_latest: def test_app_python_version_latest(self): resource_id = f"/subscriptions/{uuid4()}" app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} app_client.audit_config = {"python_latest_version": "3.12"} @@ -129,7 +135,7 @@ class Test_app_ensure_python_version_is_latest: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Python version is set to '3.12' for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_ID}'." + == f"Python version is set to '3.12' for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'." ) assert result[0].resource_id == resource_id assert result[0].resource_name == "app_id-1" @@ -139,6 +145,7 @@ class Test_app_ensure_python_version_is_latest: def test_app_python_version_not_latest(self): resource_id = f"/subscriptions/{uuid4()}" app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} app_client.audit_config = {"python_latest_version": "3.12"} @@ -177,7 +184,7 @@ class Test_app_ensure_python_version_is_latest: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Python version is 'python|3.10', the latest version that you could use is the '3.12' version, for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_ID}'." + == f"Python version is 'python|3.10', the latest version that you could use is the '3.12' version, for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'." ) assert result[0].resource_id == resource_id assert result[0].resource_name == "app_id-1" diff --git a/tests/providers/azure/services/app/app_ensure_using_http20/app_ensure_using_http20_test.py b/tests/providers/azure/services/app/app_ensure_using_http20/app_ensure_using_http20_test.py index d392d4d3b0..105a7b9a03 100644 --- a/tests/providers/azure/services/app/app_ensure_using_http20/app_ensure_using_http20_test.py +++ b/tests/providers/azure/services/app/app_ensure_using_http20/app_ensure_using_http20_test.py @@ -2,7 +2,9 @@ from unittest import mock from uuid import uuid4 from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -10,6 +12,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_app_ensure_using_http20: def test_app_no_subscriptions(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} app_client.apps = {} with ( @@ -32,6 +35,7 @@ class Test_app_ensure_using_http20: def test_app_subscription_empty(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} app_client.apps = {AZURE_SUBSCRIPTION_ID: {}} with ( @@ -55,6 +59,7 @@ class Test_app_ensure_using_http20: def test_app_configurations_none(self): resource_id = f"/subscriptions/{uuid4()}" app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -91,7 +96,7 @@ class Test_app_ensure_using_http20: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"HTTP/2.0 is not enabled for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_ID}'." + == f"HTTP/2.0 is not enabled for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'." ) assert result[0].resource_id == resource_id assert result[0].resource_name == "app_id-1" @@ -101,6 +106,7 @@ class Test_app_ensure_using_http20: def test_app_http20_enabled(self): resource_id = f"/subscriptions/{uuid4()}" app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -137,7 +143,7 @@ class Test_app_ensure_using_http20: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"HTTP/2.0 is enabled for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_ID}'." + == f"HTTP/2.0 is enabled for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'." ) assert result[0].resource_id == resource_id assert result[0].resource_name == "app_id-1" @@ -147,6 +153,7 @@ class Test_app_ensure_using_http20: def test_app_http20_not_enabled(self): resource_id = f"/subscriptions/{uuid4()}" app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -183,7 +190,7 @@ class Test_app_ensure_using_http20: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"HTTP/2.0 is not enabled for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_ID}'." + == f"HTTP/2.0 is not enabled for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'." ) assert result[0].resource_id == resource_id assert result[0].resource_name == "app_id-1" diff --git a/tests/providers/azure/services/app/app_ftp_deployment_disabled/app_ftp_deployment_disabled_test.py b/tests/providers/azure/services/app/app_ftp_deployment_disabled/app_ftp_deployment_disabled_test.py index 6d0633fef3..fc0d61b9cc 100644 --- a/tests/providers/azure/services/app/app_ftp_deployment_disabled/app_ftp_deployment_disabled_test.py +++ b/tests/providers/azure/services/app/app_ftp_deployment_disabled/app_ftp_deployment_disabled_test.py @@ -2,7 +2,9 @@ from unittest import mock from uuid import uuid4 from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -10,6 +12,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_app_ftp_deployment_disabled: def test_app_no_subscriptions(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} app_client.apps = {} with ( @@ -32,6 +35,7 @@ class Test_app_ftp_deployment_disabled: def test_app_subscriptions_empty(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} app_client.apps = {AZURE_SUBSCRIPTION_ID: {}} with ( @@ -55,6 +59,7 @@ class Test_app_ftp_deployment_disabled: def test_app_configurations_none(self): resource_id = f"/subscriptions/{uuid4()}" app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -91,7 +96,7 @@ class Test_app_ftp_deployment_disabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"FTP is enabled for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_ID}'." + == f"FTP is enabled for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'." ) assert result[0].resource_id == resource_id assert result[0].resource_name == "app_id-1" @@ -101,6 +106,7 @@ class Test_app_ftp_deployment_disabled: def test_app_ftp_deployment_disabled(self): resource_id = f"/subscriptions/{uuid4()}" app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -137,7 +143,7 @@ class Test_app_ftp_deployment_disabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"FTP is enabled for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_ID}'." + == f"FTP is enabled for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'." ) assert result[0].resource_id == resource_id assert result[0].resource_name == "app_id-1" @@ -147,6 +153,7 @@ class Test_app_ftp_deployment_disabled: def test_app_ftp_deploy_enabled(self): resource_id = f"/subscriptions/{uuid4()}" app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -183,7 +190,7 @@ class Test_app_ftp_deployment_disabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"FTP is disabled for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_ID}'." + == f"FTP is disabled for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'." ) assert result[0].resource_id == resource_id assert result[0].resource_name == "app_id-1" diff --git a/tests/providers/azure/services/app/app_function_access_keys_configured/app_function_access_keys_configured_test.py b/tests/providers/azure/services/app/app_function_access_keys_configured/app_function_access_keys_configured_test.py index 42d8360ee1..770ca07b2b 100644 --- a/tests/providers/azure/services/app/app_function_access_keys_configured/app_function_access_keys_configured_test.py +++ b/tests/providers/azure/services/app/app_function_access_keys_configured/app_function_access_keys_configured_test.py @@ -2,7 +2,9 @@ from unittest import mock from uuid import uuid4 from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -10,6 +12,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_app_function_access_keys_configured: def test_app_no_subscriptions(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -33,6 +36,7 @@ class Test_app_function_access_keys_configured: def test_app_subscription_empty(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} app_client.functions = {AZURE_SUBSCRIPTION_ID: {}} with ( @@ -55,6 +59,7 @@ class Test_app_function_access_keys_configured: def test_app_function_no_keys(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} app_client.functions = {AZURE_SUBSCRIPTION_ID: {}} with ( @@ -98,7 +103,7 @@ class Test_app_function_access_keys_configured: assert result[0].status == "FAIL" assert ( result[0].status_extended - == "Function function1 does not have function keys configured." + == f"Function function1 from subscription {AZURE_SUBSCRIPTION_DISPLAY} does not have function keys configured." ) assert result[0].resource_id == function_id assert result[0].resource_name == "function1" @@ -107,6 +112,7 @@ class Test_app_function_access_keys_configured: def test_app_function_using_functions_keys(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} app_client.functions = {AZURE_SUBSCRIPTION_ID: {}} with ( @@ -153,7 +159,7 @@ class Test_app_function_access_keys_configured: assert result[0].status == "PASS" assert ( result[0].status_extended - == "Function function1 has function keys configured." + == f"Function function1 from subscription {AZURE_SUBSCRIPTION_DISPLAY} has function keys configured." ) assert result[0].resource_id == function_id assert result[0].resource_name == "function1" diff --git a/tests/providers/azure/services/app/app_function_application_insights_enabled/app_function_application_insights_enabled_test.py b/tests/providers/azure/services/app/app_function_application_insights_enabled/app_function_application_insights_enabled_test.py index 4ea444157c..4a55b1e108 100644 --- a/tests/providers/azure/services/app/app_function_application_insights_enabled/app_function_application_insights_enabled_test.py +++ b/tests/providers/azure/services/app/app_function_application_insights_enabled/app_function_application_insights_enabled_test.py @@ -2,7 +2,9 @@ from unittest import mock from uuid import uuid4 from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -10,6 +12,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_app_function_application_insights_enabled: def test_app_no_subscriptions(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -33,6 +36,7 @@ class Test_app_function_application_insights_enabled: def test_app_subscription_empty(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -56,6 +60,7 @@ class Test_app_function_application_insights_enabled: def test_app_function_no_app_insights(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -97,7 +102,7 @@ class Test_app_function_application_insights_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == "Function function1 is not using Application Insights." + == f"Function function1 from subscription {AZURE_SUBSCRIPTION_DISPLAY} is not using Application Insights." ) assert result[0].resource_id == function_id assert result[0].resource_name == "function1" @@ -106,6 +111,7 @@ class Test_app_function_application_insights_enabled: def test_app_function_using_app_insights(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -147,7 +153,7 @@ class Test_app_function_application_insights_enabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == "Function function1 is using Application Insights." + == f"Function function1 from subscription {AZURE_SUBSCRIPTION_DISPLAY} is using Application Insights." ) assert result[0].resource_id == function_id assert result[0].resource_name == "function1" @@ -156,6 +162,7 @@ class Test_app_function_application_insights_enabled: def test_app_function_using_app_insights_different_key(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -197,7 +204,7 @@ class Test_app_function_application_insights_enabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == "Function function1 is using Application Insights." + == f"Function function1 from subscription {AZURE_SUBSCRIPTION_DISPLAY} is using Application Insights." ) assert result[0].resource_id == function_id assert result[0].resource_name == "function1" @@ -206,6 +213,7 @@ class Test_app_function_application_insights_enabled: def test_app_function_with_app_insights_no_key(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -247,7 +255,7 @@ class Test_app_function_application_insights_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == "Function function1 is not using Application Insights." + == f"Function function1 from subscription {AZURE_SUBSCRIPTION_DISPLAY} is not using Application Insights." ) assert result[0].resource_id == function_id assert result[0].resource_name == "function1" diff --git a/tests/providers/azure/services/app/app_function_ftps_deployment_disabled/app_function_ftps_deployment_disabled_test.py b/tests/providers/azure/services/app/app_function_ftps_deployment_disabled/app_function_ftps_deployment_disabled_test.py index 2546ba2f44..b08c712da1 100644 --- a/tests/providers/azure/services/app/app_function_ftps_deployment_disabled/app_function_ftps_deployment_disabled_test.py +++ b/tests/providers/azure/services/app/app_function_ftps_deployment_disabled/app_function_ftps_deployment_disabled_test.py @@ -2,7 +2,9 @@ from unittest import mock from uuid import uuid4 from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -10,6 +12,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_app_function_ftps_deployment_disabled: def test_no_subscriptions(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -33,6 +36,7 @@ class Test_app_function_ftps_deployment_disabled: def test_subscription_empty(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -56,6 +60,7 @@ class Test_app_function_ftps_deployment_disabled: def test_function_ftp_deployment_enabled(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -97,7 +102,7 @@ class Test_app_function_ftps_deployment_disabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == "Function function1 has FTP deployment enabled" + == f"Function function1 from subscription {AZURE_SUBSCRIPTION_DISPLAY} has FTP deployment enabled." ) assert result[0].resource_name == "function1" assert result[0].resource_id == function_id @@ -106,6 +111,7 @@ class Test_app_function_ftps_deployment_disabled: def test_function_ftps_deployment_enabled(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -147,7 +153,7 @@ class Test_app_function_ftps_deployment_disabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == "Function function1 has FTPS deployment enabled" + == f"Function function1 from subscription {AZURE_SUBSCRIPTION_DISPLAY} has FTPS deployment enabled." ) assert result[0].resource_name == "function1" assert result[0].resource_id == function_id @@ -156,6 +162,7 @@ class Test_app_function_ftps_deployment_disabled: def test_function_ftp_and_ftps_deployment_disabled(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -197,7 +204,7 @@ class Test_app_function_ftps_deployment_disabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == "Function function1 has FTP and FTPS deployment disabled" + == f"Function function1 from subscription {AZURE_SUBSCRIPTION_DISPLAY} has FTP and FTPS deployment disabled." ) assert result[0].resource_name == "function1" assert result[0].resource_id == function_id diff --git a/tests/providers/azure/services/app/app_function_identity_is_configured/app_function_identity_is_configured_test.py b/tests/providers/azure/services/app/app_function_identity_is_configured/app_function_identity_is_configured_test.py index ffa4a0e743..84dbece6d2 100644 --- a/tests/providers/azure/services/app/app_function_identity_is_configured/app_function_identity_is_configured_test.py +++ b/tests/providers/azure/services/app/app_function_identity_is_configured/app_function_identity_is_configured_test.py @@ -2,7 +2,9 @@ from unittest import mock from uuid import uuid4 from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -10,6 +12,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_app_function_identity_is_configured: def test_app_no_subscriptions(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -33,6 +36,7 @@ class Test_app_function_identity_is_configured: def test_app_subscription_empty(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -56,6 +60,7 @@ class Test_app_function_identity_is_configured: def test_app_function_no_identity(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -97,7 +102,7 @@ class Test_app_function_identity_is_configured: assert result[0].status == "FAIL" assert ( result[0].status_extended - == "Function function1 does not have a managed identity enabled." + == f"Function function1 from subscription {AZURE_SUBSCRIPTION_DISPLAY} does not have a managed identity enabled." ) assert result[0].resource_name == "function1" assert result[0].resource_id == function_id @@ -106,6 +111,7 @@ class Test_app_function_identity_is_configured: def test_app_function_identity_configured(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -147,7 +153,7 @@ class Test_app_function_identity_is_configured: assert result[0].status == "PASS" assert ( result[0].status_extended - == "Function function1 has a SystemAssigned identity enabled." + == f"Function function1 from subscription {AZURE_SUBSCRIPTION_DISPLAY} has a SystemAssigned identity enabled." ) assert result[0].resource_name == "function1" assert result[0].resource_id == function_id diff --git a/tests/providers/azure/services/app/app_function_identity_without_admin_privileges/app_function_identity_without_admin_privileges_test.py b/tests/providers/azure/services/app/app_function_identity_without_admin_privileges/app_function_identity_without_admin_privileges_test.py index c4761099b9..9c7f074c3b 100644 --- a/tests/providers/azure/services/app/app_function_identity_without_admin_privileges/app_function_identity_without_admin_privileges_test.py +++ b/tests/providers/azure/services/app/app_function_identity_without_admin_privileges/app_function_identity_without_admin_privileges_test.py @@ -3,7 +3,9 @@ from uuid import uuid4 from prowler.providers.azure.config import USER_ACCESS_ADMINISTRATOR_ROLE_ID from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_app_function_identity_without_admin_privileges: def test_app_no_subscriptions(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -34,6 +37,7 @@ class Test_app_function_identity_without_admin_privileges: def test_app_subscription_empty(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -57,6 +61,7 @@ class Test_app_function_identity_without_admin_privileges: def test_app_function_no_identity(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -98,7 +103,9 @@ class Test_app_function_identity_without_admin_privileges: def test_app_function_no_admin_roles(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} iam_client = mock.MagicMock + iam_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -175,7 +182,7 @@ class Test_app_function_identity_without_admin_privileges: assert result[0].status == "PASS" assert ( result[0].status_extended - == "Function function1 has a managed identity enabled but without admin privileges." + == f"Function function1 from subscription {AZURE_SUBSCRIPTION_DISPLAY} has a managed identity enabled but without admin privileges." ) assert result[0].resource_id == function_id assert result[0].resource_name == "function1" @@ -184,7 +191,9 @@ class Test_app_function_identity_without_admin_privileges: def test_app_function_admin_roles(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} iam_client = mock.MagicMock + iam_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -212,7 +221,7 @@ class Test_app_function_identity_without_admin_privileges: function_id = str(uuid4()) function_scope = f"/subscriptions/{AZURE_SUBSCRIPTION_ID}/resourceGroups/rg/providers/Microsoft.Web/sites/function1" app_client.functions = { - "subscription-name-1": { + AZURE_SUBSCRIPTION_ID: { function_id: FunctionApp( id=function_id, name="function1", @@ -229,11 +238,11 @@ class Test_app_function_identity_without_admin_privileges: } iam_client.subscriptions = { - "subscription-name-1": AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME, } iam_client.role_assignments = { - "subscription-name-1": { + AZURE_SUBSCRIPTION_ID: { "role-assignment-id-2": RoleAssignment( id="role-assignment-id-2", name="role-assignment-name-2", @@ -246,7 +255,7 @@ class Test_app_function_identity_without_admin_privileges: } iam_client.roles = { - "subscription-name-1": { + AZURE_SUBSCRIPTION_ID: { f"/subscriptions/{AZURE_SUBSCRIPTION_ID}/providers/Microsoft.Authorization/roleDefinitions/{USER_ACCESS_ADMINISTRATOR_ROLE_ID}": Role( id=f"/subscriptions/{AZURE_SUBSCRIPTION_ID}/providers/Microsoft.Authorization/roleDefinitions/{USER_ACCESS_ADMINISTRATOR_ROLE_ID}", name="User Access Administrator", @@ -263,9 +272,9 @@ class Test_app_function_identity_without_admin_privileges: assert result[0].status == "FAIL" assert ( result[0].status_extended - == "Function function1 has a managed identity enabled and it is configure with admin privileges using role User Access Administrator." + == f"Function function1 from subscription {AZURE_SUBSCRIPTION_DISPLAY} has a managed identity enabled and it is configure with admin privileges using role User Access Administrator." ) assert result[0].resource_id == function_id assert result[0].resource_name == "function1" - assert result[0].subscription == "subscription-name-1" + assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].location == "West Europe" diff --git a/tests/providers/azure/services/app/app_function_latest_runtime_version/app_function_latest_runtime_version_test.py b/tests/providers/azure/services/app/app_function_latest_runtime_version/app_function_latest_runtime_version_test.py index 533874141d..89bfc642b0 100644 --- a/tests/providers/azure/services/app/app_function_latest_runtime_version/app_function_latest_runtime_version_test.py +++ b/tests/providers/azure/services/app/app_function_latest_runtime_version/app_function_latest_runtime_version_test.py @@ -2,7 +2,9 @@ from unittest import mock from uuid import uuid4 from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -10,6 +12,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_app_function_latest_runtime_version: def test_app_no_subscriptions(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -33,6 +36,7 @@ class Test_app_function_latest_runtime_version: def test_app_subscription_empty(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -56,6 +60,7 @@ class Test_app_function_latest_runtime_version: def test_app_function_runtime_is_latest(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -96,7 +101,7 @@ class Test_app_function_latest_runtime_version: assert len(result) == 1 assert result[0].status == "PASS" assert result[0].status_extended == ( - "Function function1 is using the latest runtime." + f"Function function1 from subscription {AZURE_SUBSCRIPTION_DISPLAY} is using the latest runtime." ) assert result[0].resource_id == function_id assert result[0].resource_name == "function1" @@ -105,6 +110,7 @@ class Test_app_function_latest_runtime_version: def test_app_function_runtime_is_not_latest(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -145,7 +151,7 @@ class Test_app_function_latest_runtime_version: assert len(result) == 1 assert result[0].status == "FAIL" assert result[0].status_extended == ( - "Function function1 is not using the latest runtime. The current runtime is '2' and should be '~4'." + f"Function function1 from subscription {AZURE_SUBSCRIPTION_DISPLAY} is not using the latest runtime. The current runtime is '2' and should be '~4'." ) assert result[0].resource_id == function_id assert result[0].resource_name == "function1" diff --git a/tests/providers/azure/services/app/app_function_not_publicly_accessible/app_function_not_publicly_accessible_test.py b/tests/providers/azure/services/app/app_function_not_publicly_accessible/app_function_not_publicly_accessible_test.py index 749a094e65..3c60aebc20 100644 --- a/tests/providers/azure/services/app/app_function_not_publicly_accessible/app_function_not_publicly_accessible_test.py +++ b/tests/providers/azure/services/app/app_function_not_publicly_accessible/app_function_not_publicly_accessible_test.py @@ -2,7 +2,9 @@ from unittest import mock from uuid import uuid4 from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -10,6 +12,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_app_function_not_publicly_accessible: def test_app_no_subscriptions(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -33,6 +36,7 @@ class Test_app_function_not_publicly_accessible: def test_app_subscription_empty(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -56,6 +60,7 @@ class Test_app_function_not_publicly_accessible: def test_app_function_not_publicly_accessible(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -97,7 +102,7 @@ class Test_app_function_not_publicly_accessible: assert result[0].status == "PASS" assert ( result[0].status_extended - == "Function function1 is not publicly accessible." + == f"Function function1 from subscription {AZURE_SUBSCRIPTION_DISPLAY} is not publicly accessible." ) assert result[0].resource_name == "function1" assert result[0].resource_id == function_id @@ -106,6 +111,7 @@ class Test_app_function_not_publicly_accessible: def test_app_function_publicly_accessible(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -147,7 +153,7 @@ class Test_app_function_not_publicly_accessible: assert result[0].status == "FAIL" assert ( result[0].status_extended - == "Function function1 is publicly accessible." + == f"Function function1 from subscription {AZURE_SUBSCRIPTION_DISPLAY} is publicly accessible." ) assert result[0].resource_name == "function1" assert result[0].resource_id == function_id diff --git a/tests/providers/azure/services/app/app_function_vnet_integration_enabled/app_function_vnet_integration_enabled_test.py b/tests/providers/azure/services/app/app_function_vnet_integration_enabled/app_function_vnet_integration_enabled_test.py index 33a99d0086..f12422f1da 100644 --- a/tests/providers/azure/services/app/app_function_vnet_integration_enabled/app_function_vnet_integration_enabled_test.py +++ b/tests/providers/azure/services/app/app_function_vnet_integration_enabled/app_function_vnet_integration_enabled_test.py @@ -2,7 +2,9 @@ from unittest import mock from uuid import uuid4 from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -10,6 +12,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_app_function_vnet_integration_enabled: def test_app_no_subscriptions(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -33,6 +36,7 @@ class Test_app_function_vnet_integration_enabled: def test_app_subscription_empty(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -56,6 +60,7 @@ class Test_app_function_vnet_integration_enabled: def test_app_function_vnet_integration_enabled(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -97,14 +102,16 @@ class Test_app_function_vnet_integration_enabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == "Function function1 has Virtual Network integration enabled with subnet 'vnet_subnet_id' enabled." + == f"Function function1 from subscription {AZURE_SUBSCRIPTION_DISPLAY} has Virtual Network integration enabled with subnet 'vnet_subnet_id' enabled." ) assert result[0].resource_name == "function1" assert result[0].resource_id == function_id assert result[0].location == "West Europe" + assert result[0].subscription == AZURE_SUBSCRIPTION_ID def test_app_function_vnet_integration_disabled(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -146,8 +153,9 @@ class Test_app_function_vnet_integration_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == "Function function1 does not have virtual network integration enabled." + == f"Function function1 from subscription {AZURE_SUBSCRIPTION_DISPLAY} does not have virtual network integration enabled." ) assert result[0].resource_name == "function1" assert result[0].resource_id == function_id assert result[0].location == "West Europe" + assert result[0].subscription == AZURE_SUBSCRIPTION_ID diff --git a/tests/providers/azure/services/app/app_http_logs_enabled/app_http_logs_enabled_test.py b/tests/providers/azure/services/app/app_http_logs_enabled/app_http_logs_enabled_test.py index 7f24fe6284..6a8a0a1d83 100644 --- a/tests/providers/azure/services/app/app_http_logs_enabled/app_http_logs_enabled_test.py +++ b/tests/providers/azure/services/app/app_http_logs_enabled/app_http_logs_enabled_test.py @@ -1,7 +1,9 @@ from unittest import mock from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -10,6 +12,7 @@ class Test_app_http_logs_enabled: def test_app_http_logs_enabled_no_subscriptions(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} app_client.apps = {} with ( @@ -33,6 +36,7 @@ class Test_app_http_logs_enabled: def test_app_subscriptions_empty(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} app_client.apps = {AZURE_SUBSCRIPTION_ID: {}} with ( @@ -55,6 +59,7 @@ class Test_app_http_logs_enabled: def test_no_diagnostics_settings(self): app_client = mock.MagicMock() + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -93,12 +98,13 @@ class Test_app_http_logs_enabled: assert result[0].resource_id == "resource_id" assert ( result[0].status_extended - == f"App app1 does not have a diagnostic setting in subscription {AZURE_SUBSCRIPTION_ID}." + == f"App app1 does not have a diagnostic setting in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID def test_diagnostic_setting_configured(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -233,11 +239,12 @@ class Test_app_http_logs_enabled: assert result[0].resource_id == "resource_id2" assert ( result[0].status_extended - == f"App app_id-2 has HTTP Logs enabled in diagnostic setting name_diagnostic_setting2 in subscription {AZURE_SUBSCRIPTION_ID}" + == f"App app_id-2 has HTTP Logs enabled in diagnostic setting name_diagnostic_setting2 in subscription {AZURE_SUBSCRIPTION_DISPLAY}" ) def test_diagnostic_setting_with_all_logs_category_group(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -296,11 +303,12 @@ class Test_app_http_logs_enabled: assert result[0].resource_id == "resource_id3" assert ( result[0].status_extended - == f"App app_id-3 has allLogs category group which includes HTTP Logs enabled in diagnostic setting name_diagnostic_setting3 in subscription {AZURE_SUBSCRIPTION_ID}" + == f"App app_id-3 has allLogs category group which includes HTTP Logs enabled in diagnostic setting name_diagnostic_setting3 in subscription {AZURE_SUBSCRIPTION_DISPLAY}" ) def test_diagnostic_setting_with_all_logs_category_group_disabled(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -359,5 +367,5 @@ class Test_app_http_logs_enabled: assert result[0].resource_id == "resource_id4" assert ( result[0].status_extended - == f"App app_id-4 does not have HTTP Logs enabled in diagnostic setting name_diagnostic_setting4 in subscription {AZURE_SUBSCRIPTION_ID}" + == f"App app_id-4 does not have HTTP Logs enabled in diagnostic setting name_diagnostic_setting4 in subscription {AZURE_SUBSCRIPTION_DISPLAY}" ) diff --git a/tests/providers/azure/services/app/app_minimum_tls_version_12/app_minimum_tls_version_12_test.py b/tests/providers/azure/services/app/app_minimum_tls_version_12/app_minimum_tls_version_12_test.py index a3055c855a..41dc73d30d 100644 --- a/tests/providers/azure/services/app/app_minimum_tls_version_12/app_minimum_tls_version_12_test.py +++ b/tests/providers/azure/services/app/app_minimum_tls_version_12/app_minimum_tls_version_12_test.py @@ -2,7 +2,9 @@ from unittest import mock from uuid import uuid4 from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -10,6 +12,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_app_minimum_tls_version_12: def test_app_no_subscriptions(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} app_client.apps = {} with ( @@ -32,6 +35,7 @@ class Test_app_minimum_tls_version_12: def test_app_subscriptions_empty(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} app_client.apps = {AZURE_SUBSCRIPTION_ID: {}} with ( @@ -55,6 +59,7 @@ class Test_app_minimum_tls_version_12: def test_app_none_configurations(self): resource_id = f"/subscriptions/{uuid4()}" app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -91,7 +96,7 @@ class Test_app_minimum_tls_version_12: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Minimum TLS version is not set to 1.2 for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_ID}'." + == f"Minimum TLS version is not set to 1.2 for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'." ) assert result[0].resource_id == resource_id assert result[0].resource_name == "app_id-1" @@ -101,6 +106,7 @@ class Test_app_minimum_tls_version_12: def test_app_min_tls_version_12(self): resource_id = f"/subscriptions/{uuid4()}" app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -137,7 +143,7 @@ class Test_app_minimum_tls_version_12: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Minimum TLS version is set to 1.2 for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_ID}'." + == f"Minimum TLS version is set to 1.2 for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'." ) assert result[0].resource_id == resource_id assert result[0].resource_name == "app_id-1" @@ -147,6 +153,7 @@ class Test_app_minimum_tls_version_12: def test_app_min_tls_version_10(self): resource_id = f"/subscriptions/{uuid4()}" app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -183,7 +190,7 @@ class Test_app_minimum_tls_version_12: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Minimum TLS version is not set to 1.2 for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_ID}'." + == f"Minimum TLS version is not set to 1.2 for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'." ) assert result[0].resource_id == resource_id assert result[0].resource_name == "app_id-1" @@ -193,6 +200,7 @@ class Test_app_minimum_tls_version_12: def test_app_min_tls_version_13(self): resource_id = f"/subscriptions/{uuid4()}" app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -229,7 +237,7 @@ class Test_app_minimum_tls_version_12: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Minimum TLS version is set to 1.3 for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_ID}'." + == f"Minimum TLS version is set to 1.3 for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'." ) assert result[0].resource_id == resource_id assert result[0].resource_name == "app_id-1" diff --git a/tests/providers/azure/services/app/app_register_with_identity/app_register_with_identity_test.py b/tests/providers/azure/services/app/app_register_with_identity/app_register_with_identity_test.py index 785439fe0e..218a0ce136 100644 --- a/tests/providers/azure/services/app/app_register_with_identity/app_register_with_identity_test.py +++ b/tests/providers/azure/services/app/app_register_with_identity/app_register_with_identity_test.py @@ -2,7 +2,9 @@ from unittest import mock from uuid import uuid4 from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -10,6 +12,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_app_register_with_identity: def test_app_no_subscriptions(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} app_client.apps = {} with ( @@ -32,6 +35,7 @@ class Test_app_register_with_identity: def test_app_subscriptions_empty(self): app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} app_client.apps = {AZURE_SUBSCRIPTION_ID: {}} with ( @@ -55,6 +59,7 @@ class Test_app_register_with_identity: def test_app_none_configurations(self): resource_id = f"/subscriptions/{uuid4()}" app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -91,7 +96,7 @@ class Test_app_register_with_identity: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"App 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_ID}' does not have an identity configured." + == f"App 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}' does not have an identity configured." ) assert result[0].resource_id == resource_id assert result[0].resource_name == "app_id-1" @@ -101,6 +106,7 @@ class Test_app_register_with_identity: def test_app_identity(self): resource_id = f"/subscriptions/{uuid4()}" app_client = mock.MagicMock + app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -137,7 +143,7 @@ class Test_app_register_with_identity: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"App 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_ID}' has an identity configured." + == f"App 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}' has an identity configured." ) assert result[0].resource_id == resource_id assert result[0].resource_name == "app_id-1" diff --git a/tests/providers/azure/services/appinsights/appinsights_ensure_is_configured/appinsights_ensure_is_configured_test.py b/tests/providers/azure/services/appinsights/appinsights_ensure_is_configured/appinsights_ensure_is_configured_test.py index 4982a718cc..513414ce57 100644 --- a/tests/providers/azure/services/appinsights/appinsights_ensure_is_configured/appinsights_ensure_is_configured_test.py +++ b/tests/providers/azure/services/appinsights/appinsights_ensure_is_configured/appinsights_ensure_is_configured_test.py @@ -2,7 +2,9 @@ from unittest import mock from prowler.providers.azure.services.appinsights.appinsights_service import Component from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -10,6 +12,9 @@ from tests.providers.azure.azure_fixtures import ( class Test_appinsights_ensure_is_configured: def test_appinsights_no_subscriptions(self): appinsights_client = mock.MagicMock + appinsights_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } appinsights_client.components = {} with ( @@ -34,7 +39,7 @@ class Test_appinsights_ensure_is_configured: appinsights_client = mock.MagicMock appinsights_client.components = {AZURE_SUBSCRIPTION_ID: {}} appinsights_client.subscriptions = { - AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_ID + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME } with ( @@ -60,7 +65,7 @@ class Test_appinsights_ensure_is_configured: assert result[0].resource_name == AZURE_SUBSCRIPTION_ID assert ( result[0].status_extended - == f"There are no AppInsight configured in subscription {AZURE_SUBSCRIPTION_ID}." + == f"There are no AppInsight configured in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) def test_appinsights_configured(self): @@ -76,7 +81,7 @@ class Test_appinsights_ensure_is_configured: } } appinsights_client.subscriptions = { - AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_ID + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME } with ( @@ -103,5 +108,5 @@ class Test_appinsights_ensure_is_configured: assert result[0].location == "global" assert ( result[0].status_extended - == f"There is at least one AppInsight configured in subscription {AZURE_SUBSCRIPTION_ID}." + == f"There is at least one AppInsight configured in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) diff --git a/tests/providers/azure/services/containerregistry/containerregistry_admin_user_disabled/containerregistry_admin_user_disabled_test.py b/tests/providers/azure/services/containerregistry/containerregistry_admin_user_disabled/containerregistry_admin_user_disabled_test.py index a2295b6d06..62cc55b1d3 100644 --- a/tests/providers/azure/services/containerregistry/containerregistry_admin_user_disabled/containerregistry_admin_user_disabled_test.py +++ b/tests/providers/azure/services/containerregistry/containerregistry_admin_user_disabled/containerregistry_admin_user_disabled_test.py @@ -3,7 +3,9 @@ from unittest.mock import MagicMock from uuid import uuid4 from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,9 @@ from tests.providers.azure.azure_fixtures import ( class TestContainerRegistryAdminUserDisabled: def test_no_container_registries(self): containerregistry_client = MagicMock() + containerregistry_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } containerregistry_client.registries = {} with ( @@ -33,6 +38,9 @@ class TestContainerRegistryAdminUserDisabled: def test_container_registry_admin_user_enabled(self): containerregistry_client = MagicMock() + containerregistry_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } registry_id = str(uuid4()) with ( @@ -76,7 +84,7 @@ class TestContainerRegistryAdminUserDisabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Container Registry mock_registry from subscription {AZURE_SUBSCRIPTION_ID} has its admin user enabled." + == f"Container Registry mock_registry from subscription {AZURE_SUBSCRIPTION_DISPLAY} has its admin user enabled." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "mock_registry" @@ -90,6 +98,9 @@ class TestContainerRegistryAdminUserDisabled: def test_container_registry_admin_user_disabled(self): containerregistry_client = mock.MagicMock() + containerregistry_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } containerregistry_client.registries = {} with ( @@ -135,7 +146,7 @@ class TestContainerRegistryAdminUserDisabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Container Registry mock_registry from subscription {AZURE_SUBSCRIPTION_ID} has its admin user disabled." + == f"Container Registry mock_registry from subscription {AZURE_SUBSCRIPTION_DISPLAY} has its admin user disabled." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "mock_registry" diff --git a/tests/providers/azure/services/containerregistry/containerregistry_not_publicly_accessible/containerregistry_not_publicly_accessible_test.py b/tests/providers/azure/services/containerregistry/containerregistry_not_publicly_accessible/containerregistry_not_publicly_accessible_test.py index 683552daca..40b6550382 100644 --- a/tests/providers/azure/services/containerregistry/containerregistry_not_publicly_accessible/containerregistry_not_publicly_accessible_test.py +++ b/tests/providers/azure/services/containerregistry/containerregistry_not_publicly_accessible/containerregistry_not_publicly_accessible_test.py @@ -3,7 +3,9 @@ from unittest.mock import MagicMock from uuid import uuid4 from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,9 @@ from tests.providers.azure.azure_fixtures import ( class Test_containerregistry_not_publicly_accessible: def test_no_container_registries(self): containerregistry_client = MagicMock() + containerregistry_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } containerregistry_client.registries = {} with ( @@ -33,6 +38,9 @@ class Test_containerregistry_not_publicly_accessible: def test_container_registry_network_access_unrestricted(self): containerregistry_client = MagicMock() + containerregistry_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } registry_id = str(uuid4()) with ( @@ -93,7 +101,7 @@ class Test_containerregistry_not_publicly_accessible: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Container Registry {containerregistry_client.registries[AZURE_SUBSCRIPTION_ID][registry_id].name} from subscription {AZURE_SUBSCRIPTION_ID} allows unrestricted network access." + == f"Container Registry {containerregistry_client.registries[AZURE_SUBSCRIPTION_ID][registry_id].name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} allows unrestricted network access." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "mock_registry" @@ -107,6 +115,9 @@ class Test_containerregistry_not_publicly_accessible: def test_container_registry_network_access_restricted(self): containerregistry_client = mock.MagicMock() + containerregistry_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } containerregistry_client.registries = {} with ( @@ -168,7 +179,7 @@ class Test_containerregistry_not_publicly_accessible: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Container Registry {containerregistry_client.registries[AZURE_SUBSCRIPTION_ID][registry_id].name} from subscription {AZURE_SUBSCRIPTION_ID} does not allow unrestricted network access." + == f"Container Registry {containerregistry_client.registries[AZURE_SUBSCRIPTION_ID][registry_id].name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} does not allow unrestricted network access." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "mock_registry" diff --git a/tests/providers/azure/services/containerregistry/containerregistry_uses_private_link/containerregistry_uses_private_link_test.py b/tests/providers/azure/services/containerregistry/containerregistry_uses_private_link/containerregistry_uses_private_link_test.py index f8b9237a21..aa0afd1742 100644 --- a/tests/providers/azure/services/containerregistry/containerregistry_uses_private_link/containerregistry_uses_private_link_test.py +++ b/tests/providers/azure/services/containerregistry/containerregistry_uses_private_link/containerregistry_uses_private_link_test.py @@ -3,7 +3,9 @@ from unittest.mock import MagicMock from uuid import uuid4 from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,9 @@ from tests.providers.azure.azure_fixtures import ( class Test_containerregistry_uses_private_link: def test_no_container_registries(self): containerregistry_client = MagicMock() + containerregistry_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } containerregistry_client.registries = {} with ( @@ -33,6 +38,9 @@ class Test_containerregistry_uses_private_link: def test_container_registry_not_uses_private_link(self): containerregistry_client = MagicMock() + containerregistry_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } registry_id = str(uuid4()) with ( @@ -76,7 +84,7 @@ class Test_containerregistry_uses_private_link: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Container Registry mock_registry from subscription {AZURE_SUBSCRIPTION_ID} does not use a private link." + == f"Container Registry mock_registry from subscription {AZURE_SUBSCRIPTION_DISPLAY} does not use a private link." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "mock_registry" @@ -90,6 +98,9 @@ class Test_containerregistry_uses_private_link: def test_container_registry_uses_private_link(self): containerregistry_client = mock.MagicMock() + containerregistry_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } containerregistry_client.registries = {} with ( @@ -141,7 +152,7 @@ class Test_containerregistry_uses_private_link: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Container Registry mock_registry from subscription {AZURE_SUBSCRIPTION_ID} uses a private link." + == f"Container Registry mock_registry from subscription {AZURE_SUBSCRIPTION_DISPLAY} uses a private link." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "mock_registry" diff --git a/tests/providers/azure/services/cosmosdb/cosmosdb_account_firewall_use_selected_networks/cosmosdb_account_firewall_use_selected_networks_test.py b/tests/providers/azure/services/cosmosdb/cosmosdb_account_firewall_use_selected_networks/cosmosdb_account_firewall_use_selected_networks_test.py index 7d7b793954..7f27f2ebf1 100644 --- a/tests/providers/azure/services/cosmosdb/cosmosdb_account_firewall_use_selected_networks/cosmosdb_account_firewall_use_selected_networks_test.py +++ b/tests/providers/azure/services/cosmosdb/cosmosdb_account_firewall_use_selected_networks/cosmosdb_account_firewall_use_selected_networks_test.py @@ -3,7 +3,9 @@ from uuid import uuid4 from prowler.providers.azure.services.cosmosdb.cosmosdb_service import Account from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_cosmosdb_account_firewall_use_selected_networks: def test_no_accounts(self): cosmosdb_client = mock.MagicMock + cosmosdb_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} cosmosdb_client.accounts = {} with ( @@ -33,6 +36,7 @@ class Test_cosmosdb_account_firewall_use_selected_networks: def test_accounts_no_virtual_network_filter_enabled(self): cosmosdb_client = mock.MagicMock + cosmosdb_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} account_name = "Account Name" account_id = str(uuid4()) cosmosdb_client.accounts = { @@ -71,7 +75,7 @@ class Test_cosmosdb_account_firewall_use_selected_networks: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"CosmosDB account {account_name} from subscription {AZURE_SUBSCRIPTION_ID} has firewall rules that allow access from all networks." + == f"CosmosDB account {account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has firewall rules that allow access from all networks." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == account_name @@ -80,6 +84,7 @@ class Test_cosmosdb_account_firewall_use_selected_networks: def test_accounts_virtual_network_filter_enabled(self): cosmosdb_client = mock.MagicMock + cosmosdb_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} account_name = "Account Name" account_id = str(uuid4()) cosmosdb_client.accounts = { @@ -118,7 +123,7 @@ class Test_cosmosdb_account_firewall_use_selected_networks: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"CosmosDB account {account_name} from subscription {AZURE_SUBSCRIPTION_ID} has firewall rules that allow access only from selected networks." + == f"CosmosDB account {account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has firewall rules that allow access only from selected networks." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == account_name diff --git a/tests/providers/azure/services/cosmosdb/cosmosdb_account_use_aad_and_rbac/cosmosdb_account_use_aad_and_rbac_test.py b/tests/providers/azure/services/cosmosdb/cosmosdb_account_use_aad_and_rbac/cosmosdb_account_use_aad_and_rbac_test.py index 8fcdf99fa9..5430469cfd 100644 --- a/tests/providers/azure/services/cosmosdb/cosmosdb_account_use_aad_and_rbac/cosmosdb_account_use_aad_and_rbac_test.py +++ b/tests/providers/azure/services/cosmosdb/cosmosdb_account_use_aad_and_rbac/cosmosdb_account_use_aad_and_rbac_test.py @@ -3,7 +3,9 @@ from uuid import uuid4 from prowler.providers.azure.services.cosmosdb.cosmosdb_service import Account from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_cosmosdb_account_use_aad_and_rbac: def test_no_accounts(self): cosmosdb_client = mock.MagicMock + cosmosdb_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} cosmosdb_client.accounts = {} with ( @@ -33,6 +36,7 @@ class Test_cosmosdb_account_use_aad_and_rbac: def test_accounts_disable_local_auth_false(self): cosmosdb_client = mock.MagicMock + cosmosdb_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} account_name = "Account Name" account_id = str(uuid4()) cosmosdb_client.accounts = { @@ -71,7 +75,7 @@ class Test_cosmosdb_account_use_aad_and_rbac: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"CosmosDB account {account_name} from subscription {AZURE_SUBSCRIPTION_ID} is not using AAD and RBAC" + == f"CosmosDB account {account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} is not using AAD and RBAC" ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == account_name @@ -80,6 +84,7 @@ class Test_cosmosdb_account_use_aad_and_rbac: def test_accounts_disable_local_auth_true(self): cosmosdb_client = mock.MagicMock + cosmosdb_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} account_name = "Account Name" account_id = str(uuid4()) cosmosdb_client.accounts = { @@ -118,7 +123,7 @@ class Test_cosmosdb_account_use_aad_and_rbac: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"CosmosDB account {account_name} from subscription {AZURE_SUBSCRIPTION_ID} is using AAD and RBAC" + == f"CosmosDB account {account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} is using AAD and RBAC" ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == account_name diff --git a/tests/providers/azure/services/cosmosdb/cosmosdb_account_use_private_endpoints/cosmosdb_account_use_private_endpoints_test.py b/tests/providers/azure/services/cosmosdb/cosmosdb_account_use_private_endpoints/cosmosdb_account_use_private_endpoints_test.py index d3827dee0b..1c62ca289e 100644 --- a/tests/providers/azure/services/cosmosdb/cosmosdb_account_use_private_endpoints/cosmosdb_account_use_private_endpoints_test.py +++ b/tests/providers/azure/services/cosmosdb/cosmosdb_account_use_private_endpoints/cosmosdb_account_use_private_endpoints_test.py @@ -5,7 +5,9 @@ from azure.mgmt.cosmosdb.models import PrivateEndpointConnection from prowler.providers.azure.services.cosmosdb.cosmosdb_service import Account from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -13,6 +15,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_cosmosdb_account_use_private_endpoints: def test_no_accounts(self): cosmosdb_client = mock.MagicMock + cosmosdb_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} cosmosdb_client.accounts = {} with ( @@ -35,6 +38,7 @@ class Test_cosmosdb_account_use_private_endpoints: def test_accounts_no_private_endpoints_connections(self): cosmosdb_client = mock.MagicMock + cosmosdb_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} account_name = "Account Name" account_id = str(uuid4()) cosmosdb_client.accounts = { @@ -73,7 +77,7 @@ class Test_cosmosdb_account_use_private_endpoints: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"CosmosDB account {account_name} from subscription {AZURE_SUBSCRIPTION_ID} is not using private endpoints connections" + == f"CosmosDB account {account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} is not using private endpoints connections" ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == account_name @@ -82,6 +86,7 @@ class Test_cosmosdb_account_use_private_endpoints: def test_accounts_private_endpoints_connections(self): cosmosdb_client = mock.MagicMock + cosmosdb_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} account_name = "Account Name" account_id = str(uuid4()) cosmosdb_client.accounts = { @@ -124,7 +129,7 @@ class Test_cosmosdb_account_use_private_endpoints: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"CosmosDB account {account_name} from subscription {AZURE_SUBSCRIPTION_ID} is using private endpoints connections" + == f"CosmosDB account {account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} is using private endpoints connections" ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == account_name diff --git a/tests/providers/azure/services/databricks/databricks_workspace_cmk_encryption_enabled/databricks_workspace_cmk_encryption_enabled_test.py b/tests/providers/azure/services/databricks/databricks_workspace_cmk_encryption_enabled/databricks_workspace_cmk_encryption_enabled_test.py index 1e145d4f80..679aec0fff 100644 --- a/tests/providers/azure/services/databricks/databricks_workspace_cmk_encryption_enabled/databricks_workspace_cmk_encryption_enabled_test.py +++ b/tests/providers/azure/services/databricks/databricks_workspace_cmk_encryption_enabled/databricks_workspace_cmk_encryption_enabled_test.py @@ -6,7 +6,9 @@ from prowler.providers.azure.services.databricks.databricks_service import ( ManagedDiskEncryption, ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -14,6 +16,9 @@ from tests.providers.azure.azure_fixtures import ( class Test_databricks_workspace_cmk_encryption_enabled: def test_no_databricks_workspaces(self): databricks_client = mock.MagicMock + databricks_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } databricks_client.workspaces = {} with ( @@ -39,6 +44,9 @@ class Test_databricks_workspace_cmk_encryption_enabled: workspace_name = "test-workspace" databricks_client = mock.MagicMock + databricks_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } databricks_client.workspaces = { AZURE_SUBSCRIPTION_ID: { workspace_id: DatabricksWorkspace( @@ -71,7 +79,7 @@ class Test_databricks_workspace_cmk_encryption_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Databricks workspace {workspace_name} in subscription {AZURE_SUBSCRIPTION_ID} does not have customer-managed key (CMK) encryption enabled." + == f"Databricks workspace {workspace_name} in subscription {AZURE_SUBSCRIPTION_DISPLAY} does not have customer-managed key (CMK) encryption enabled." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == workspace_name @@ -86,6 +94,9 @@ class Test_databricks_workspace_cmk_encryption_enabled: key_vault_uri = "test-vault-uri" databricks_client = mock.MagicMock + databricks_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } databricks_client.workspaces = { AZURE_SUBSCRIPTION_ID: { workspace_id: DatabricksWorkspace( @@ -122,7 +133,7 @@ class Test_databricks_workspace_cmk_encryption_enabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Databricks workspace {workspace_name} in subscription {AZURE_SUBSCRIPTION_ID} has customer-managed key (CMK) encryption enabled with key {key_vault_uri}/{key_name}/{key_version}." + == f"Databricks workspace {workspace_name} in subscription {AZURE_SUBSCRIPTION_DISPLAY} has customer-managed key (CMK) encryption enabled with key {key_vault_uri}/{key_name}/{key_version}." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == workspace_name diff --git a/tests/providers/azure/services/databricks/databricks_workspace_vnet_injection_enabled/databricks_workspace_vnet_injection_enabled_test.py b/tests/providers/azure/services/databricks/databricks_workspace_vnet_injection_enabled/databricks_workspace_vnet_injection_enabled_test.py index 912ee363ac..f8f9b7bd2c 100644 --- a/tests/providers/azure/services/databricks/databricks_workspace_vnet_injection_enabled/databricks_workspace_vnet_injection_enabled_test.py +++ b/tests/providers/azure/services/databricks/databricks_workspace_vnet_injection_enabled/databricks_workspace_vnet_injection_enabled_test.py @@ -5,7 +5,9 @@ from prowler.providers.azure.services.databricks.databricks_service import ( DatabricksWorkspace, ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -13,6 +15,9 @@ from tests.providers.azure.azure_fixtures import ( class Test_databricks_workspace_vnet_injection_enabled: def test_databricks_no_workspaces(self): databricks_client = mock.MagicMock + databricks_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } databricks_client.workspaces = {} with ( @@ -37,6 +42,9 @@ class Test_databricks_workspace_vnet_injection_enabled: workspace_id = str(uuid4()) workspace_name = "test-workspace" databricks_client = mock.MagicMock + databricks_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } databricks_client.workspaces = { AZURE_SUBSCRIPTION_ID: { workspace_id: DatabricksWorkspace( @@ -68,7 +76,7 @@ class Test_databricks_workspace_vnet_injection_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Databricks workspace {workspace_name} in subscription {AZURE_SUBSCRIPTION_ID} is not deployed in a customer-managed VNet (VNet Injection is not enabled)." + == f"Databricks workspace {workspace_name} in subscription {AZURE_SUBSCRIPTION_DISPLAY} is not deployed in a customer-managed VNet (VNet Injection is not enabled)." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == workspace_name @@ -80,6 +88,9 @@ class Test_databricks_workspace_vnet_injection_enabled: workspace_name = "test-workspace" vnet_id = "test-vnet-id" databricks_client = mock.MagicMock + databricks_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } databricks_client.workspaces = { AZURE_SUBSCRIPTION_ID: { workspace_id: DatabricksWorkspace( @@ -111,7 +122,7 @@ class Test_databricks_workspace_vnet_injection_enabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Databricks workspace {workspace_name} in subscription {AZURE_SUBSCRIPTION_ID} is deployed in a customer-managed VNet ({vnet_id})." + == f"Databricks workspace {workspace_name} in subscription {AZURE_SUBSCRIPTION_DISPLAY} is deployed in a customer-managed VNet ({vnet_id})." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == workspace_name diff --git a/tests/providers/azure/services/defender/defender_additional_email_configured_with_a_security_contact/defender_additional_email_configured_with_a_security_contact_test.py b/tests/providers/azure/services/defender/defender_additional_email_configured_with_a_security_contact/defender_additional_email_configured_with_a_security_contact_test.py index 752c8a6641..75f3d5014a 100644 --- a/tests/providers/azure/services/defender/defender_additional_email_configured_with_a_security_contact/defender_additional_email_configured_with_a_security_contact_test.py +++ b/tests/providers/azure/services/defender/defender_additional_email_configured_with_a_security_contact/defender_additional_email_configured_with_a_security_contact_test.py @@ -6,7 +6,9 @@ from prowler.providers.azure.services.defender.defender_service import ( SecurityContactConfiguration, ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -14,6 +16,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_defender_additional_email_configured_with_a_security_contact: def test_defender_no_subscriptions(self): defender_client = mock.MagicMock() + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.security_contact_configurations = {} with ( @@ -37,6 +40,7 @@ class Test_defender_additional_email_configured_with_a_security_contact: def test_defender_no_additional_emails(self): resource_id = str(uuid4()) defender_client = mock.MagicMock() + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.security_contact_configurations = { AZURE_SUBSCRIPTION_ID: { resource_id: SecurityContactConfiguration( @@ -74,7 +78,7 @@ class Test_defender_additional_email_configured_with_a_security_contact: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"There is not another correct email configured for subscription {AZURE_SUBSCRIPTION_ID}." + == f"There is not another correct email configured for subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "default" @@ -83,6 +87,7 @@ class Test_defender_additional_email_configured_with_a_security_contact: def test_defender_additional_email_configured(self): resource_id = str(uuid4()) defender_client = mock.MagicMock() + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.security_contact_configurations = { AZURE_SUBSCRIPTION_ID: { resource_id: SecurityContactConfiguration( @@ -120,7 +125,7 @@ class Test_defender_additional_email_configured_with_a_security_contact: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"There is another correct email configured for subscription {AZURE_SUBSCRIPTION_ID}." + == f"There is another correct email configured for subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "default" diff --git a/tests/providers/azure/services/defender/defender_assessments_vm_endpoint_protection_installed/defender_assessments_vm_endpoint_protection_installed_test.py b/tests/providers/azure/services/defender/defender_assessments_vm_endpoint_protection_installed/defender_assessments_vm_endpoint_protection_installed_test.py index dd16c7571c..1e567ac153 100644 --- a/tests/providers/azure/services/defender/defender_assessments_vm_endpoint_protection_installed/defender_assessments_vm_endpoint_protection_installed_test.py +++ b/tests/providers/azure/services/defender/defender_assessments_vm_endpoint_protection_installed/defender_assessments_vm_endpoint_protection_installed_test.py @@ -3,7 +3,9 @@ from uuid import uuid4 from prowler.providers.azure.services.defender.defender_service import Assesment from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_defender_assessments_vm_endpoint_protection_installed: def test_defender_no_subscriptions(self): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.assessments = {} with ( @@ -33,6 +36,7 @@ class Test_defender_assessments_vm_endpoint_protection_installed: def test_defender_subscriptions_with_no_assessments(self): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.assessments = {AZURE_SUBSCRIPTION_ID: {}} with ( @@ -55,6 +59,7 @@ class Test_defender_assessments_vm_endpoint_protection_installed: def test_defender_subscriptions_with_healthy_assessments(self): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} resource_id = str(uuid4()) defender_client.assessments = { AZURE_SUBSCRIPTION_ID: { @@ -86,13 +91,14 @@ class Test_defender_assessments_vm_endpoint_protection_installed: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Endpoint protection is set up in all VMs in subscription {AZURE_SUBSCRIPTION_ID}." + == f"Endpoint protection is set up in all VMs in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) assert result[0].resource_name == "vm1" assert result[0].resource_id == resource_id def test_defender_subscriptions_with_unhealthy_assessments(self): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} resource_id = str(uuid4()) defender_client.assessments = { AZURE_SUBSCRIPTION_ID: { @@ -124,7 +130,7 @@ class Test_defender_assessments_vm_endpoint_protection_installed: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Endpoint protection is not set up in all VMs in subscription {AZURE_SUBSCRIPTION_ID}." + == f"Endpoint protection is not set up in all VMs in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) assert result[0].resource_name == "vm1" assert result[0].resource_id == resource_id diff --git a/tests/providers/azure/services/defender/defender_attack_path_notifications_properly_configured/defender_attack_path_notifications_properly_configured_test.py b/tests/providers/azure/services/defender/defender_attack_path_notifications_properly_configured/defender_attack_path_notifications_properly_configured_test.py index cd21783149..ebece2e029 100644 --- a/tests/providers/azure/services/defender/defender_attack_path_notifications_properly_configured/defender_attack_path_notifications_properly_configured_test.py +++ b/tests/providers/azure/services/defender/defender_attack_path_notifications_properly_configured/defender_attack_path_notifications_properly_configured_test.py @@ -6,7 +6,9 @@ from prowler.providers.azure.services.defender.defender_service import ( SecurityContactConfiguration, ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -14,6 +16,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_defender_attack_path_notifications_properly_configured: def test_no_subscriptions(self): defender_client = mock.MagicMock() + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.security_contact_configurations = {} defender_client.audit_config = {} with ( @@ -38,6 +41,7 @@ class Test_defender_attack_path_notifications_properly_configured: resource_id = str(uuid4()) contact_name = "default" defender_client = mock.MagicMock() + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.security_contact_configurations = { AZURE_SUBSCRIPTION_ID: { resource_id: SecurityContactConfiguration( @@ -74,7 +78,7 @@ class Test_defender_attack_path_notifications_properly_configured: assert len(result) == 1 assert result[0].status == "FAIL" assert result[0].status_extended == ( - f"Attack path notifications are not enabled in subscription {AZURE_SUBSCRIPTION_ID} for security contact {contact_name}." + f"Attack path notifications are not enabled in subscription {AZURE_SUBSCRIPTION_DISPLAY} for security contact {contact_name}." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == contact_name @@ -85,6 +89,7 @@ class Test_defender_attack_path_notifications_properly_configured: resource_id = str(uuid4()) contact_name = "default" defender_client = mock.MagicMock() + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.security_contact_configurations = { AZURE_SUBSCRIPTION_ID: { resource_id: SecurityContactConfiguration( @@ -123,7 +128,7 @@ class Test_defender_attack_path_notifications_properly_configured: assert len(result) == 1 assert result[0].status == "PASS" assert result[0].status_extended == ( - f"Attack path notifications are enabled with minimal risk level Medium in subscription {AZURE_SUBSCRIPTION_ID} for security contact {contact_name}." + f"Attack path notifications are enabled with minimal risk level Medium in subscription {AZURE_SUBSCRIPTION_DISPLAY} for security contact {contact_name}." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == contact_name @@ -134,6 +139,7 @@ class Test_defender_attack_path_notifications_properly_configured: resource_id = str(uuid4()) contact_name = "default" defender_client = mock.MagicMock() + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.security_contact_configurations = { AZURE_SUBSCRIPTION_ID: { resource_id: SecurityContactConfiguration( @@ -172,7 +178,7 @@ class Test_defender_attack_path_notifications_properly_configured: assert len(result) == 1 assert result[0].status == "PASS" assert result[0].status_extended == ( - f"Attack path notifications are enabled with minimal risk level Medium in subscription {AZURE_SUBSCRIPTION_ID} for security contact {contact_name}." + f"Attack path notifications are enabled with minimal risk level Medium in subscription {AZURE_SUBSCRIPTION_DISPLAY} for security contact {contact_name}." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == contact_name @@ -183,6 +189,7 @@ class Test_defender_attack_path_notifications_properly_configured: resource_id = str(uuid4()) contact_name = "default" defender_client = mock.MagicMock() + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.security_contact_configurations = { AZURE_SUBSCRIPTION_ID: { resource_id: SecurityContactConfiguration( @@ -219,7 +226,7 @@ class Test_defender_attack_path_notifications_properly_configured: assert len(result) == 1 assert result[0].status == "PASS" assert result[0].status_extended == ( - f"Attack path notifications are enabled with minimal risk level Low in subscription {AZURE_SUBSCRIPTION_ID} for security contact {contact_name}." + f"Attack path notifications are enabled with minimal risk level Low in subscription {AZURE_SUBSCRIPTION_DISPLAY} for security contact {contact_name}." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == contact_name @@ -230,6 +237,7 @@ class Test_defender_attack_path_notifications_properly_configured: resource_id = str(uuid4()) contact_name = "default" defender_client = mock.MagicMock() + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.security_contact_configurations = { AZURE_SUBSCRIPTION_ID: { resource_id: SecurityContactConfiguration( @@ -266,7 +274,7 @@ class Test_defender_attack_path_notifications_properly_configured: assert len(result) == 1 assert result[0].status == "PASS" assert result[0].status_extended == ( - f"Attack path notifications are enabled with minimal risk level Medium in subscription {AZURE_SUBSCRIPTION_ID} for security contact {contact_name}." + f"Attack path notifications are enabled with minimal risk level Medium in subscription {AZURE_SUBSCRIPTION_DISPLAY} for security contact {contact_name}." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == contact_name @@ -277,6 +285,7 @@ class Test_defender_attack_path_notifications_properly_configured: resource_id = str(uuid4()) contact_name = "default" defender_client = mock.MagicMock() + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.security_contact_configurations = { AZURE_SUBSCRIPTION_ID: { resource_id: SecurityContactConfiguration( @@ -313,7 +322,7 @@ class Test_defender_attack_path_notifications_properly_configured: assert len(result) == 1 assert result[0].status == "PASS" assert result[0].status_extended == ( - f"Attack path notifications are enabled with minimal risk level High in subscription {AZURE_SUBSCRIPTION_ID} for security contact {contact_name}." + f"Attack path notifications are enabled with minimal risk level High in subscription {AZURE_SUBSCRIPTION_DISPLAY} for security contact {contact_name}." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == contact_name @@ -324,6 +333,7 @@ class Test_defender_attack_path_notifications_properly_configured: resource_id = str(uuid4()) contact_name = "default" defender_client = mock.MagicMock() + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.security_contact_configurations = { AZURE_SUBSCRIPTION_ID: { resource_id: SecurityContactConfiguration( @@ -360,7 +370,7 @@ class Test_defender_attack_path_notifications_properly_configured: assert len(result) == 1 assert result[0].status == "FAIL" assert result[0].status_extended == ( - f"Attack path notifications are enabled with minimal risk level Critical in subscription {AZURE_SUBSCRIPTION_ID} for security contact {contact_name}." + f"Attack path notifications are enabled with minimal risk level Critical in subscription {AZURE_SUBSCRIPTION_DISPLAY} for security contact {contact_name}." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == contact_name diff --git a/tests/providers/azure/services/defender/defender_auto_provisioning_log_analytics_agent_vms_on/defender_auto_provisioning_log_analytics_agent_vms_on_test.py b/tests/providers/azure/services/defender/defender_auto_provisioning_log_analytics_agent_vms_on/defender_auto_provisioning_log_analytics_agent_vms_on_test.py index 3f39654c2b..9a99281e94 100644 --- a/tests/providers/azure/services/defender/defender_auto_provisioning_log_analytics_agent_vms_on/defender_auto_provisioning_log_analytics_agent_vms_on_test.py +++ b/tests/providers/azure/services/defender/defender_auto_provisioning_log_analytics_agent_vms_on/defender_auto_provisioning_log_analytics_agent_vms_on_test.py @@ -5,7 +5,9 @@ from prowler.providers.azure.services.defender.defender_service import ( AutoProvisioningSetting, ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -13,6 +15,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_defender_auto_provisioning_log_analytics_agent_vms_on: def test_defender_no_app_services(self): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.auto_provisioning_settings = {} with ( @@ -36,6 +39,7 @@ class Test_defender_auto_provisioning_log_analytics_agent_vms_on: def test_defender_auto_provisioning_log_analytics_off(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.auto_provisioning_settings = { AZURE_SUBSCRIPTION_ID: { "default": AutoProvisioningSetting( @@ -67,7 +71,7 @@ class Test_defender_auto_provisioning_log_analytics_agent_vms_on: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Defender Auto Provisioning Log Analytics Agents from subscription {AZURE_SUBSCRIPTION_ID} is set to OFF." + == f"Defender Auto Provisioning Log Analytics Agents from subscription {AZURE_SUBSCRIPTION_DISPLAY} is set to OFF." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "default" @@ -76,6 +80,7 @@ class Test_defender_auto_provisioning_log_analytics_agent_vms_on: def test_defender_auto_provisioning_log_analytics_on(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.auto_provisioning_settings = { AZURE_SUBSCRIPTION_ID: { "default": AutoProvisioningSetting( @@ -107,7 +112,7 @@ class Test_defender_auto_provisioning_log_analytics_agent_vms_on: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Defender Auto Provisioning Log Analytics Agents from subscription {AZURE_SUBSCRIPTION_ID} is set to ON." + == f"Defender Auto Provisioning Log Analytics Agents from subscription {AZURE_SUBSCRIPTION_DISPLAY} is set to ON." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "default" @@ -116,6 +121,7 @@ class Test_defender_auto_provisioning_log_analytics_agent_vms_on: def test_defender_auto_provisioning_log_analytics_on_and_off(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.auto_provisioning_settings = { AZURE_SUBSCRIPTION_ID: { "default": AutoProvisioningSetting( @@ -153,7 +159,7 @@ class Test_defender_auto_provisioning_log_analytics_agent_vms_on: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Defender Auto Provisioning Log Analytics Agents from subscription {AZURE_SUBSCRIPTION_ID} is set to ON." + == f"Defender Auto Provisioning Log Analytics Agents from subscription {AZURE_SUBSCRIPTION_DISPLAY} is set to ON." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "default" @@ -162,7 +168,7 @@ class Test_defender_auto_provisioning_log_analytics_agent_vms_on: assert result[1].status == "FAIL" assert ( result[1].status_extended - == f"Defender Auto Provisioning Log Analytics Agents from subscription {AZURE_SUBSCRIPTION_ID} is set to OFF." + == f"Defender Auto Provisioning Log Analytics Agents from subscription {AZURE_SUBSCRIPTION_DISPLAY} is set to OFF." ) assert result[1].subscription == AZURE_SUBSCRIPTION_ID assert result[1].resource_name == "default2" diff --git a/tests/providers/azure/services/defender/defender_auto_provisioning_vulnerabilty_assessments_machines_on/defender_auto_provisioning_vulnerabilty_assessments_machines_on_test.py b/tests/providers/azure/services/defender/defender_auto_provisioning_vulnerabilty_assessments_machines_on/defender_auto_provisioning_vulnerabilty_assessments_machines_on_test.py index bb78eb8fae..eeddb61012 100644 --- a/tests/providers/azure/services/defender/defender_auto_provisioning_vulnerabilty_assessments_machines_on/defender_auto_provisioning_vulnerabilty_assessments_machines_on_test.py +++ b/tests/providers/azure/services/defender/defender_auto_provisioning_vulnerabilty_assessments_machines_on/defender_auto_provisioning_vulnerabilty_assessments_machines_on_test.py @@ -3,7 +3,9 @@ from uuid import uuid4 from prowler.providers.azure.services.defender.defender_service import Assesment from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_defender_auto_provisioning_vulnerabilty_assessments_machines_on: def test_defender_no_app_services(self): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.assessments = {} with ( @@ -34,6 +37,7 @@ class Test_defender_auto_provisioning_vulnerabilty_assessments_machines_on: def test_defender_machines_no_vulnerability_assessment_solution(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.assessments = { AZURE_SUBSCRIPTION_ID: { "Machines should have a vulnerability assessment solution": Assesment( @@ -64,7 +68,7 @@ class Test_defender_auto_provisioning_vulnerabilty_assessments_machines_on: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Vulnerability assessment is not set up in all VMs in subscription {AZURE_SUBSCRIPTION_ID}." + == f"Vulnerability assessment is not set up in all VMs in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "vm1" @@ -73,6 +77,7 @@ class Test_defender_auto_provisioning_vulnerabilty_assessments_machines_on: def test_defender_machines_vulnerability_assessment_solution(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.assessments = { AZURE_SUBSCRIPTION_ID: { "Machines should have a vulnerability assessment solution": Assesment( @@ -103,7 +108,7 @@ class Test_defender_auto_provisioning_vulnerabilty_assessments_machines_on: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Vulnerability assessment is set up in all VMs in subscription {AZURE_SUBSCRIPTION_ID}." + == f"Vulnerability assessment is set up in all VMs in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "vm1" diff --git a/tests/providers/azure/services/defender/defender_container_images_resolved_vulnerabilities/defender_container_images_resolved_vulnerabilities_test.py b/tests/providers/azure/services/defender/defender_container_images_resolved_vulnerabilities/defender_container_images_resolved_vulnerabilities_test.py index bfaabbce9a..510a995692 100644 --- a/tests/providers/azure/services/defender/defender_container_images_resolved_vulnerabilities/defender_container_images_resolved_vulnerabilities_test.py +++ b/tests/providers/azure/services/defender/defender_container_images_resolved_vulnerabilities/defender_container_images_resolved_vulnerabilities_test.py @@ -3,7 +3,9 @@ from uuid import uuid4 from prowler.providers.azure.services.defender.defender_service import Assesment from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_defender_container_images_resolved_vulnerabilities: def test_defender_no_subscriptions(self): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.assessments = {} with ( @@ -33,6 +36,7 @@ class Test_defender_container_images_resolved_vulnerabilities: def test_defender_subscription_empty(self): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.assessments = {AZURE_SUBSCRIPTION_ID: {}} with ( @@ -55,6 +59,7 @@ class Test_defender_container_images_resolved_vulnerabilities: def test_defender_subscription_no_assesment(self): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.assessments = { AZURE_SUBSCRIPTION_ID: { "": Assesment( @@ -85,6 +90,7 @@ class Test_defender_container_images_resolved_vulnerabilities: def test_defender_subscription_assesment_unhealthy(self): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.assessments = { AZURE_SUBSCRIPTION_ID: { "Azure running container images should have vulnerabilities resolved (powered by Microsoft Defender Vulnerability Management)": Assesment( @@ -128,11 +134,12 @@ class Test_defender_container_images_resolved_vulnerabilities: assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert ( result[0].status_extended - == f"Azure running container images have unresolved vulnerabilities in subscription '{AZURE_SUBSCRIPTION_ID}'." + == f"Azure running container images have unresolved vulnerabilities in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'." ) def test_defender_subscription_assesment_healthy(self): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.assessments = { AZURE_SUBSCRIPTION_ID: { "Azure running container images should have vulnerabilities resolved (powered by Microsoft Defender Vulnerability Management)": Assesment( @@ -176,11 +183,12 @@ class Test_defender_container_images_resolved_vulnerabilities: assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert ( result[0].status_extended - == f"Azure running container images do not have unresolved vulnerabilities in subscription '{AZURE_SUBSCRIPTION_ID}'." + == f"Azure running container images do not have unresolved vulnerabilities in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'." ) def test_defender_subscription_assesment_not_applicable(self): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.assessments = { AZURE_SUBSCRIPTION_ID: { "Azure running container images should have vulnerabilities resolved (powered by Microsoft Defender Vulnerability Management)": Assesment( diff --git a/tests/providers/azure/services/defender/defender_container_images_scan_enabled/defender_container_images_scan_enabled_test.py b/tests/providers/azure/services/defender/defender_container_images_scan_enabled/defender_container_images_scan_enabled_test.py index 6bb9fb0e9a..977ee8acdb 100644 --- a/tests/providers/azure/services/defender/defender_container_images_scan_enabled/defender_container_images_scan_enabled_test.py +++ b/tests/providers/azure/services/defender/defender_container_images_scan_enabled/defender_container_images_scan_enabled_test.py @@ -4,7 +4,9 @@ from uuid import uuid4 from prowler.providers.azure.services.defender.defender_service import Pricing from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -12,6 +14,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_defender_container_images_scan_enabled: def test_defender_no_subscriptions(self): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = {} with ( @@ -34,6 +37,7 @@ class Test_defender_container_images_scan_enabled: def test_defender_subscription_empty(self): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = {AZURE_SUBSCRIPTION_ID: {}} with ( @@ -56,6 +60,7 @@ class Test_defender_container_images_scan_enabled: def test_defender_subscription_no_containers(self): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = { AZURE_SUBSCRIPTION_ID: { "NotContainers": Pricing( @@ -87,6 +92,7 @@ class Test_defender_container_images_scan_enabled: def test_defender_subscription_containers_no_extensions(self): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = { AZURE_SUBSCRIPTION_ID: { "Containers": Pricing( @@ -118,7 +124,7 @@ class Test_defender_container_images_scan_enabled: assert len(result) == 1 assert result[0].status == "FAIL" assert result[0].status_extended == ( - f"Container image scan is disabled in subscription {AZURE_SUBSCRIPTION_ID}." + f"Container image scan is disabled in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) assert ( result[0].resource_id @@ -131,6 +137,7 @@ class Test_defender_container_images_scan_enabled: def test_defender_subscription_containers_container_images_scan_off(self): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = { AZURE_SUBSCRIPTION_ID: { "Containers": Pricing( @@ -162,7 +169,7 @@ class Test_defender_container_images_scan_enabled: assert len(result) == 1 assert result[0].status == "FAIL" assert result[0].status_extended == ( - f"Container image scan is disabled in subscription {AZURE_SUBSCRIPTION_ID}." + f"Container image scan is disabled in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) assert ( result[0].resource_id @@ -175,6 +182,7 @@ class Test_defender_container_images_scan_enabled: def test_defender_subscription_containers_container_images_scan_on(self): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = { AZURE_SUBSCRIPTION_ID: { "Containers": Pricing( @@ -206,7 +214,7 @@ class Test_defender_container_images_scan_enabled: assert len(result) == 1 assert result[0].status == "PASS" assert result[0].status_extended == ( - f"Container image scan is enabled in subscription {AZURE_SUBSCRIPTION_ID}." + f"Container image scan is enabled in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) assert ( result[0].resource_id diff --git a/tests/providers/azure/services/defender/defender_ensure_defender_for_app_services_is_on/defender_ensure_defender_for_app_services_is_on_test.py b/tests/providers/azure/services/defender/defender_ensure_defender_for_app_services_is_on/defender_ensure_defender_for_app_services_is_on_test.py index a3b10bb0d4..b2528e28e7 100644 --- a/tests/providers/azure/services/defender/defender_ensure_defender_for_app_services_is_on/defender_ensure_defender_for_app_services_is_on_test.py +++ b/tests/providers/azure/services/defender/defender_ensure_defender_for_app_services_is_on/defender_ensure_defender_for_app_services_is_on_test.py @@ -3,7 +3,9 @@ from uuid import uuid4 from prowler.providers.azure.services.defender.defender_service import Pricing from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_defender_ensure_defender_for_app_services_is_on: def test_defender_no_app_services(self): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = {} with ( @@ -34,6 +37,7 @@ class Test_defender_ensure_defender_for_app_services_is_on: def test_defender_app_services_pricing_tier_not_standard(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = { AZURE_SUBSCRIPTION_ID: { "AppServices": Pricing( @@ -65,7 +69,7 @@ class Test_defender_ensure_defender_for_app_services_is_on: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Defender plan Defender for App Services from subscription {AZURE_SUBSCRIPTION_ID} is set to OFF (pricing tier not standard)." + == f"Defender plan Defender for App Services from subscription {AZURE_SUBSCRIPTION_DISPLAY} is set to OFF (pricing tier not standard)." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "Defender plan App Services" @@ -74,6 +78,7 @@ class Test_defender_ensure_defender_for_app_services_is_on: def test_defender_app_services_pricing_tier_standard(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = { AZURE_SUBSCRIPTION_ID: { "AppServices": Pricing( @@ -105,7 +110,7 @@ class Test_defender_ensure_defender_for_app_services_is_on: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Defender plan Defender for App Services from subscription {AZURE_SUBSCRIPTION_ID} is set to ON (pricing tier standard)." + == f"Defender plan Defender for App Services from subscription {AZURE_SUBSCRIPTION_DISPLAY} is set to ON (pricing tier standard)." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "Defender plan App Services" diff --git a/tests/providers/azure/services/defender/defender_ensure_defender_for_arm_is_on/defender_ensure_defender_for_arm_is_on_test.py b/tests/providers/azure/services/defender/defender_ensure_defender_for_arm_is_on/defender_ensure_defender_for_arm_is_on_test.py index 247cebf877..357e3ca9e7 100644 --- a/tests/providers/azure/services/defender/defender_ensure_defender_for_arm_is_on/defender_ensure_defender_for_arm_is_on_test.py +++ b/tests/providers/azure/services/defender/defender_ensure_defender_for_arm_is_on/defender_ensure_defender_for_arm_is_on_test.py @@ -3,7 +3,9 @@ from uuid import uuid4 from prowler.providers.azure.services.defender.defender_service import Pricing from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_defender_ensure_defender_for_arm_is_on: def test_defender_no_arm(self): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = {} with ( @@ -34,6 +37,7 @@ class Test_defender_ensure_defender_for_arm_is_on: def test_defender_arm_pricing_tier_not_standard(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = { AZURE_SUBSCRIPTION_ID: { "Arm": Pricing( @@ -65,7 +69,7 @@ class Test_defender_ensure_defender_for_arm_is_on: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Defender plan Defender for ARM from subscription {AZURE_SUBSCRIPTION_ID} is set to OFF (pricing tier not standard)." + == f"Defender plan Defender for ARM from subscription {AZURE_SUBSCRIPTION_DISPLAY} is set to OFF (pricing tier not standard)." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "Defender plan ARM" @@ -74,6 +78,7 @@ class Test_defender_ensure_defender_for_arm_is_on: def test_defender_arm_pricing_tier_standard(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = { AZURE_SUBSCRIPTION_ID: { "Arm": Pricing( @@ -105,7 +110,7 @@ class Test_defender_ensure_defender_for_arm_is_on: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Defender plan Defender for ARM from subscription {AZURE_SUBSCRIPTION_ID} is set to ON (pricing tier standard)." + == f"Defender plan Defender for ARM from subscription {AZURE_SUBSCRIPTION_DISPLAY} is set to ON (pricing tier standard)." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "Defender plan ARM" diff --git a/tests/providers/azure/services/defender/defender_ensure_defender_for_azure_sql_databases_is_on/defender_ensure_defender_for_azure_sql_databases_is_on_test.py b/tests/providers/azure/services/defender/defender_ensure_defender_for_azure_sql_databases_is_on/defender_ensure_defender_for_azure_sql_databases_is_on_test.py index 5a0ab49b7a..c10314042b 100644 --- a/tests/providers/azure/services/defender/defender_ensure_defender_for_azure_sql_databases_is_on/defender_ensure_defender_for_azure_sql_databases_is_on_test.py +++ b/tests/providers/azure/services/defender/defender_ensure_defender_for_azure_sql_databases_is_on/defender_ensure_defender_for_azure_sql_databases_is_on_test.py @@ -3,7 +3,9 @@ from uuid import uuid4 from prowler.providers.azure.services.defender.defender_service import Pricing from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_defender_ensure_defender_for_azure_sql_databases_is_on: def test_defender_no_sql_databases(self): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = {} with ( @@ -34,6 +37,7 @@ class Test_defender_ensure_defender_for_azure_sql_databases_is_on: def test_defender_sql_databases_pricing_tier_not_standard(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = { AZURE_SUBSCRIPTION_ID: { "SqlServers": Pricing( @@ -65,7 +69,7 @@ class Test_defender_ensure_defender_for_azure_sql_databases_is_on: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Defender plan Defender for Azure SQL DB Servers from subscription {AZURE_SUBSCRIPTION_ID} is set to OFF (pricing tier not standard)." + == f"Defender plan Defender for Azure SQL DB Servers from subscription {AZURE_SUBSCRIPTION_DISPLAY} is set to OFF (pricing tier not standard)." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "Defender plan Servers" @@ -74,6 +78,7 @@ class Test_defender_ensure_defender_for_azure_sql_databases_is_on: def test_defender_sql_databases_pricing_tier_standard(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = { AZURE_SUBSCRIPTION_ID: { "SqlServers": Pricing( @@ -105,7 +110,7 @@ class Test_defender_ensure_defender_for_azure_sql_databases_is_on: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Defender plan Defender for Azure SQL DB Servers from subscription {AZURE_SUBSCRIPTION_ID} is set to ON (pricing tier standard)." + == f"Defender plan Defender for Azure SQL DB Servers from subscription {AZURE_SUBSCRIPTION_DISPLAY} is set to ON (pricing tier standard)." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "Defender plan Servers" diff --git a/tests/providers/azure/services/defender/defender_ensure_defender_for_containers_is_on/defender_ensure_defender_for_containers_is_on_test.py b/tests/providers/azure/services/defender/defender_ensure_defender_for_containers_is_on/defender_ensure_defender_for_containers_is_on_test.py index 622ad77b42..7ff728add9 100644 --- a/tests/providers/azure/services/defender/defender_ensure_defender_for_containers_is_on/defender_ensure_defender_for_containers_is_on_test.py +++ b/tests/providers/azure/services/defender/defender_ensure_defender_for_containers_is_on/defender_ensure_defender_for_containers_is_on_test.py @@ -3,7 +3,9 @@ from uuid import uuid4 from prowler.providers.azure.services.defender.defender_service import Pricing from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_defender_ensure_defender_for_containers_is_on: def test_defender_no_container_registries(self): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = {} with ( @@ -34,6 +37,7 @@ class Test_defender_ensure_defender_for_containers_is_on: def test_defender_container_registries_pricing_tier_not_standard(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = { AZURE_SUBSCRIPTION_ID: { "Containers": Pricing( @@ -65,7 +69,7 @@ class Test_defender_ensure_defender_for_containers_is_on: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Defender plan Defender for Containers from subscription {AZURE_SUBSCRIPTION_ID} is set to OFF (pricing tier not standard)." + == f"Defender plan Defender for Containers from subscription {AZURE_SUBSCRIPTION_DISPLAY} is set to OFF (pricing tier not standard)." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "Defender plan Servers" @@ -74,6 +78,7 @@ class Test_defender_ensure_defender_for_containers_is_on: def test_defender_container_registries_pricing_tier_standard(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = { AZURE_SUBSCRIPTION_ID: { "Containers": Pricing( @@ -105,7 +110,7 @@ class Test_defender_ensure_defender_for_containers_is_on: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Defender plan Defender for Containers from subscription {AZURE_SUBSCRIPTION_ID} is set to ON (pricing tier standard)." + == f"Defender plan Defender for Containers from subscription {AZURE_SUBSCRIPTION_DISPLAY} is set to ON (pricing tier standard)." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "Defender plan Servers" diff --git a/tests/providers/azure/services/defender/defender_ensure_defender_for_cosmosdb_is_on/defender_ensure_defender_for_cosmosdb_is_on_test.py b/tests/providers/azure/services/defender/defender_ensure_defender_for_cosmosdb_is_on/defender_ensure_defender_for_cosmosdb_is_on_test.py index 21507b9c20..351f38d97f 100644 --- a/tests/providers/azure/services/defender/defender_ensure_defender_for_cosmosdb_is_on/defender_ensure_defender_for_cosmosdb_is_on_test.py +++ b/tests/providers/azure/services/defender/defender_ensure_defender_for_cosmosdb_is_on/defender_ensure_defender_for_cosmosdb_is_on_test.py @@ -3,7 +3,9 @@ from uuid import uuid4 from prowler.providers.azure.services.defender.defender_service import Pricing from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_defender_ensure_defender_for_cosmosdb_is_on: def test_defender_no_cosmosdb(self): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = {} with ( @@ -34,6 +37,7 @@ class Test_defender_ensure_defender_for_cosmosdb_is_on: def test_defender_cosmosdb_pricing_tier_not_standard(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = { AZURE_SUBSCRIPTION_ID: { "CosmosDbs": Pricing( @@ -65,7 +69,7 @@ class Test_defender_ensure_defender_for_cosmosdb_is_on: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Defender plan Defender for Cosmos DB from subscription {AZURE_SUBSCRIPTION_ID} is set to OFF (pricing tier not standard)." + == f"Defender plan Defender for Cosmos DB from subscription {AZURE_SUBSCRIPTION_DISPLAY} is set to OFF (pricing tier not standard)." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "Defender plan Cosmos DB" @@ -74,6 +78,7 @@ class Test_defender_ensure_defender_for_cosmosdb_is_on: def test_defender_cosmosdb_pricing_tier_standard(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = { AZURE_SUBSCRIPTION_ID: { "CosmosDbs": Pricing( @@ -105,7 +110,7 @@ class Test_defender_ensure_defender_for_cosmosdb_is_on: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Defender plan Defender for Cosmos DB from subscription {AZURE_SUBSCRIPTION_ID} is set to ON (pricing tier standard)." + == f"Defender plan Defender for Cosmos DB from subscription {AZURE_SUBSCRIPTION_DISPLAY} is set to ON (pricing tier standard)." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "Defender plan Cosmos DB" diff --git a/tests/providers/azure/services/defender/defender_ensure_defender_for_databases_is_on/defender_ensure_defender_for_databases_is_on_test.py b/tests/providers/azure/services/defender/defender_ensure_defender_for_databases_is_on/defender_ensure_defender_for_databases_is_on_test.py index 354025d4b6..48cbc57ad1 100644 --- a/tests/providers/azure/services/defender/defender_ensure_defender_for_databases_is_on/defender_ensure_defender_for_databases_is_on_test.py +++ b/tests/providers/azure/services/defender/defender_ensure_defender_for_databases_is_on/defender_ensure_defender_for_databases_is_on_test.py @@ -3,7 +3,9 @@ from uuid import uuid4 from prowler.providers.azure.services.defender.defender_service import Pricing from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_defender_ensure_defender_for_databases_is_on: def test_defender_no_databases(self): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = {} with ( @@ -34,6 +37,7 @@ class Test_defender_ensure_defender_for_databases_is_on: def test_defender_databases_sql_servers(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = { AZURE_SUBSCRIPTION_ID: { "SqlServers": Pricing( @@ -66,6 +70,7 @@ class Test_defender_ensure_defender_for_databases_is_on: def test_defender_databases_sql_server_virtual_machines(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = { AZURE_SUBSCRIPTION_ID: { "SqlServerVirtualMachines": Pricing( @@ -98,6 +103,7 @@ class Test_defender_ensure_defender_for_databases_is_on: def test_defender_databases_open_source_relation_databases(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = { AZURE_SUBSCRIPTION_ID: { "OpenSourceRelationalDatabases": Pricing( @@ -130,6 +136,7 @@ class Test_defender_ensure_defender_for_databases_is_on: def test_defender_databases_cosmosdbs(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = { AZURE_SUBSCRIPTION_ID: { "CosmosDbs": Pricing( @@ -162,6 +169,7 @@ class Test_defender_ensure_defender_for_databases_is_on: def test_defender_databases_all_standard(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = { AZURE_SUBSCRIPTION_ID: { "SqlServers": Pricing( @@ -211,7 +219,7 @@ class Test_defender_ensure_defender_for_databases_is_on: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Defender plan Defender for Databases from subscription {AZURE_SUBSCRIPTION_ID} is set to ON (pricing tier standard)." + == f"Defender plan Defender for Databases from subscription {AZURE_SUBSCRIPTION_DISPLAY} is set to ON (pricing tier standard)." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "Defender plan Servers" @@ -220,6 +228,7 @@ class Test_defender_ensure_defender_for_databases_is_on: def test_defender_databases_cosmosdb_not_standard(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = { AZURE_SUBSCRIPTION_ID: { "SqlServers": Pricing( @@ -269,7 +278,7 @@ class Test_defender_ensure_defender_for_databases_is_on: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Defender plan Defender for Databases from subscription {AZURE_SUBSCRIPTION_ID} is set to OFF (pricing tier not standard)." + == f"Defender plan Defender for Databases from subscription {AZURE_SUBSCRIPTION_DISPLAY} is set to OFF (pricing tier not standard)." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "Defender plan Servers" diff --git a/tests/providers/azure/services/defender/defender_ensure_defender_for_dns_is_on/defender_ensure_defender_for_dns_is_on_test.py b/tests/providers/azure/services/defender/defender_ensure_defender_for_dns_is_on/defender_ensure_defender_for_dns_is_on_test.py index 22729f4677..6b50ea4c5f 100644 --- a/tests/providers/azure/services/defender/defender_ensure_defender_for_dns_is_on/defender_ensure_defender_for_dns_is_on_test.py +++ b/tests/providers/azure/services/defender/defender_ensure_defender_for_dns_is_on/defender_ensure_defender_for_dns_is_on_test.py @@ -3,7 +3,9 @@ from uuid import uuid4 from prowler.providers.azure.services.defender.defender_service import Pricing from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_defender_ensure_defender_for_dns_is_on: def test_defender_no_dns(self): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = {} with ( @@ -34,6 +37,7 @@ class Test_defender_ensure_defender_for_dns_is_on: def test_defender_dns_pricing_tier_not_standard(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = { AZURE_SUBSCRIPTION_ID: { "Dns": Pricing( @@ -65,7 +69,7 @@ class Test_defender_ensure_defender_for_dns_is_on: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Defender plan Defender for DNS from subscription {AZURE_SUBSCRIPTION_ID} is set to OFF (pricing tier not standard)." + == f"Defender plan Defender for DNS from subscription {AZURE_SUBSCRIPTION_DISPLAY} is set to OFF (pricing tier not standard)." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "Defender plan DNS" @@ -74,6 +78,7 @@ class Test_defender_ensure_defender_for_dns_is_on: def test_defender_dns_pricing_tier_standard(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = { AZURE_SUBSCRIPTION_ID: { "Dns": Pricing( @@ -105,7 +110,7 @@ class Test_defender_ensure_defender_for_dns_is_on: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Defender plan Defender for DNS from subscription {AZURE_SUBSCRIPTION_ID} is set to ON (pricing tier standard)." + == f"Defender plan Defender for DNS from subscription {AZURE_SUBSCRIPTION_DISPLAY} is set to ON (pricing tier standard)." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "Defender plan DNS" diff --git a/tests/providers/azure/services/defender/defender_ensure_defender_for_keyvault_is_on/defender_ensure_defender_for_keyvault_is_on_test.py b/tests/providers/azure/services/defender/defender_ensure_defender_for_keyvault_is_on/defender_ensure_defender_for_keyvault_is_on_test.py index f2ef503114..f587a92961 100644 --- a/tests/providers/azure/services/defender/defender_ensure_defender_for_keyvault_is_on/defender_ensure_defender_for_keyvault_is_on_test.py +++ b/tests/providers/azure/services/defender/defender_ensure_defender_for_keyvault_is_on/defender_ensure_defender_for_keyvault_is_on_test.py @@ -3,7 +3,9 @@ from uuid import uuid4 from prowler.providers.azure.services.defender.defender_service import Pricing from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_defender_ensure_defender_for_keyvault_is_on: def test_defender_no_keyvaults(self): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = {} with ( @@ -34,6 +37,7 @@ class Test_defender_ensure_defender_for_keyvault_is_on: def test_defender_keyvaults_pricing_tier_not_standard(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = { AZURE_SUBSCRIPTION_ID: { "KeyVaults": Pricing( @@ -65,7 +69,7 @@ class Test_defender_ensure_defender_for_keyvault_is_on: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Defender plan Defender for KeyVaults from subscription {AZURE_SUBSCRIPTION_ID} is set to OFF (pricing tier not standard)." + == f"Defender plan Defender for KeyVaults from subscription {AZURE_SUBSCRIPTION_DISPLAY} is set to OFF (pricing tier not standard)." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "Defender plan KeyVaults" @@ -74,6 +78,7 @@ class Test_defender_ensure_defender_for_keyvault_is_on: def test_defender_keyvaults_pricing_tier_standard(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = { AZURE_SUBSCRIPTION_ID: { "KeyVaults": Pricing( @@ -105,7 +110,7 @@ class Test_defender_ensure_defender_for_keyvault_is_on: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Defender plan Defender for KeyVaults from subscription {AZURE_SUBSCRIPTION_ID} is set to ON (pricing tier standard)." + == f"Defender plan Defender for KeyVaults from subscription {AZURE_SUBSCRIPTION_DISPLAY} is set to ON (pricing tier standard)." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "Defender plan KeyVaults" diff --git a/tests/providers/azure/services/defender/defender_ensure_defender_for_os_relational_databases_is_on/defender_ensure_defender_for_os_relational_databases_is_on_test.py b/tests/providers/azure/services/defender/defender_ensure_defender_for_os_relational_databases_is_on/defender_ensure_defender_for_os_relational_databases_is_on_test.py index a00ad47526..dc28fb3bb2 100644 --- a/tests/providers/azure/services/defender/defender_ensure_defender_for_os_relational_databases_is_on/defender_ensure_defender_for_os_relational_databases_is_on_test.py +++ b/tests/providers/azure/services/defender/defender_ensure_defender_for_os_relational_databases_is_on/defender_ensure_defender_for_os_relational_databases_is_on_test.py @@ -3,7 +3,9 @@ from uuid import uuid4 from prowler.providers.azure.services.defender.defender_service import Pricing from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_defender_ensure_defender_for_os_relational_databases_is_on: def test_defender_no_os_relational_databases(self): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = {} with ( @@ -34,6 +37,7 @@ class Test_defender_ensure_defender_for_os_relational_databases_is_on: def test_defender_os_relational_databases_pricing_tier_not_standard(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = { AZURE_SUBSCRIPTION_ID: { "OpenSourceRelationalDatabases": Pricing( @@ -65,7 +69,7 @@ class Test_defender_ensure_defender_for_os_relational_databases_is_on: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Defender plan Defender for Open-Source Relational Databases from subscription {AZURE_SUBSCRIPTION_ID} is set to OFF (pricing tier not standard)." + == f"Defender plan Defender for Open-Source Relational Databases from subscription {AZURE_SUBSCRIPTION_DISPLAY} is set to OFF (pricing tier not standard)." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert ( @@ -77,6 +81,7 @@ class Test_defender_ensure_defender_for_os_relational_databases_is_on: def test_defender_os_relational_databases_pricing_tier_standard(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = { AZURE_SUBSCRIPTION_ID: { "OpenSourceRelationalDatabases": Pricing( @@ -108,7 +113,7 @@ class Test_defender_ensure_defender_for_os_relational_databases_is_on: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Defender plan Defender for Open-Source Relational Databases from subscription {AZURE_SUBSCRIPTION_ID} is set to ON (pricing tier standard)." + == f"Defender plan Defender for Open-Source Relational Databases from subscription {AZURE_SUBSCRIPTION_DISPLAY} is set to ON (pricing tier standard)." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert ( diff --git a/tests/providers/azure/services/defender/defender_ensure_defender_for_server_is_on/defender_ensure_defender_for_server_is_on_test.py b/tests/providers/azure/services/defender/defender_ensure_defender_for_server_is_on/defender_ensure_defender_for_server_is_on_test.py index 460a206150..226b26ad3a 100644 --- a/tests/providers/azure/services/defender/defender_ensure_defender_for_server_is_on/defender_ensure_defender_for_server_is_on_test.py +++ b/tests/providers/azure/services/defender/defender_ensure_defender_for_server_is_on/defender_ensure_defender_for_server_is_on_test.py @@ -3,7 +3,9 @@ from uuid import uuid4 from prowler.providers.azure.services.defender.defender_service import Pricing from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_defender_ensure_defender_for_server_is_on: def test_defender_no_server(self): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = {} with ( @@ -34,6 +37,7 @@ class Test_defender_ensure_defender_for_server_is_on: def test_defender_server_pricing_tier_not_standard(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = { AZURE_SUBSCRIPTION_ID: { "VirtualMachines": Pricing( @@ -65,7 +69,7 @@ class Test_defender_ensure_defender_for_server_is_on: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Defender plan Defender for Servers from subscription {AZURE_SUBSCRIPTION_ID} is set to OFF (pricing tier not standard)." + == f"Defender plan Defender for Servers from subscription {AZURE_SUBSCRIPTION_DISPLAY} is set to OFF (pricing tier not standard)." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "Defender plan Servers" @@ -74,6 +78,7 @@ class Test_defender_ensure_defender_for_server_is_on: def test_defender_server_pricing_tier_standard(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = { AZURE_SUBSCRIPTION_ID: { "VirtualMachines": Pricing( @@ -105,7 +110,7 @@ class Test_defender_ensure_defender_for_server_is_on: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Defender plan Defender for Servers from subscription {AZURE_SUBSCRIPTION_ID} is set to ON (pricing tier standard)." + == f"Defender plan Defender for Servers from subscription {AZURE_SUBSCRIPTION_DISPLAY} is set to ON (pricing tier standard)." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "Defender plan Servers" diff --git a/tests/providers/azure/services/defender/defender_ensure_defender_for_sql_servers_is_on/defender_ensure_defender_for_sql_servers_is_on_test.py b/tests/providers/azure/services/defender/defender_ensure_defender_for_sql_servers_is_on/defender_ensure_defender_for_sql_servers_is_on_test.py index c99a270e89..1907cdbb6c 100644 --- a/tests/providers/azure/services/defender/defender_ensure_defender_for_sql_servers_is_on/defender_ensure_defender_for_sql_servers_is_on_test.py +++ b/tests/providers/azure/services/defender/defender_ensure_defender_for_sql_servers_is_on/defender_ensure_defender_for_sql_servers_is_on_test.py @@ -3,7 +3,9 @@ from uuid import uuid4 from prowler.providers.azure.services.defender.defender_service import Pricing from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_defender_ensure_defender_for_sql_servers_is_on: def test_defender_no_server(self): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = {} with ( @@ -34,6 +37,7 @@ class Test_defender_ensure_defender_for_sql_servers_is_on: def test_defender_server_pricing_tier_not_standard(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = { AZURE_SUBSCRIPTION_ID: { "SqlServerVirtualMachines": Pricing( @@ -65,7 +69,7 @@ class Test_defender_ensure_defender_for_sql_servers_is_on: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Defender plan Defender for SQL Server VMs from subscription {AZURE_SUBSCRIPTION_ID} is set to OFF (pricing tier not standard)." + == f"Defender plan Defender for SQL Server VMs from subscription {AZURE_SUBSCRIPTION_DISPLAY} is set to OFF (pricing tier not standard)." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "Defender plan SQL Server VMs" @@ -74,6 +78,7 @@ class Test_defender_ensure_defender_for_sql_servers_is_on: def test_defender_server_pricing_tier_standard(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = { AZURE_SUBSCRIPTION_ID: { "SqlServerVirtualMachines": Pricing( @@ -105,7 +110,7 @@ class Test_defender_ensure_defender_for_sql_servers_is_on: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Defender plan Defender for SQL Server VMs from subscription {AZURE_SUBSCRIPTION_ID} is set to ON (pricing tier standard)." + == f"Defender plan Defender for SQL Server VMs from subscription {AZURE_SUBSCRIPTION_DISPLAY} is set to ON (pricing tier standard)." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "Defender plan SQL Server VMs" diff --git a/tests/providers/azure/services/defender/defender_ensure_defender_for_storage_is_on/defender_ensure_defender_for_storage_is_on_test.py b/tests/providers/azure/services/defender/defender_ensure_defender_for_storage_is_on/defender_ensure_defender_for_storage_is_on_test.py index d3d22ee1d7..f5eee6879a 100644 --- a/tests/providers/azure/services/defender/defender_ensure_defender_for_storage_is_on/defender_ensure_defender_for_storage_is_on_test.py +++ b/tests/providers/azure/services/defender/defender_ensure_defender_for_storage_is_on/defender_ensure_defender_for_storage_is_on_test.py @@ -3,7 +3,9 @@ from uuid import uuid4 from prowler.providers.azure.services.defender.defender_service import Pricing from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_defender_ensure_defender_for_storage_is_on: def test_defender_no_server(self): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = {} with ( @@ -34,6 +37,7 @@ class Test_defender_ensure_defender_for_storage_is_on: def test_defender_server_pricing_tier_not_standard(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = { AZURE_SUBSCRIPTION_ID: { "StorageAccounts": Pricing( @@ -65,7 +69,7 @@ class Test_defender_ensure_defender_for_storage_is_on: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Defender plan Defender for Storage Accounts from subscription {AZURE_SUBSCRIPTION_ID} is set to OFF (pricing tier not standard)." + == f"Defender plan Defender for Storage Accounts from subscription {AZURE_SUBSCRIPTION_DISPLAY} is set to OFF (pricing tier not standard)." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "Defender plan Storage Accounts" @@ -74,6 +78,7 @@ class Test_defender_ensure_defender_for_storage_is_on: def test_defender_server_pricing_tier_standard(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.pricings = { AZURE_SUBSCRIPTION_ID: { "StorageAccounts": Pricing( @@ -105,7 +110,7 @@ class Test_defender_ensure_defender_for_storage_is_on: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Defender plan Defender for Storage Accounts from subscription {AZURE_SUBSCRIPTION_ID} is set to ON (pricing tier standard)." + == f"Defender plan Defender for Storage Accounts from subscription {AZURE_SUBSCRIPTION_DISPLAY} is set to ON (pricing tier standard)." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "Defender plan Storage Accounts" diff --git a/tests/providers/azure/services/defender/defender_ensure_iot_hub_defender_is_on/defender_ensure_iot_hub_defender_is_on_test.py b/tests/providers/azure/services/defender/defender_ensure_iot_hub_defender_is_on/defender_ensure_iot_hub_defender_is_on_test.py index 14ae870fdf..f4ac17c5ae 100644 --- a/tests/providers/azure/services/defender/defender_ensure_iot_hub_defender_is_on/defender_ensure_iot_hub_defender_is_on_test.py +++ b/tests/providers/azure/services/defender/defender_ensure_iot_hub_defender_is_on/defender_ensure_iot_hub_defender_is_on_test.py @@ -5,7 +5,9 @@ from prowler.providers.azure.services.defender.defender_service import ( IoTSecuritySolution, ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -13,6 +15,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_defender_ensure_iot_hub_defender_is_on: def test_defender_no_subscriptions(self): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.iot_security_solutions = {} with ( @@ -36,7 +39,7 @@ class Test_defender_ensure_iot_hub_defender_is_on: def test_defender_no_iot_hub_solutions(self): defender_client = mock.MagicMock defender_client.iot_security_solutions = {AZURE_SUBSCRIPTION_ID: {}} - defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_ID} + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -58,7 +61,7 @@ class Test_defender_ensure_iot_hub_defender_is_on: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"No IoT Security Solutions found in the subscription {AZURE_SUBSCRIPTION_ID}." + == f"No IoT Security Solutions found in the subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) assert result[0].resource_name == AZURE_SUBSCRIPTION_ID assert result[0].resource_id == f"/subscriptions/{AZURE_SUBSCRIPTION_ID}" @@ -66,6 +69,7 @@ class Test_defender_ensure_iot_hub_defender_is_on: def test_defender_iot_hub_solution_disabled(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.iot_security_solutions = { AZURE_SUBSCRIPTION_ID: { resource_id: IoTSecuritySolution( @@ -94,7 +98,7 @@ class Test_defender_ensure_iot_hub_defender_is_on: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"The security solution iot_sec_solution is disabled in subscription {AZURE_SUBSCRIPTION_ID}" + == f"The security solution iot_sec_solution is disabled in subscription {AZURE_SUBSCRIPTION_DISPLAY}" ) assert result[0].resource_name == "iot_sec_solution" assert result[0].resource_id == resource_id @@ -102,6 +106,7 @@ class Test_defender_ensure_iot_hub_defender_is_on: def test_defender_iot_hub_solution_enabled(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.iot_security_solutions = { AZURE_SUBSCRIPTION_ID: { resource_id: IoTSecuritySolution( @@ -130,7 +135,7 @@ class Test_defender_ensure_iot_hub_defender_is_on: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"The security solution iot_sec_solution is enabled in subscription {AZURE_SUBSCRIPTION_ID}." + == f"The security solution iot_sec_solution is enabled in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) assert result[0].resource_name == "iot_sec_solution" assert result[0].resource_id == resource_id @@ -140,6 +145,7 @@ class Test_defender_ensure_iot_hub_defender_is_on: resource_id_enabled = str(uuid4()) resource_id_disabled = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.iot_security_solutions = { AZURE_SUBSCRIPTION_ID: { resource_id_enabled: IoTSecuritySolution( @@ -175,7 +181,7 @@ class Test_defender_ensure_iot_hub_defender_is_on: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"The security solution iot_sec_solution_enabled is enabled in subscription {AZURE_SUBSCRIPTION_ID}." + == f"The security solution iot_sec_solution_enabled is enabled in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) assert result[0].resource_name == "iot_sec_solution_enabled" assert result[0].resource_id == resource_id_enabled @@ -184,7 +190,7 @@ class Test_defender_ensure_iot_hub_defender_is_on: assert result[1].status == "FAIL" assert ( result[1].status_extended - == f"The security solution iot_sec_solution_disabled is disabled in subscription {AZURE_SUBSCRIPTION_ID}" + == f"The security solution iot_sec_solution_disabled is disabled in subscription {AZURE_SUBSCRIPTION_DISPLAY}" ) assert result[1].resource_name == "iot_sec_solution_disabled" assert result[1].resource_id == resource_id_disabled diff --git a/tests/providers/azure/services/defender/defender_ensure_mcas_is_enabled/defender_ensure_mcas_is_enabled_test.py b/tests/providers/azure/services/defender/defender_ensure_mcas_is_enabled/defender_ensure_mcas_is_enabled_test.py index 5db8eb19a0..7770ab0baf 100644 --- a/tests/providers/azure/services/defender/defender_ensure_mcas_is_enabled/defender_ensure_mcas_is_enabled_test.py +++ b/tests/providers/azure/services/defender/defender_ensure_mcas_is_enabled/defender_ensure_mcas_is_enabled_test.py @@ -3,7 +3,9 @@ from uuid import uuid4 from prowler.providers.azure.services.defender.defender_service import Setting from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_defender_ensure_mcas_is_enabled: def test_defender_no_settings(self): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.settings = {} with ( @@ -34,6 +37,7 @@ class Test_defender_ensure_mcas_is_enabled: def test_defender_mcas_disabled(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.settings = { AZURE_SUBSCRIPTION_ID: { "MCAS": Setting( @@ -66,7 +70,7 @@ class Test_defender_ensure_mcas_is_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Microsoft Defender for Cloud Apps is disabled for subscription {AZURE_SUBSCRIPTION_ID}." + == f"Microsoft Defender for Cloud Apps is disabled for subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "MCAS" @@ -75,6 +79,7 @@ class Test_defender_ensure_mcas_is_enabled: def test_defender_mcas_enabled(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.settings = { AZURE_SUBSCRIPTION_ID: { "MCAS": Setting( @@ -107,7 +112,7 @@ class Test_defender_ensure_mcas_is_enabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Microsoft Defender for Cloud Apps is enabled for subscription {AZURE_SUBSCRIPTION_ID}." + == f"Microsoft Defender for Cloud Apps is enabled for subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "MCAS" @@ -116,7 +121,7 @@ class Test_defender_ensure_mcas_is_enabled: def test_defender_mcas_no_settings(self): defender_client = mock.MagicMock defender_client.settings = {AZURE_SUBSCRIPTION_ID: {}} - defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_ID} + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -138,7 +143,7 @@ class Test_defender_ensure_mcas_is_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Microsoft Defender for Cloud Apps not exists for subscription {AZURE_SUBSCRIPTION_ID}." + == f"Microsoft Defender for Cloud Apps not exists for subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == AZURE_SUBSCRIPTION_ID diff --git a/tests/providers/azure/services/defender/defender_ensure_notify_alerts_severity_is_high/defender_ensure_notify_alerts_severity_is_high_test.py b/tests/providers/azure/services/defender/defender_ensure_notify_alerts_severity_is_high/defender_ensure_notify_alerts_severity_is_high_test.py index 85355bc1f0..8d2a3a05f7 100644 --- a/tests/providers/azure/services/defender/defender_ensure_notify_alerts_severity_is_high/defender_ensure_notify_alerts_severity_is_high_test.py +++ b/tests/providers/azure/services/defender/defender_ensure_notify_alerts_severity_is_high/defender_ensure_notify_alerts_severity_is_high_test.py @@ -6,7 +6,9 @@ from prowler.providers.azure.services.defender.defender_service import ( SecurityContactConfiguration, ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -14,6 +16,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_defender_ensure_notify_alerts_severity_is_high: def test_defender_no_subscriptions(self): defender_client = mock.MagicMock() + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.security_contact_configurations = {} with ( @@ -37,6 +40,7 @@ class Test_defender_ensure_notify_alerts_severity_is_high: def test_defender_severity_alerts_critical(self): resource_id = str(uuid4()) defender_client = mock.MagicMock() + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.security_contact_configurations = { AZURE_SUBSCRIPTION_ID: { resource_id: SecurityContactConfiguration( @@ -74,7 +78,7 @@ class Test_defender_ensure_notify_alerts_severity_is_high: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Notifications are not enabled for alerts with a minimum severity of high or lower in subscription {AZURE_SUBSCRIPTION_ID}." + == f"Notifications are not enabled for alerts with a minimum severity of high or lower in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "default" @@ -83,6 +87,7 @@ class Test_defender_ensure_notify_alerts_severity_is_high: def test_defender_severity_alerts_high(self): resource_id = str(uuid4()) defender_client = mock.MagicMock() + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.security_contact_configurations = { AZURE_SUBSCRIPTION_ID: { resource_id: SecurityContactConfiguration( @@ -121,7 +126,7 @@ class Test_defender_ensure_notify_alerts_severity_is_high: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Notifications are enabled for alerts with a minimum severity of high or lower (High) in subscription {AZURE_SUBSCRIPTION_ID}." + == f"Notifications are enabled for alerts with a minimum severity of high or lower (High) in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "default" @@ -130,6 +135,7 @@ class Test_defender_ensure_notify_alerts_severity_is_high: def test_defender_severity_alerts_low(self): resource_id = str(uuid4()) defender_client = mock.MagicMock() + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.security_contact_configurations = { AZURE_SUBSCRIPTION_ID: { resource_id: SecurityContactConfiguration( @@ -168,7 +174,7 @@ class Test_defender_ensure_notify_alerts_severity_is_high: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Notifications are enabled for alerts with a minimum severity of high or lower (Low) in subscription {AZURE_SUBSCRIPTION_ID}." + == f"Notifications are enabled for alerts with a minimum severity of high or lower (Low) in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "default" @@ -176,6 +182,7 @@ class Test_defender_ensure_notify_alerts_severity_is_high: def test_defender_default_security_contact_not_found(self): defender_client = mock.MagicMock() + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.security_contact_configurations = { AZURE_SUBSCRIPTION_ID: { f"/subscriptions/{AZURE_SUBSCRIPTION_ID}/providers/Microsoft.Security/securityContacts/default": SecurityContactConfiguration( @@ -212,7 +219,7 @@ class Test_defender_ensure_notify_alerts_severity_is_high: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Notifications are not enabled for alerts with a minimum severity of high or lower in subscription {AZURE_SUBSCRIPTION_ID}." + == f"Notifications are not enabled for alerts with a minimum severity of high or lower in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "default" diff --git a/tests/providers/azure/services/defender/defender_ensure_notify_emails_to_owners/defender_ensure_notify_emails_to_owners_test.py b/tests/providers/azure/services/defender/defender_ensure_notify_emails_to_owners/defender_ensure_notify_emails_to_owners_test.py index e4c2dc4371..b125320764 100644 --- a/tests/providers/azure/services/defender/defender_ensure_notify_emails_to_owners/defender_ensure_notify_emails_to_owners_test.py +++ b/tests/providers/azure/services/defender/defender_ensure_notify_emails_to_owners/defender_ensure_notify_emails_to_owners_test.py @@ -6,7 +6,9 @@ from prowler.providers.azure.services.defender.defender_service import ( SecurityContactConfiguration, ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -14,6 +16,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_defender_ensure_notify_emails_to_owners: def test_defender_no_subscriptions(self): defender_client = mock.MagicMock() + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.security_contact_configurations = {} with ( @@ -37,6 +40,7 @@ class Test_defender_ensure_notify_emails_to_owners: def test_defender_no_notify_emails_to_owners(self): resource_id = str(uuid4()) defender_client = mock.MagicMock() + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.security_contact_configurations = { AZURE_SUBSCRIPTION_ID: { resource_id: SecurityContactConfiguration( @@ -76,6 +80,7 @@ class Test_defender_ensure_notify_emails_to_owners: def test_defender_notify_emails_to_owners_off(self): resource_id = str(uuid4()) defender_client = mock.MagicMock() + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.security_contact_configurations = { AZURE_SUBSCRIPTION_ID: { resource_id: SecurityContactConfiguration( @@ -113,7 +118,7 @@ class Test_defender_ensure_notify_emails_to_owners: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"The Owner role is not notified for subscription {AZURE_SUBSCRIPTION_ID}." + == f"The Owner role is not notified for subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "default" @@ -122,6 +127,7 @@ class Test_defender_ensure_notify_emails_to_owners: def test_defender_notify_emails_to_owners(self): resource_id = str(uuid4()) defender_client = mock.MagicMock() + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.security_contact_configurations = { AZURE_SUBSCRIPTION_ID: { resource_id: SecurityContactConfiguration( @@ -159,7 +165,7 @@ class Test_defender_ensure_notify_emails_to_owners: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"The Owner role is notified for subscription {AZURE_SUBSCRIPTION_ID}." + == f"The Owner role is notified for subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "default" diff --git a/tests/providers/azure/services/defender/defender_ensure_system_updates_are_applied/defender_ensure_system_updates_are_applied_test.py b/tests/providers/azure/services/defender/defender_ensure_system_updates_are_applied/defender_ensure_system_updates_are_applied_test.py index 4a98f0bba6..e6a80853dd 100644 --- a/tests/providers/azure/services/defender/defender_ensure_system_updates_are_applied/defender_ensure_system_updates_are_applied_test.py +++ b/tests/providers/azure/services/defender/defender_ensure_system_updates_are_applied/defender_ensure_system_updates_are_applied_test.py @@ -3,7 +3,9 @@ from uuid import uuid4 from prowler.providers.azure.services.defender.defender_service import Assesment from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_defender_ensure_system_updates_are_applied: def test_defender_no_app_services(self): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.assessments = {} with ( @@ -34,6 +37,7 @@ class Test_defender_ensure_system_updates_are_applied: def test_defender_machines_no_log_analytics_installed(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.assessments = { AZURE_SUBSCRIPTION_ID: { "Log Analytics agent should be installed on virtual machines": Assesment( @@ -74,7 +78,7 @@ class Test_defender_ensure_system_updates_are_applied: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"System updates are not applied for all the VMs in the subscription {AZURE_SUBSCRIPTION_ID}." + == f"System updates are not applied for all the VMs in the subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "vm1" @@ -85,6 +89,7 @@ class Test_defender_ensure_system_updates_are_applied: ): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.assessments = { AZURE_SUBSCRIPTION_ID: { "Log Analytics agent should be installed on virtual machines": Assesment( @@ -125,7 +130,7 @@ class Test_defender_ensure_system_updates_are_applied: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"System updates are not applied for all the VMs in the subscription {AZURE_SUBSCRIPTION_ID}." + == f"System updates are not applied for all the VMs in the subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "vm1" @@ -134,6 +139,7 @@ class Test_defender_ensure_system_updates_are_applied: def test_defender_machines_no_system_updates_installed(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.assessments = { AZURE_SUBSCRIPTION_ID: { "Log Analytics agent should be installed on virtual machines": Assesment( @@ -174,7 +180,7 @@ class Test_defender_ensure_system_updates_are_applied: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"System updates are not applied for all the VMs in the subscription {AZURE_SUBSCRIPTION_ID}." + == f"System updates are not applied for all the VMs in the subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "vm1" @@ -185,6 +191,7 @@ class Test_defender_ensure_system_updates_are_applied: ): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.assessments = { AZURE_SUBSCRIPTION_ID: { "Log Analytics agent should be installed on virtual machines": Assesment( @@ -225,7 +232,7 @@ class Test_defender_ensure_system_updates_are_applied: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"System updates are applied for all the VMs in the subscription {AZURE_SUBSCRIPTION_ID}." + == f"System updates are applied for all the VMs in the subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "vm1" diff --git a/tests/providers/azure/services/defender/defender_ensure_wdatp_is_enabled/defender_ensure_wdatp_is_enabled_test.py b/tests/providers/azure/services/defender/defender_ensure_wdatp_is_enabled/defender_ensure_wdatp_is_enabled_test.py index fba1d24ba8..202e332b3f 100644 --- a/tests/providers/azure/services/defender/defender_ensure_wdatp_is_enabled/defender_ensure_wdatp_is_enabled_test.py +++ b/tests/providers/azure/services/defender/defender_ensure_wdatp_is_enabled/defender_ensure_wdatp_is_enabled_test.py @@ -3,7 +3,9 @@ from uuid import uuid4 from prowler.providers.azure.services.defender.defender_service import Setting from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_defender_ensure_wdatp_is_enabled: def test_defender_no_settings(self): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.settings = {} with ( @@ -34,6 +37,7 @@ class Test_defender_ensure_wdatp_is_enabled: def test_defender_wdatp_disabled(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.settings = { AZURE_SUBSCRIPTION_ID: { "WDATP": Setting( @@ -66,7 +70,7 @@ class Test_defender_ensure_wdatp_is_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Microsoft Defender for Endpoint integration is disabled for subscription {AZURE_SUBSCRIPTION_ID}." + == f"Microsoft Defender for Endpoint integration is disabled for subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "WDATP" @@ -75,6 +79,7 @@ class Test_defender_ensure_wdatp_is_enabled: def test_defender_wdatp_enabled(self): resource_id = str(uuid4()) defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.settings = { AZURE_SUBSCRIPTION_ID: { "WDATP": Setting( @@ -107,7 +112,7 @@ class Test_defender_ensure_wdatp_is_enabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Microsoft Defender for Endpoint integration is enabled for subscription {AZURE_SUBSCRIPTION_ID}." + == f"Microsoft Defender for Endpoint integration is enabled for subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "WDATP" @@ -116,7 +121,7 @@ class Test_defender_ensure_wdatp_is_enabled: def test_defender_wdatp_no_settings(self): defender_client = mock.MagicMock defender_client.settings = {AZURE_SUBSCRIPTION_ID: {}} - defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_ID} + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -138,7 +143,7 @@ class Test_defender_ensure_wdatp_is_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Microsoft Defender for Endpoint integration not exists for subscription {AZURE_SUBSCRIPTION_ID}." + == f"Microsoft Defender for Endpoint integration not exists for subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == AZURE_SUBSCRIPTION_ID diff --git a/tests/providers/azure/services/entra/entra_user_with_vm_access_has_mfa/entra_user_with_vm_access_has_mfa_test.py b/tests/providers/azure/services/entra/entra_user_with_vm_access_has_mfa/entra_user_with_vm_access_has_mfa_test.py index b9ebe959ef..46dc9389af 100644 --- a/tests/providers/azure/services/entra/entra_user_with_vm_access_has_mfa/entra_user_with_vm_access_has_mfa_test.py +++ b/tests/providers/azure/services/entra/entra_user_with_vm_access_has_mfa/entra_user_with_vm_access_has_mfa_test.py @@ -3,7 +3,9 @@ from uuid import uuid4 from prowler.providers.azure.config import VIRTUAL_MACHINE_ADMINISTRATOR_LOGIN_ROLE_ID from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, DOMAIN, set_mocked_azure_provider, ) @@ -12,7 +14,9 @@ from tests.providers.azure.azure_fixtures import ( class Test_iam_assignment_priviledge_access_vm_has_mfa: def test_iam_no_roles(self): iam_client = mock.MagicMock + iam_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} entra_client = mock.MagicMock + entra_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -37,8 +41,10 @@ class Test_iam_assignment_priviledge_access_vm_has_mfa: def test_entra_user_with_vm_access_has_mfa(self): iam_client = mock.MagicMock + iam_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} role_assigment_id = str(uuid4()) entra_client = mock.MagicMock + entra_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} user_id = str(uuid4()) with ( @@ -98,7 +104,7 @@ class Test_iam_assignment_priviledge_access_vm_has_mfa: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"User test can access VMs in subscription {AZURE_SUBSCRIPTION_ID} but it has MFA." + == f"User test can access VMs in subscription {AZURE_SUBSCRIPTION_DISPLAY} but it has MFA." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "test" @@ -106,8 +112,10 @@ class Test_iam_assignment_priviledge_access_vm_has_mfa: def test_entra_user_with_vm_access_has_mfa_no_mfa(self): iam_client = mock.MagicMock + iam_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} role_assigment_id = str(uuid4()) entra_client = mock.MagicMock + entra_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} user_id = str(uuid4()) with ( @@ -167,7 +175,7 @@ class Test_iam_assignment_priviledge_access_vm_has_mfa: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"User test without MFA can access VMs in subscription {AZURE_SUBSCRIPTION_ID}" + == f"User test without MFA can access VMs in subscription {AZURE_SUBSCRIPTION_DISPLAY}" ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == "test" @@ -175,8 +183,10 @@ class Test_iam_assignment_priviledge_access_vm_has_mfa: def test_entra_user_with_vm_access_has_mfa_no_user(self): iam_client = mock.MagicMock + iam_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} role_assigment_id = str(uuid4()) entra_client = mock.MagicMock + entra_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} user_id = str(uuid4()) with ( @@ -227,8 +237,10 @@ class Test_iam_assignment_priviledge_access_vm_has_mfa: def test_entra_user_with_vm_access_has_mfa_no_role(self): iam_client = mock.MagicMock + iam_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} role_assigment_id = str(uuid4()) entra_client = mock.MagicMock + entra_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} user_id = str(uuid4()) with ( diff --git a/tests/providers/azure/services/iam/iam_custom_role_has_permissions_to_administer_resource_locks/iam_custom_role_has_permissions_to_administer_resource_locks_test.py b/tests/providers/azure/services/iam/iam_custom_role_has_permissions_to_administer_resource_locks/iam_custom_role_has_permissions_to_administer_resource_locks_test.py index eaa59eff14..5125130871 100644 --- a/tests/providers/azure/services/iam/iam_custom_role_has_permissions_to_administer_resource_locks/iam_custom_role_has_permissions_to_administer_resource_locks_test.py +++ b/tests/providers/azure/services/iam/iam_custom_role_has_permissions_to_administer_resource_locks/iam_custom_role_has_permissions_to_administer_resource_locks_test.py @@ -4,7 +4,9 @@ from azure.mgmt.authorization.v2022_04_01.models import Permission from prowler.providers.azure.services.iam.iam_service import Role from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -12,6 +14,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_iam_custom_role_has_permissions_to_administer_resource_locks: def test_iam_no_roles(self): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.custom_roles = {} with ( @@ -36,6 +39,7 @@ class Test_iam_custom_role_has_permissions_to_administer_resource_locks: self, ): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} role_name = "test-role" defender_client.custom_roles = { AZURE_SUBSCRIPTION_ID: { @@ -76,7 +80,7 @@ class Test_iam_custom_role_has_permissions_to_administer_resource_locks: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Role {role_name} from subscription {AZURE_SUBSCRIPTION_ID} has permission to administer resource locks." + == f"Role {role_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has permission to administer resource locks." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert ( @@ -91,6 +95,7 @@ class Test_iam_custom_role_has_permissions_to_administer_resource_locks: self, ): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} role_name = "test-role" defender_client.custom_roles = { AZURE_SUBSCRIPTION_ID: { @@ -124,7 +129,7 @@ class Test_iam_custom_role_has_permissions_to_administer_resource_locks: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Role {role_name} from subscription {AZURE_SUBSCRIPTION_ID} has no permission to administer resource locks." + == f"Role {role_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has no permission to administer resource locks." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert ( @@ -139,6 +144,7 @@ class Test_iam_custom_role_has_permissions_to_administer_resource_locks: self, ): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} role_name = "test-role" role_name2 = "test-role2" defender_client.custom_roles = { @@ -194,7 +200,7 @@ class Test_iam_custom_role_has_permissions_to_administer_resource_locks: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Role {role_name} from subscription {AZURE_SUBSCRIPTION_ID} has permission to administer resource locks." + == f"Role {role_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has permission to administer resource locks." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert ( @@ -206,6 +212,7 @@ class Test_iam_custom_role_has_permissions_to_administer_resource_locks: def test_iam_custom_roles_empty_list_but_with_key(self): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.custom_roles = {AZURE_SUBSCRIPTION_ID: {}} with ( diff --git a/tests/providers/azure/services/iam/iam_role_user_access_admin_restricted/iam_role_user_access_admin_restricted_test.py b/tests/providers/azure/services/iam/iam_role_user_access_admin_restricted/iam_role_user_access_admin_restricted_test.py index dbf842d589..8ccf6e6f64 100644 --- a/tests/providers/azure/services/iam/iam_role_user_access_admin_restricted/iam_role_user_access_admin_restricted_test.py +++ b/tests/providers/azure/services/iam/iam_role_user_access_admin_restricted/iam_role_user_access_admin_restricted_test.py @@ -3,7 +3,9 @@ from uuid import uuid4 from prowler.providers.azure.services.iam.iam_service import Role, RoleAssignment from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_iam_role_user_access_admin_restricted: def test_iam_no_role_assignments(self): iam_client = mock.MagicMock + iam_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} iam_client.role_assignments = {} iam_client.roles = {} @@ -40,11 +43,11 @@ class Test_iam_role_user_access_admin_restricted: role_name = "User Access Administrator" iam_client.subscriptions = { - "subscription-name-1": AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME, } iam_client.role_assignments = { - "subscription-name-1": { + AZURE_SUBSCRIPTION_ID: { role_assignment_id: RoleAssignment( id=role_assignment_id, name="test-assignment", @@ -56,7 +59,7 @@ class Test_iam_role_user_access_admin_restricted: } } iam_client.roles = { - "subscription-name-1": { + AZURE_SUBSCRIPTION_ID: { f"/subscriptions/{AZURE_SUBSCRIPTION_ID}/providers/Microsoft.Authorization/roleDefinitions/{role_id}": Role( id=role_id, name=role_name, @@ -87,9 +90,9 @@ class Test_iam_role_user_access_admin_restricted: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Role assignment test-assignment in subscription subscription-name-1 grants User Access Administrator role to User {agent_id}." + == f"Role assignment test-assignment in subscription {AZURE_SUBSCRIPTION_DISPLAY} grants User Access Administrator role to User {agent_id}." ) - assert result[0].subscription == "subscription-name-1" + assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_id == role_assignment_id def test_iam_non_user_access_administrator_role_assigned(self): @@ -100,11 +103,11 @@ class Test_iam_role_user_access_admin_restricted: role_name = "Reader" iam_client.subscriptions = { - "subscription-name-1": AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME, } iam_client.role_assignments = { - "subscription-name-1": { + AZURE_SUBSCRIPTION_ID: { role_assignment_id: RoleAssignment( id=role_assignment_id, name="test-assignment", @@ -116,7 +119,7 @@ class Test_iam_role_user_access_admin_restricted: } } iam_client.roles = { - "subscription-name-1": { + AZURE_SUBSCRIPTION_ID: { f"/subscriptions/{AZURE_SUBSCRIPTION_ID}/providers/Microsoft.Authorization/roleDefinitions/{role_id}": Role( id=role_id, name=role_name, @@ -147,7 +150,7 @@ class Test_iam_role_user_access_admin_restricted: assert result[0].status == "PASS" assert ( result[0].status_extended - == "Role assignment test-assignment in subscription subscription-name-1 does not grant User Access Administrator role." + == f"Role assignment test-assignment in subscription {AZURE_SUBSCRIPTION_DISPLAY} does not grant User Access Administrator role." ) - assert result[0].subscription == "subscription-name-1" + assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_id == role_assignment_id diff --git a/tests/providers/azure/services/iam/iam_subscription_roles_owner_custom_not_created/iam_subscription_roles_owner_custom_not_created_test.py b/tests/providers/azure/services/iam/iam_subscription_roles_owner_custom_not_created/iam_subscription_roles_owner_custom_not_created_test.py index 7f15b69466..1d2d37ee11 100644 --- a/tests/providers/azure/services/iam/iam_subscription_roles_owner_custom_not_created/iam_subscription_roles_owner_custom_not_created_test.py +++ b/tests/providers/azure/services/iam/iam_subscription_roles_owner_custom_not_created/iam_subscription_roles_owner_custom_not_created_test.py @@ -4,7 +4,9 @@ from azure.mgmt.authorization.v2022_04_01.models import Permission from prowler.providers.azure.services.iam.iam_service import Role from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -12,6 +14,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_iam_subscription_roles_owner_custom_not_created: def test_iam_no_roles(self): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.custom_roles = {} with ( @@ -34,6 +37,7 @@ class Test_iam_subscription_roles_owner_custom_not_created: def test_iam_custom_owner_role_created_with_all(self): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} role_name = "test-role" defender_client.custom_roles = { AZURE_SUBSCRIPTION_ID: { @@ -67,7 +71,7 @@ class Test_iam_subscription_roles_owner_custom_not_created: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Role {role_name} from subscription {AZURE_SUBSCRIPTION_ID} is a custom owner role." + == f"Role {role_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} is a custom owner role." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert ( @@ -80,6 +84,7 @@ class Test_iam_subscription_roles_owner_custom_not_created: def test_iam_custom_owner_role_created_with_no_permissions(self): defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} role_name = "test-role" defender_client.custom_roles = { AZURE_SUBSCRIPTION_ID: { @@ -113,7 +118,7 @@ class Test_iam_subscription_roles_owner_custom_not_created: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Role {role_name} from subscription {AZURE_SUBSCRIPTION_ID} is not a custom owner role." + == f"Role {role_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} is not a custom owner role." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert ( diff --git a/tests/providers/azure/services/keyvault/keyvault_access_only_through_private_endpoints/keyvault_access_only_through_private_endpoints_test.py b/tests/providers/azure/services/keyvault/keyvault_access_only_through_private_endpoints/keyvault_access_only_through_private_endpoints_test.py index 793b3b4912..4244684d1a 100644 --- a/tests/providers/azure/services/keyvault/keyvault_access_only_through_private_endpoints/keyvault_access_only_through_private_endpoints_test.py +++ b/tests/providers/azure/services/keyvault/keyvault_access_only_through_private_endpoints/keyvault_access_only_through_private_endpoints_test.py @@ -2,7 +2,9 @@ from unittest import mock from uuid import uuid4 from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -10,6 +12,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_keyvault_access_only_through_private_endpoints: def test_no_key_vaults(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_client.key_vaults = {} with ( @@ -32,6 +35,7 @@ class Test_keyvault_access_only_through_private_endpoints: def test_key_vaults_no_private_endpoints(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_name = "Keyvault Name" keyvault_id = str(uuid4()) @@ -78,6 +82,7 @@ class Test_keyvault_access_only_through_private_endpoints: def test_key_vaults_with_private_endpoints_public_access_enabled(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_name = "Keyvault Name" keyvault_id = str(uuid4()) @@ -127,7 +132,7 @@ class Test_keyvault_access_only_through_private_endpoints: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Keyvault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_ID} has public network access enabled while using private endpoints." + == f"Keyvault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has public network access enabled while using private endpoints." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == keyvault_name @@ -136,6 +141,7 @@ class Test_keyvault_access_only_through_private_endpoints: def test_key_vaults_with_private_endpoints_public_access_disabled(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_name = "Keyvault Name" keyvault_id = str(uuid4()) @@ -185,7 +191,7 @@ class Test_keyvault_access_only_through_private_endpoints: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Keyvault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_ID} has public network access disabled and is using private endpoints." + == f"Keyvault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has public network access disabled and is using private endpoints." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == keyvault_name diff --git a/tests/providers/azure/services/keyvault/keyvault_key_expiration_set_in_non_rbac/keyvault_key_expiration_set_in_non_rbac_test.py b/tests/providers/azure/services/keyvault/keyvault_key_expiration_set_in_non_rbac/keyvault_key_expiration_set_in_non_rbac_test.py index 5995971555..9da66f9eda 100644 --- a/tests/providers/azure/services/keyvault/keyvault_key_expiration_set_in_non_rbac/keyvault_key_expiration_set_in_non_rbac_test.py +++ b/tests/providers/azure/services/keyvault/keyvault_key_expiration_set_in_non_rbac/keyvault_key_expiration_set_in_non_rbac_test.py @@ -4,7 +4,9 @@ from uuid import uuid4 from azure.mgmt.keyvault.v2023_07_01.models import KeyAttributes, VaultProperties from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -12,6 +14,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_keyvault_key_expiration_set_in_non_rbac: def test_no_key_vaults(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_client.key_vaults = {} with ( @@ -34,6 +37,7 @@ class Test_keyvault_key_expiration_set_in_non_rbac: def test_no_keys(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -75,6 +79,7 @@ class Test_keyvault_key_expiration_set_in_non_rbac: def test_key_vaults_invalid_keys(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_name = "Keyvault Name" keyvault_id = str(uuid4()) key_name = "Key Name" @@ -127,7 +132,7 @@ class Test_keyvault_key_expiration_set_in_non_rbac: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Key {key_name} in Key Vault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_ID} does not have an expiration date set." + == f"Key {key_name} in Key Vault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} does not have an expiration date set." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == key_name @@ -136,6 +141,7 @@ class Test_keyvault_key_expiration_set_in_non_rbac: def test_key_vaults_valid_keys(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_name = "Keyvault Name" keyvault_id = str(uuid4()) key_name = "Key Name" @@ -188,7 +194,7 @@ class Test_keyvault_key_expiration_set_in_non_rbac: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Key {key_name} in Key Vault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_ID} has an expiration date set." + == f"Key {key_name} in Key Vault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has an expiration date set." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == key_name @@ -197,6 +203,7 @@ class Test_keyvault_key_expiration_set_in_non_rbac: def test_disabled_key_skipped(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_name = "Keyvault Name" keyvault_id = str(uuid4()) @@ -248,6 +255,7 @@ class Test_keyvault_key_expiration_set_in_non_rbac: def test_multiple_keys_mixed_expiration(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_name = "Keyvault Name" keyvault_id = str(uuid4()) key_with_expiry = "key_with_expiry" diff --git a/tests/providers/azure/services/keyvault/keyvault_key_rotation_enabled/keyvault_key_rotation_enabled_test.py b/tests/providers/azure/services/keyvault/keyvault_key_rotation_enabled/keyvault_key_rotation_enabled_test.py index 75d1e922e2..fd6e2495ce 100644 --- a/tests/providers/azure/services/keyvault/keyvault_key_rotation_enabled/keyvault_key_rotation_enabled_test.py +++ b/tests/providers/azure/services/keyvault/keyvault_key_rotation_enabled/keyvault_key_rotation_enabled_test.py @@ -4,7 +4,9 @@ from azure.keyvault.keys import KeyRotationLifetimeAction, KeyRotationPolicy from azure.mgmt.keyvault.v2023_07_01.models import KeyAttributes, VaultProperties from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -12,6 +14,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_keyvault_key_rotation_enabled: def test_no_key_vaults(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_client.key_vaults = {} with ( @@ -34,6 +37,7 @@ class Test_keyvault_key_rotation_enabled: def test_no_keys(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -75,6 +79,7 @@ class Test_keyvault_key_rotation_enabled: def test_key_without_rotation_policy(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_name = "keyvault_name" key_name = "key_name" @@ -128,7 +133,7 @@ class Test_keyvault_key_rotation_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Key {key_name} in Key Vault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_ID} does not have a rotation policy set." + == f"Key {key_name} in Key Vault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} does not have a rotation policy set." ) assert result[0].resource_name == key_name assert result[0].resource_id == "id" @@ -137,6 +142,7 @@ class Test_keyvault_key_rotation_enabled: def test_key_with_rotation_policy(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_name = "keyvault_name" key_name = "key_name" @@ -198,7 +204,7 @@ class Test_keyvault_key_rotation_enabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Key {key_name} in Key Vault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_ID} has a rotation policy set." + == f"Key {key_name} in Key Vault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has a rotation policy set." ) assert result[0].resource_name == key_name assert result[0].resource_id == "id" @@ -207,6 +213,7 @@ class Test_keyvault_key_rotation_enabled: def test_multiple_keys_mixed_rotation_policies(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_name = "keyvault_name" key_with_rotation = "key_with_rotation" key_without_rotation = "key_without_rotation" @@ -306,6 +313,7 @@ class Test_keyvault_key_rotation_enabled: def test_rotation_action_not_first_in_lifetime_actions(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_name = "keyvault_name" key_name = "key_name" @@ -372,5 +380,5 @@ class Test_keyvault_key_rotation_enabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Key {key_name} in Key Vault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_ID} has a rotation policy set." + == f"Key {key_name} in Key Vault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has a rotation policy set." ) diff --git a/tests/providers/azure/services/keyvault/keyvault_logging_enabled/keyvault_logging_enabled_test.py b/tests/providers/azure/services/keyvault/keyvault_logging_enabled/keyvault_logging_enabled_test.py index 7594860d76..2fff845a7d 100644 --- a/tests/providers/azure/services/keyvault/keyvault_logging_enabled/keyvault_logging_enabled_test.py +++ b/tests/providers/azure/services/keyvault/keyvault_logging_enabled/keyvault_logging_enabled_test.py @@ -3,7 +3,9 @@ from unittest import mock from azure.mgmt.keyvault.v2023_07_01.models import VaultProperties from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_keyvault_logging_enabled: def test_no_key_vaults(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_client.key_vaults = {} with ( @@ -37,6 +40,7 @@ class Test_keyvault_logging_enabled: def test_no_diagnostic_settings(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -83,13 +87,14 @@ class Test_keyvault_logging_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Key Vault name_keyvault in subscription {AZURE_SUBSCRIPTION_ID} does not have a diagnostic setting with audit logging." + == f"Key Vault name_keyvault in subscription {AZURE_SUBSCRIPTION_DISPLAY} does not have a diagnostic setting with audit logging." ) assert result[0].resource_name == "name_keyvault" assert result[0].resource_id == "id" def test_diagnostic_setting_without_audit_logging(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -158,13 +163,14 @@ class Test_keyvault_logging_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Key Vault name_keyvault in subscription {AZURE_SUBSCRIPTION_ID} does not have a diagnostic setting with audit logging." + == f"Key Vault name_keyvault in subscription {AZURE_SUBSCRIPTION_DISPLAY} does not have a diagnostic setting with audit logging." ) assert result[0].resource_name == "name_keyvault" assert result[0].resource_id == "id" def test_diagnostic_setting_with_audit_logging(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -233,13 +239,14 @@ class Test_keyvault_logging_enabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Key Vault name_keyvault in subscription {AZURE_SUBSCRIPTION_ID} has a diagnostic setting with audit logging." + == f"Key Vault name_keyvault in subscription {AZURE_SUBSCRIPTION_DISPLAY} has a diagnostic setting with audit logging." ) assert result[0].resource_name == "name_keyvault" assert result[0].resource_id == "id" def test_multiple_diagnostic_settings_one_compliant(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -329,6 +336,7 @@ class Test_keyvault_logging_enabled: def test_multiple_vaults_mixed(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( diff --git a/tests/providers/azure/services/keyvault/keyvault_non_rbac_secret_expiration_set/keyvault_non_rbac_secret_expiration_set_test.py b/tests/providers/azure/services/keyvault/keyvault_non_rbac_secret_expiration_set/keyvault_non_rbac_secret_expiration_set_test.py index 22b7664ec4..eb6036f9c7 100644 --- a/tests/providers/azure/services/keyvault/keyvault_non_rbac_secret_expiration_set/keyvault_non_rbac_secret_expiration_set_test.py +++ b/tests/providers/azure/services/keyvault/keyvault_non_rbac_secret_expiration_set/keyvault_non_rbac_secret_expiration_set_test.py @@ -4,7 +4,9 @@ from uuid import uuid4 from azure.mgmt.keyvault.v2023_07_01.models import SecretAttributes, VaultProperties from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -12,6 +14,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_keyvault_non_rbac_secret_expiration_set: def test_no_key_vaults(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_client.key_vaults = {} with ( @@ -34,6 +37,7 @@ class Test_keyvault_non_rbac_secret_expiration_set: def test_no_secrets(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -76,6 +80,7 @@ class Test_keyvault_non_rbac_secret_expiration_set: def test_key_vaults_invalid_secrets(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_name = "Keyvault Name" keyvault_id = str(uuid4()) secret_name = "Secret" @@ -128,7 +133,7 @@ class Test_keyvault_non_rbac_secret_expiration_set: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Secret {secret_name} in Key Vault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_ID} does not have an expiration date set." + == f"Secret {secret_name} in Key Vault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} does not have an expiration date set." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == secret_name @@ -137,6 +142,7 @@ class Test_keyvault_non_rbac_secret_expiration_set: def test_key_vaults_invalid_multiple_secrets(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_name = "Keyvault Name" keyvault_id = str(uuid4()) secret1_name = "Secret1" @@ -202,6 +208,7 @@ class Test_keyvault_non_rbac_secret_expiration_set: def test_key_vaults_valid_secrets(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_name = "Keyvault Name" keyvault_id = str(uuid4()) secret_name = "name" @@ -254,7 +261,7 @@ class Test_keyvault_non_rbac_secret_expiration_set: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Secret {secret_name} in Key Vault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_ID} has an expiration date set." + == f"Secret {secret_name} in Key Vault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has an expiration date set." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == secret_name @@ -263,6 +270,7 @@ class Test_keyvault_non_rbac_secret_expiration_set: def test_disabled_secret_skipped(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_name = "Keyvault Name" keyvault_id = str(uuid4()) diff --git a/tests/providers/azure/services/keyvault/keyvault_private_endpoints/keyvault_private_endpoints_test.py b/tests/providers/azure/services/keyvault/keyvault_private_endpoints/keyvault_private_endpoints_test.py index e071453485..783ba6c319 100644 --- a/tests/providers/azure/services/keyvault/keyvault_private_endpoints/keyvault_private_endpoints_test.py +++ b/tests/providers/azure/services/keyvault/keyvault_private_endpoints/keyvault_private_endpoints_test.py @@ -7,7 +7,9 @@ from azure.mgmt.keyvault.v2023_07_01.models import ( ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -15,6 +17,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_keyvault_private_endpoints: def test_no_key_vaults(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_client.key_vaults = {} with ( @@ -37,6 +40,7 @@ class Test_keyvault_private_endpoints: def test_key_vaults_no_private_endpoints(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_name = "Keyvault Name" keyvault_id = str(uuid4()) @@ -82,7 +86,7 @@ class Test_keyvault_private_endpoints: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Keyvault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_ID} is not using private endpoints." + == f"Keyvault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} is not using private endpoints." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == keyvault_name @@ -91,6 +95,7 @@ class Test_keyvault_private_endpoints: def test_key_vaults_using_private_endpoints(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_name = "Keyvault Name" keyvault_id = str(uuid4()) private_endpoint = PrivateEndpointConnectionItem( @@ -141,7 +146,7 @@ class Test_keyvault_private_endpoints: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Keyvault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_ID} is using private endpoints." + == f"Keyvault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} is using private endpoints." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == keyvault_name diff --git a/tests/providers/azure/services/keyvault/keyvault_rbac_enabled/keyvault_rbac_enabled_test.py b/tests/providers/azure/services/keyvault/keyvault_rbac_enabled/keyvault_rbac_enabled_test.py index 00729ab0bf..be72938b0e 100644 --- a/tests/providers/azure/services/keyvault/keyvault_rbac_enabled/keyvault_rbac_enabled_test.py +++ b/tests/providers/azure/services/keyvault/keyvault_rbac_enabled/keyvault_rbac_enabled_test.py @@ -4,7 +4,9 @@ from uuid import uuid4 from azure.mgmt.keyvault.v2023_07_01.models import VaultProperties from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -12,6 +14,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_keyvault_rbac_enabled: def test_no_key_vaults(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_client.key_vaults = {} with ( @@ -34,6 +37,7 @@ class Test_keyvault_rbac_enabled: def test_key_vaults_no_rbac(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_name = "Keyvault Name" keyvault_id = str(uuid4()) @@ -77,7 +81,7 @@ class Test_keyvault_rbac_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Keyvault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_ID} is not using RBAC for access control." + == f"Keyvault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} is not using RBAC for access control." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == keyvault_name @@ -86,6 +90,7 @@ class Test_keyvault_rbac_enabled: def test_key_vaults_rbac(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_name = "Keyvault Name" keyvault_id = str(uuid4()) @@ -129,7 +134,7 @@ class Test_keyvault_rbac_enabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Keyvault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_ID} is using RBAC for access control." + == f"Keyvault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} is using RBAC for access control." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == keyvault_name diff --git a/tests/providers/azure/services/keyvault/keyvault_rbac_key_expiration_set/keyvault_rbac_key_expiration_set_test.py b/tests/providers/azure/services/keyvault/keyvault_rbac_key_expiration_set/keyvault_rbac_key_expiration_set_test.py index c12c000526..b73948adef 100644 --- a/tests/providers/azure/services/keyvault/keyvault_rbac_key_expiration_set/keyvault_rbac_key_expiration_set_test.py +++ b/tests/providers/azure/services/keyvault/keyvault_rbac_key_expiration_set/keyvault_rbac_key_expiration_set_test.py @@ -4,7 +4,9 @@ from uuid import uuid4 from azure.mgmt.keyvault.v2023_07_01.models import KeyAttributes, VaultProperties from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -12,6 +14,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_keyvault_rbac_key_expiration_set: def test_no_key_vaults(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_client.key_vaults = {} with ( @@ -34,6 +37,7 @@ class Test_keyvault_rbac_key_expiration_set: def test_no_keys(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -75,6 +79,7 @@ class Test_keyvault_rbac_key_expiration_set: def test_key_vaults_invalid_keys(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_name = "Keyvault Name" keyvault_id = str(uuid4()) key_name = "Key Name" @@ -127,7 +132,7 @@ class Test_keyvault_rbac_key_expiration_set: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Key {key_name} in Key Vault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_ID} does not have an expiration date set." + == f"Key {key_name} in Key Vault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} does not have an expiration date set." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == key_name @@ -136,6 +141,7 @@ class Test_keyvault_rbac_key_expiration_set: def test_key_vaults_valid_keys(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_name = "Keyvault Name" keyvault_id = str(uuid4()) key_name = "Key Name" @@ -188,7 +194,7 @@ class Test_keyvault_rbac_key_expiration_set: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Key {key_name} in Key Vault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_ID} has an expiration date set." + == f"Key {key_name} in Key Vault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has an expiration date set." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == key_name @@ -197,6 +203,7 @@ class Test_keyvault_rbac_key_expiration_set: def test_disabled_key_skipped(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_name = "Keyvault Name" keyvault_id = str(uuid4()) @@ -248,6 +255,7 @@ class Test_keyvault_rbac_key_expiration_set: def test_multiple_keys_mixed_expiration(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_name = "Keyvault Name" keyvault_id = str(uuid4()) key_with_expiry = "key_with_expiry" diff --git a/tests/providers/azure/services/keyvault/keyvault_rbac_secret_expiration_set/keyvault_rbac_secret_expiration_set_test.py b/tests/providers/azure/services/keyvault/keyvault_rbac_secret_expiration_set/keyvault_rbac_secret_expiration_set_test.py index d6b78380bc..dffcca1f7a 100644 --- a/tests/providers/azure/services/keyvault/keyvault_rbac_secret_expiration_set/keyvault_rbac_secret_expiration_set_test.py +++ b/tests/providers/azure/services/keyvault/keyvault_rbac_secret_expiration_set/keyvault_rbac_secret_expiration_set_test.py @@ -4,7 +4,9 @@ from uuid import uuid4 from azure.mgmt.keyvault.v2023_07_01.models import SecretAttributes, VaultProperties from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -12,6 +14,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_keyvault_rbac_secret_expiration_set: def test_no_key_vaults(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_client.key_vaults = {} with ( @@ -34,6 +37,7 @@ class Test_keyvault_rbac_secret_expiration_set: def test_no_secrets(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -75,6 +79,7 @@ class Test_keyvault_rbac_secret_expiration_set: def test_key_vaults_invalid_secrets(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_name = "Keyvault Name" keyvault_id = str(uuid4()) secret_name = "Secret" @@ -127,7 +132,7 @@ class Test_keyvault_rbac_secret_expiration_set: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Secret {secret_name} in Key Vault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_ID} does not have an expiration date set." + == f"Secret {secret_name} in Key Vault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} does not have an expiration date set." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == secret_name @@ -136,6 +141,7 @@ class Test_keyvault_rbac_secret_expiration_set: def test_key_vaults_invalid_multiple_secrets(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_name = "Keyvault Name" keyvault_id = str(uuid4()) secret1_name = "Secret1" @@ -201,6 +207,7 @@ class Test_keyvault_rbac_secret_expiration_set: def test_key_vaults_valid_secrets(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_name = "Keyvault Name" keyvault_id = str(uuid4()) secret_name = "name" @@ -253,7 +260,7 @@ class Test_keyvault_rbac_secret_expiration_set: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Secret {secret_name} in Key Vault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_ID} has an expiration date set." + == f"Secret {secret_name} in Key Vault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has an expiration date set." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == secret_name @@ -262,6 +269,7 @@ class Test_keyvault_rbac_secret_expiration_set: def test_disabled_secret_skipped(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_name = "Keyvault Name" keyvault_id = str(uuid4()) diff --git a/tests/providers/azure/services/keyvault/keyvault_recoverable/keyvault_recoverable_test.py b/tests/providers/azure/services/keyvault/keyvault_recoverable/keyvault_recoverable_test.py index 733683d7a1..f0a0592e02 100644 --- a/tests/providers/azure/services/keyvault/keyvault_recoverable/keyvault_recoverable_test.py +++ b/tests/providers/azure/services/keyvault/keyvault_recoverable/keyvault_recoverable_test.py @@ -4,7 +4,9 @@ from uuid import uuid4 from azure.mgmt.keyvault.v2023_07_01.models import SecretAttributes, VaultProperties from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -13,6 +15,7 @@ class Test_keyvault_recoverable: def test_no_key_vaults(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_client.key_vaults = {} with ( @@ -35,6 +38,7 @@ class Test_keyvault_recoverable: def test_key_vaults_no_purge(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_name = "Keyvault Name" keyvault_id = str(uuid4()) @@ -80,7 +84,7 @@ class Test_keyvault_recoverable: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Keyvault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_ID} is not recoverable." + == f"Keyvault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} is not recoverable." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == keyvault_name @@ -89,6 +93,7 @@ class Test_keyvault_recoverable: def test_key_vaults_no_soft_delete(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_name = "Keyvault Name" keyvault_id = str(uuid4()) @@ -149,7 +154,7 @@ class Test_keyvault_recoverable: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Keyvault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_ID} is not recoverable." + == f"Keyvault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} is not recoverable." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == keyvault_name @@ -158,6 +163,7 @@ class Test_keyvault_recoverable: def test_key_vaults_valid_configuration(self): keyvault_client = mock.MagicMock + keyvault_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} keyvault_name = "Keyvault Name" keyvault_id = str(uuid4()) @@ -211,7 +217,7 @@ class Test_keyvault_recoverable: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Keyvault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_ID} is recoverable." + == f"Keyvault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} is recoverable." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == keyvault_name diff --git a/tests/providers/azure/services/monitor/monitor_alert_create_policy_assignment/monitor_alert_create_policy_assignment_test.py b/tests/providers/azure/services/monitor/monitor_alert_create_policy_assignment/monitor_alert_create_policy_assignment_test.py index 4e380d2c33..d785de72f4 100644 --- a/tests/providers/azure/services/monitor/monitor_alert_create_policy_assignment/monitor_alert_create_policy_assignment_test.py +++ b/tests/providers/azure/services/monitor/monitor_alert_create_policy_assignment/monitor_alert_create_policy_assignment_test.py @@ -3,7 +3,9 @@ from unittest import mock from azure.mgmt.monitor.models import AlertRuleAnyOfOrLeafCondition from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_monitor_alert_create_policy_assignment: def test_monitor_alert_create_policy_assignment_no_subscriptions(self): monitor_client = mock.MagicMock + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} monitor_client.alert_rules = {} with ( @@ -34,7 +37,7 @@ class Test_monitor_alert_create_policy_assignment: def test_no_alert_rules(self): monitor_client = mock.MagicMock monitor_client.alert_rules = {AZURE_SUBSCRIPTION_ID: []} - monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_ID} + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -58,11 +61,12 @@ class Test_monitor_alert_create_policy_assignment: assert result[0].resource_id == f"/subscriptions/{AZURE_SUBSCRIPTION_ID}" assert ( result[0].status_extended - == f"There is not an alert for creating Policy Assignments in subscription {AZURE_SUBSCRIPTION_ID}." + == f"There is not an alert for creating Policy Assignments in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) def test_alert_rules_configured(self): monitor_client = mock.MagicMock + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -125,5 +129,5 @@ class Test_monitor_alert_create_policy_assignment: assert result[0].resource_id == "id2" assert ( result[0].status_extended - == f"There is an alert configured for creating Policy Assignments in subscription {AZURE_SUBSCRIPTION_ID}." + == f"There is an alert configured for creating Policy Assignments in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) diff --git a/tests/providers/azure/services/monitor/monitor_alert_create_update_nsg/monitor_alert_create_update_nsg_test.py b/tests/providers/azure/services/monitor/monitor_alert_create_update_nsg/monitor_alert_create_update_nsg_test.py index ef620f468b..224a2eaca3 100644 --- a/tests/providers/azure/services/monitor/monitor_alert_create_update_nsg/monitor_alert_create_update_nsg_test.py +++ b/tests/providers/azure/services/monitor/monitor_alert_create_update_nsg/monitor_alert_create_update_nsg_test.py @@ -3,7 +3,9 @@ from unittest import mock from azure.mgmt.monitor.models import AlertRuleAnyOfOrLeafCondition from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_monitor_alert_create_update_nsg: def test_monitor_alert_create_update_nsg_no_subscriptions(self): monitor_client = mock.MagicMock + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} monitor_client.alert_rules = {} with ( mock.patch( @@ -33,7 +36,7 @@ class Test_monitor_alert_create_update_nsg: def test_no_alert_rules(self): monitor_client = mock.MagicMock() monitor_client.alert_rules = {AZURE_SUBSCRIPTION_ID: []} - monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_ID} + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -57,11 +60,12 @@ class Test_monitor_alert_create_update_nsg: assert result[0].resource_id == f"/subscriptions/{AZURE_SUBSCRIPTION_ID}" assert ( result[0].status_extended - == f"There is not an alert for creating/updating Network Security Groups in subscription {AZURE_SUBSCRIPTION_ID}." + == f"There is not an alert for creating/updating Network Security Groups in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) def test_alert_rules_configured(self): monitor_client = mock.MagicMock + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -123,5 +127,5 @@ class Test_monitor_alert_create_update_nsg: assert result[0].resource_id == "id2" assert ( result[0].status_extended - == f"There is an alert configured for creating/updating Network Security Groups in subscription {AZURE_SUBSCRIPTION_ID}." + == f"There is an alert configured for creating/updating Network Security Groups in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) diff --git a/tests/providers/azure/services/monitor/monitor_alert_create_update_public_ip_address_rule/monitor_alert_create_update_public_ip_address_rule_test.py b/tests/providers/azure/services/monitor/monitor_alert_create_update_public_ip_address_rule/monitor_alert_create_update_public_ip_address_rule_test.py index 987532ed24..0917f1f381 100644 --- a/tests/providers/azure/services/monitor/monitor_alert_create_update_public_ip_address_rule/monitor_alert_create_update_public_ip_address_rule_test.py +++ b/tests/providers/azure/services/monitor/monitor_alert_create_update_public_ip_address_rule/monitor_alert_create_update_public_ip_address_rule_test.py @@ -3,7 +3,9 @@ from unittest import mock from azure.mgmt.monitor.models import AlertRuleAnyOfOrLeafCondition from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_monitor_alert_create_update_security_solution: def test_monitor_alert_create_update_public_ip_address_rule_no_subscriptions(self): monitor_client = mock.MagicMock() + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} monitor_client.alert_rules = {} with ( mock.patch( @@ -33,7 +36,7 @@ class Test_monitor_alert_create_update_security_solution: def test_no_alert_rules(self): monitor_client = mock.MagicMock() monitor_client.alert_rules = {AZURE_SUBSCRIPTION_ID: []} - monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_ID} + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -57,11 +60,12 @@ class Test_monitor_alert_create_update_security_solution: assert result[0].resource_id == f"/subscriptions/{AZURE_SUBSCRIPTION_ID}" assert ( result[0].status_extended - == f"There is not an alert for creating/updating Public IP address rule in subscription {AZURE_SUBSCRIPTION_ID}." + == f"There is not an alert for creating/updating Public IP address rule in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) def test_alert_rules_configured(self): monitor_client = mock.MagicMock + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -124,5 +128,5 @@ class Test_monitor_alert_create_update_security_solution: assert result[0].resource_id == "id2" assert ( result[0].status_extended - == f"There is an alert configured for creating/updating Public IP address rule in subscription {AZURE_SUBSCRIPTION_ID}." + == f"There is an alert configured for creating/updating Public IP address rule in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) diff --git a/tests/providers/azure/services/monitor/monitor_alert_create_update_security_solution/monitor_alert_create_update_security_solution_test.py b/tests/providers/azure/services/monitor/monitor_alert_create_update_security_solution/monitor_alert_create_update_security_solution_test.py index ba7f475ca3..8638ba0718 100644 --- a/tests/providers/azure/services/monitor/monitor_alert_create_update_security_solution/monitor_alert_create_update_security_solution_test.py +++ b/tests/providers/azure/services/monitor/monitor_alert_create_update_security_solution/monitor_alert_create_update_security_solution_test.py @@ -3,7 +3,9 @@ from unittest import mock from azure.mgmt.monitor.models import AlertRuleAnyOfOrLeafCondition from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_monitor_alert_create_update_security_solution: def test_monitor_alert_create_update_security_solution_no_subscriptions(self): monitor_client = mock.MagicMock() + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} monitor_client.alert_rules = {} with ( mock.patch( @@ -33,7 +36,7 @@ class Test_monitor_alert_create_update_security_solution: def test_no_alert_rules(self): monitor_client = mock.MagicMock() monitor_client.alert_rules = {AZURE_SUBSCRIPTION_ID: []} - monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_ID} + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -57,11 +60,12 @@ class Test_monitor_alert_create_update_security_solution: assert result[0].resource_id == f"/subscriptions/{AZURE_SUBSCRIPTION_ID}" assert ( result[0].status_extended - == f"There is not an alert for creating/updating Security Solution in subscription {AZURE_SUBSCRIPTION_ID}." + == f"There is not an alert for creating/updating Security Solution in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) def test_alert_rules_configured(self): monitor_client = mock.MagicMock + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -124,5 +128,5 @@ class Test_monitor_alert_create_update_security_solution: assert result[0].resource_id == "id2" assert ( result[0].status_extended - == f"There is an alert configured for creating/updating Security Solution in subscription {AZURE_SUBSCRIPTION_ID}." + == f"There is an alert configured for creating/updating Security Solution in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) diff --git a/tests/providers/azure/services/monitor/monitor_alert_create_update_sqlserver_fr/monitor_alert_create_update_sqlserver_fr_test.py b/tests/providers/azure/services/monitor/monitor_alert_create_update_sqlserver_fr/monitor_alert_create_update_sqlserver_fr_test.py index 7195e321e3..d56c7e7ba8 100644 --- a/tests/providers/azure/services/monitor/monitor_alert_create_update_sqlserver_fr/monitor_alert_create_update_sqlserver_fr_test.py +++ b/tests/providers/azure/services/monitor/monitor_alert_create_update_sqlserver_fr/monitor_alert_create_update_sqlserver_fr_test.py @@ -3,7 +3,9 @@ from unittest import mock from azure.mgmt.monitor.models import AlertRuleAnyOfOrLeafCondition from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_monitor_alert_create_update_sqlserver_fr: def test_monitor_alert_create_update_sqlserver_fr_no_subscriptions(self): monitor_client = mock.MagicMock() + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} monitor_client.alert_rules = {} with ( mock.patch( @@ -33,7 +36,7 @@ class Test_monitor_alert_create_update_sqlserver_fr: def test_no_alert_rules(self): monitor_client = mock.MagicMock() monitor_client.alert_rules = {AZURE_SUBSCRIPTION_ID: []} - monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_ID} + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -57,11 +60,12 @@ class Test_monitor_alert_create_update_sqlserver_fr: assert result[0].resource_id == f"/subscriptions/{AZURE_SUBSCRIPTION_ID}" assert ( result[0].status_extended - == f"There is not an alert for creating/updating SQL Server firewall rule in subscription {AZURE_SUBSCRIPTION_ID}." + == f"There is not an alert for creating/updating SQL Server firewall rule in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) def test_alert_rules_configured(self): monitor_client = mock.MagicMock + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -124,5 +128,5 @@ class Test_monitor_alert_create_update_sqlserver_fr: assert result[0].resource_id == "id2" assert ( result[0].status_extended - == f"There is an alert configured for creating/updating SQL Server firewall rule in subscription {AZURE_SUBSCRIPTION_ID}." + == f"There is an alert configured for creating/updating SQL Server firewall rule in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) diff --git a/tests/providers/azure/services/monitor/monitor_alert_delete_nsg/monitor_alert_delete_nsg_test.py b/tests/providers/azure/services/monitor/monitor_alert_delete_nsg/monitor_alert_delete_nsg_test.py index 161b0de95d..a06a40b532 100644 --- a/tests/providers/azure/services/monitor/monitor_alert_delete_nsg/monitor_alert_delete_nsg_test.py +++ b/tests/providers/azure/services/monitor/monitor_alert_delete_nsg/monitor_alert_delete_nsg_test.py @@ -3,7 +3,9 @@ from unittest import mock from azure.mgmt.monitor.models import AlertRuleAnyOfOrLeafCondition from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_monitor_alert_delete_nsg: def test_monitor_alert_delete_nsg_no_subscriptions(self): monitor_client = mock.MagicMock() + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} monitor_client.alert_rules = {} with ( mock.patch( @@ -33,7 +36,7 @@ class Test_monitor_alert_delete_nsg: def test_no_alert_rules(self): monitor_client = mock.MagicMock() monitor_client.alert_rules = {AZURE_SUBSCRIPTION_ID: []} - monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_ID} + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -57,11 +60,12 @@ class Test_monitor_alert_delete_nsg: assert result[0].resource_id == f"/subscriptions/{AZURE_SUBSCRIPTION_ID}" assert ( result[0].status_extended - == f"There is not an alert for deleting Network Security Groups in subscription {AZURE_SUBSCRIPTION_ID}." + == f"There is not an alert for deleting Network Security Groups in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) def test_alert_rules_configured(self): monitor_client = mock.MagicMock + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -124,5 +128,5 @@ class Test_monitor_alert_delete_nsg: assert result[0].resource_id == "id2" assert ( result[0].status_extended - == f"There is an alert configured for deleting Network Security Groups in subscription {AZURE_SUBSCRIPTION_ID}." + == f"There is an alert configured for deleting Network Security Groups in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) diff --git a/tests/providers/azure/services/monitor/monitor_alert_delete_policy_assignment/monitor_alert_delete_policy_assignment_test.py b/tests/providers/azure/services/monitor/monitor_alert_delete_policy_assignment/monitor_alert_delete_policy_assignment_test.py index 1f52f15480..afa492a468 100644 --- a/tests/providers/azure/services/monitor/monitor_alert_delete_policy_assignment/monitor_alert_delete_policy_assignment_test.py +++ b/tests/providers/azure/services/monitor/monitor_alert_delete_policy_assignment/monitor_alert_delete_policy_assignment_test.py @@ -3,7 +3,9 @@ from unittest import mock from azure.mgmt.monitor.models import AlertRuleAnyOfOrLeafCondition from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_monitor_alert_delete_policy_assignment: def test_monitor_alert_delete_policy_assignment_no_subscriptions(self): monitor_client = mock.MagicMock + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} monitor_client.alert_rules = {} with ( @@ -34,7 +37,7 @@ class Test_monitor_alert_delete_policy_assignment: def test_no_alert_rules(self): monitor_client = mock.MagicMock monitor_client.alert_rules = {AZURE_SUBSCRIPTION_ID: []} - monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_ID} + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -58,11 +61,12 @@ class Test_monitor_alert_delete_policy_assignment: assert result[0].resource_id == f"/subscriptions/{AZURE_SUBSCRIPTION_ID}" assert ( result[0].status_extended - == f"There is not an alert for deleting policy assignment in subscription {AZURE_SUBSCRIPTION_ID}." + == f"There is not an alert for deleting policy assignment in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) def test_alert_rules_configured(self): monitor_client = mock.MagicMock + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -125,5 +129,5 @@ class Test_monitor_alert_delete_policy_assignment: assert result[0].resource_id == "id2" assert ( result[0].status_extended - == f"There is an alert configured for deleting policy assignment in subscription {AZURE_SUBSCRIPTION_ID}." + == f"There is an alert configured for deleting policy assignment in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) diff --git a/tests/providers/azure/services/monitor/monitor_alert_delete_public_ip_address_rule/monitor_alert_delete_public_ip_address_rule_test.py b/tests/providers/azure/services/monitor/monitor_alert_delete_public_ip_address_rule/monitor_alert_delete_public_ip_address_rule_test.py index 79aef33e20..44fdfd41c8 100644 --- a/tests/providers/azure/services/monitor/monitor_alert_delete_public_ip_address_rule/monitor_alert_delete_public_ip_address_rule_test.py +++ b/tests/providers/azure/services/monitor/monitor_alert_delete_public_ip_address_rule/monitor_alert_delete_public_ip_address_rule_test.py @@ -3,7 +3,9 @@ from unittest import mock from azure.mgmt.monitor.models import AlertRuleAnyOfOrLeafCondition from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_monitor_alert_create_update_security_solution: def test_monitor_alert_delete_public_ip_address_rule_no_subscriptions(self): monitor_client = mock.MagicMock() + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} monitor_client.alert_rules = {} with ( mock.patch( @@ -33,7 +36,7 @@ class Test_monitor_alert_create_update_security_solution: def test_no_alert_rules(self): monitor_client = mock.MagicMock() monitor_client.alert_rules = {AZURE_SUBSCRIPTION_ID: []} - monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_ID} + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -57,11 +60,12 @@ class Test_monitor_alert_create_update_security_solution: assert result[0].resource_id == f"/subscriptions/{AZURE_SUBSCRIPTION_ID}" assert ( result[0].status_extended - == f"There is not an alert for deleting public IP address rule in subscription {AZURE_SUBSCRIPTION_ID}." + == f"There is not an alert for deleting public IP address rule in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) def test_alert_rules_configured(self): monitor_client = mock.MagicMock + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -124,5 +128,5 @@ class Test_monitor_alert_create_update_security_solution: assert result[0].resource_id == "id2" assert ( result[0].status_extended - == f"There is an alert configured for deleting public IP address rule in subscription {AZURE_SUBSCRIPTION_ID}." + == f"There is an alert configured for deleting public IP address rule in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) diff --git a/tests/providers/azure/services/monitor/monitor_alert_delete_security_solution/monitor_alert_delete_security_solution_test.py b/tests/providers/azure/services/monitor/monitor_alert_delete_security_solution/monitor_alert_delete_security_solution_test.py index ba80bfff41..3c204de572 100644 --- a/tests/providers/azure/services/monitor/monitor_alert_delete_security_solution/monitor_alert_delete_security_solution_test.py +++ b/tests/providers/azure/services/monitor/monitor_alert_delete_security_solution/monitor_alert_delete_security_solution_test.py @@ -3,7 +3,9 @@ from unittest import mock from azure.mgmt.monitor.models import AlertRuleAnyOfOrLeafCondition from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_monitor_alert_create_update_security_solution: def test_monitor_alert_delete_security_solution_no_subscriptions(self): monitor_client = mock.MagicMock() + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} monitor_client.alert_rules = {} with ( mock.patch( @@ -33,7 +36,7 @@ class Test_monitor_alert_create_update_security_solution: def test_no_alert_rules(self): monitor_client = mock.MagicMock() monitor_client.alert_rules = {AZURE_SUBSCRIPTION_ID: []} - monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_ID} + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -57,11 +60,12 @@ class Test_monitor_alert_create_update_security_solution: assert result[0].resource_id == f"/subscriptions/{AZURE_SUBSCRIPTION_ID}" assert ( result[0].status_extended - == f"There is not an alert for deleting Security Solution in subscription {AZURE_SUBSCRIPTION_ID}." + == f"There is not an alert for deleting Security Solution in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) def test_alert_rules_configured(self): monitor_client = mock.MagicMock + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -124,5 +128,5 @@ class Test_monitor_alert_create_update_security_solution: assert result[0].resource_id == "id2" assert ( result[0].status_extended - == f"There is an alert configured for deleting Security Solution in subscription {AZURE_SUBSCRIPTION_ID}." + == f"There is an alert configured for deleting Security Solution in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) diff --git a/tests/providers/azure/services/monitor/monitor_alert_delete_sqlserver_fr/monitor_alert_delete_sqlserver_fr_test.py b/tests/providers/azure/services/monitor/monitor_alert_delete_sqlserver_fr/monitor_alert_delete_sqlserver_fr_test.py index 2725fafd19..1a0a63a869 100644 --- a/tests/providers/azure/services/monitor/monitor_alert_delete_sqlserver_fr/monitor_alert_delete_sqlserver_fr_test.py +++ b/tests/providers/azure/services/monitor/monitor_alert_delete_sqlserver_fr/monitor_alert_delete_sqlserver_fr_test.py @@ -3,7 +3,9 @@ from unittest import mock from azure.mgmt.monitor.models import AlertRuleAnyOfOrLeafCondition from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_monitor_alert_delete_sqlserver_fr: def test_monitor_alert_delete_sqlserver_fr_no_subscriptions(self): monitor_client = mock.MagicMock() + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} monitor_client.alert_rules = {} with ( mock.patch( @@ -33,7 +36,7 @@ class Test_monitor_alert_delete_sqlserver_fr: def test_no_alert_rules(self): monitor_client = mock.MagicMock() monitor_client.alert_rules = {AZURE_SUBSCRIPTION_ID: []} - monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_ID} + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -57,11 +60,12 @@ class Test_monitor_alert_delete_sqlserver_fr: assert result[0].resource_id == f"/subscriptions/{AZURE_SUBSCRIPTION_ID}" assert ( result[0].status_extended - == f"There is not an alert for deleting SQL Server firewall rule in subscription {AZURE_SUBSCRIPTION_ID}." + == f"There is not an alert for deleting SQL Server firewall rule in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) def test_alert_rules_configured(self): monitor_client = mock.MagicMock + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -124,5 +128,5 @@ class Test_monitor_alert_delete_sqlserver_fr: assert result[0].resource_id == "id2" assert ( result[0].status_extended - == f"There is an alert configured for deleting SQL Server firewall rule in subscription {AZURE_SUBSCRIPTION_ID}." + == f"There is an alert configured for deleting SQL Server firewall rule in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) diff --git a/tests/providers/azure/services/monitor/monitor_alert_service_health_exists/monitor_alert_service_health_exists_test.py b/tests/providers/azure/services/monitor/monitor_alert_service_health_exists/monitor_alert_service_health_exists_test.py index 3cfd80bedf..ba928af0e3 100644 --- a/tests/providers/azure/services/monitor/monitor_alert_service_health_exists/monitor_alert_service_health_exists_test.py +++ b/tests/providers/azure/services/monitor/monitor_alert_service_health_exists/monitor_alert_service_health_exists_test.py @@ -1,7 +1,9 @@ from unittest import mock from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -9,6 +11,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_monitor_alert_service_health_exists: def test_monitor_alert_service_health_exists_no_subscriptions(self): monitor_client = mock.MagicMock() + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} monitor_client.alert_rules = {} with ( mock.patch( @@ -31,7 +34,7 @@ class Test_monitor_alert_service_health_exists: def test_no_alert_rules(self): monitor_client = mock.MagicMock() monitor_client.alert_rules = {AZURE_SUBSCRIPTION_ID: []} - monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_ID} + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -55,11 +58,12 @@ class Test_monitor_alert_service_health_exists: assert result[0].resource_id == f"/subscriptions/{AZURE_SUBSCRIPTION_ID}" assert ( result[0].status_extended - == f"There is no activity log alert for Service Health in subscription {AZURE_SUBSCRIPTION_ID}." + == f"There is no activity log alert for Service Health in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) def test_alert_rules_configured(self): monitor_client = mock.MagicMock() + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -108,11 +112,12 @@ class Test_monitor_alert_service_health_exists: assert result[0].resource_id == "id1" assert ( result[0].status_extended - == f"There is an activity log alert for Service Health in subscription {AZURE_SUBSCRIPTION_ID}." + == f"There is an activity log alert for Service Health in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) def test_alert_rules_configured_but_disabled(self): monitor_client = mock.MagicMock() + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -153,7 +158,7 @@ class Test_monitor_alert_service_health_exists: ] } monitor_client.subscriptions = { - AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_ID + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME } check = monitor_alert_service_health_exists() result = check.execute() @@ -164,5 +169,5 @@ class Test_monitor_alert_service_health_exists: assert result[0].resource_id == f"/subscriptions/{AZURE_SUBSCRIPTION_ID}" assert ( result[0].status_extended - == f"There is no activity log alert for Service Health in subscription {AZURE_SUBSCRIPTION_ID}." + == f"There is no activity log alert for Service Health in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) diff --git a/tests/providers/azure/services/monitor/monitor_diagnostic_setting_with_appropriate_categories/monitor_diagnostic_setting_with_appropriate_categories_test.py b/tests/providers/azure/services/monitor/monitor_diagnostic_setting_with_appropriate_categories/monitor_diagnostic_setting_with_appropriate_categories_test.py index 56d10199bc..6c111b76f8 100644 --- a/tests/providers/azure/services/monitor/monitor_diagnostic_setting_with_appropriate_categories/monitor_diagnostic_setting_with_appropriate_categories_test.py +++ b/tests/providers/azure/services/monitor/monitor_diagnostic_setting_with_appropriate_categories/monitor_diagnostic_setting_with_appropriate_categories_test.py @@ -1,7 +1,9 @@ from unittest import mock from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ class Test_monitor_diagnostic_setting_with_appropriate_categories: self, ): monitor_client = mock.MagicMock + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} monitor_client.diagnostics_settings = {} with ( @@ -34,7 +37,7 @@ class Test_monitor_diagnostic_setting_with_appropriate_categories: def test_no_diagnostic_settings(self): monitor_client = mock.MagicMock monitor_client.diagnostics_settings = {AZURE_SUBSCRIPTION_ID: []} - monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_ID} + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -58,11 +61,12 @@ class Test_monitor_diagnostic_setting_with_appropriate_categories: assert result[0].resource_name == AZURE_SUBSCRIPTION_ID assert ( result[0].status_extended - == f"No diagnostic setting captures all appropriate categories (Administrative, Security, Alert, Policy) in subscription {AZURE_SUBSCRIPTION_ID}." + == f"No diagnostic setting captures all appropriate categories (Administrative, Security, Alert, Policy) in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) def test_diagnostic_settings_configured(self): monitor_client = mock.MagicMock + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -128,5 +132,5 @@ class Test_monitor_diagnostic_setting_with_appropriate_categories: assert result[0].resource_name == "name" assert ( result[0].status_extended - == f"Diagnostic setting name captures appropriate categories in subscription {AZURE_SUBSCRIPTION_ID}." + == f"Diagnostic setting name captures appropriate categories in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) diff --git a/tests/providers/azure/services/monitor/monitor_diagnostic_settings_exists/monitor_diagnostic_settings_exists_test.py b/tests/providers/azure/services/monitor/monitor_diagnostic_settings_exists/monitor_diagnostic_settings_exists_test.py index a4638ffac7..4bac8b5bd7 100644 --- a/tests/providers/azure/services/monitor/monitor_diagnostic_settings_exists/monitor_diagnostic_settings_exists_test.py +++ b/tests/providers/azure/services/monitor/monitor_diagnostic_settings_exists/monitor_diagnostic_settings_exists_test.py @@ -1,7 +1,9 @@ from unittest import mock from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ class Test_monitor_diagnostic_settings_exists: self, ): monitor_client = mock.MagicMock + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} monitor_client.diagnostics_settings = {} with ( @@ -34,7 +37,7 @@ class Test_monitor_diagnostic_settings_exists: def test_no_diagnostic_settings(self): monitor_client = mock.MagicMock monitor_client.diagnostics_settings = {AZURE_SUBSCRIPTION_ID: []} - monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_ID} + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -58,12 +61,14 @@ class Test_monitor_diagnostic_settings_exists: assert result[0].resource_id == f"/subscriptions/{AZURE_SUBSCRIPTION_ID}" assert ( result[0].status_extended - == f"No diagnostic settings found in subscription {AZURE_SUBSCRIPTION_ID}." + == f"No diagnostic settings found in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) def test_diagnostic_settings_configured(self): monitor_client = mock.MagicMock + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -196,5 +201,5 @@ class Test_monitor_diagnostic_settings_exists: assert result[0].resource_id == "id" assert ( result[0].status_extended - == f"Diagnostic setting name found in subscription {AZURE_SUBSCRIPTION_ID}." + == f"Diagnostic setting name found in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) diff --git a/tests/providers/azure/services/monitor/monitor_storage_account_with_activity_logs_cmk_encrypted/monitor_storage_account_with_activity_logs_cmk_encrypted_test.py b/tests/providers/azure/services/monitor/monitor_storage_account_with_activity_logs_cmk_encrypted/monitor_storage_account_with_activity_logs_cmk_encrypted_test.py index 707fd11af2..cf4dbcc54b 100644 --- a/tests/providers/azure/services/monitor/monitor_storage_account_with_activity_logs_cmk_encrypted/monitor_storage_account_with_activity_logs_cmk_encrypted_test.py +++ b/tests/providers/azure/services/monitor/monitor_storage_account_with_activity_logs_cmk_encrypted/monitor_storage_account_with_activity_logs_cmk_encrypted_test.py @@ -1,7 +1,9 @@ from unittest import mock from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ class Test_monitor_storage_account_with_activity_logs_cmk_encrypted: self, ): monitor_client = mock.MagicMock + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} monitor_client.diagnostics_settings = {} with ( @@ -33,6 +36,7 @@ class Test_monitor_storage_account_with_activity_logs_cmk_encrypted: def test_no_diagnostic_settings(self): monitor_client = mock.MagicMock + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} monitor_client.diagnostics_settings = {AZURE_SUBSCRIPTION_ID: []} with ( mock.patch( @@ -54,7 +58,9 @@ class Test_monitor_storage_account_with_activity_logs_cmk_encrypted: def test_diagnostic_settings_configured(self): monitor_client = mock.MagicMock + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -191,7 +197,7 @@ class Test_monitor_storage_account_with_activity_logs_cmk_encrypted: ) assert ( result[0].status_extended - == f"Storage account {storage_client.storage_accounts[AZURE_SUBSCRIPTION_ID][0].name} storing activity log in subscription {AZURE_SUBSCRIPTION_ID} is encrypted with Customer Managed Key or not necessary." + == f"Storage account {storage_client.storage_accounts[AZURE_SUBSCRIPTION_ID][0].name} storing activity log in subscription {AZURE_SUBSCRIPTION_DISPLAY} is encrypted with Customer Managed Key or not necessary." ) assert result[1].status == "FAIL" assert result[1].resource_name == "storageaccountname2" @@ -202,5 +208,5 @@ class Test_monitor_storage_account_with_activity_logs_cmk_encrypted: ) assert ( result[1].status_extended - == f"Storage account {storage_client.storage_accounts[AZURE_SUBSCRIPTION_ID][1].name} storing activity log in subscription {AZURE_SUBSCRIPTION_ID} is not encrypted with Customer Managed Key." + == f"Storage account {storage_client.storage_accounts[AZURE_SUBSCRIPTION_ID][1].name} storing activity log in subscription {AZURE_SUBSCRIPTION_DISPLAY} is not encrypted with Customer Managed Key." ) diff --git a/tests/providers/azure/services/monitor/monitor_storage_account_with_activity_logs_is_private/monitor_storage_account_with_activity_logs_is_private_test.py b/tests/providers/azure/services/monitor/monitor_storage_account_with_activity_logs_is_private/monitor_storage_account_with_activity_logs_is_private_test.py index debcab0321..0e69470251 100644 --- a/tests/providers/azure/services/monitor/monitor_storage_account_with_activity_logs_is_private/monitor_storage_account_with_activity_logs_is_private_test.py +++ b/tests/providers/azure/services/monitor/monitor_storage_account_with_activity_logs_is_private/monitor_storage_account_with_activity_logs_is_private_test.py @@ -1,7 +1,9 @@ from unittest import mock from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ class Test_monitor_storage_account_with_activity_logs_is_private: self, ): monitor_client = mock.MagicMock + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} monitor_client.diagnostics_settings = {} with ( @@ -33,6 +36,7 @@ class Test_monitor_storage_account_with_activity_logs_is_private: def test_no_diagnostic_settings(self): monitor_client = mock.MagicMock + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} monitor_client.diagnostics_settings = {AZURE_SUBSCRIPTION_ID: []} with ( mock.patch( @@ -54,7 +58,9 @@ class Test_monitor_storage_account_with_activity_logs_is_private: def test_diagnostic_settings_configured(self): monitor_client = mock.MagicMock + monitor_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -190,7 +196,7 @@ class Test_monitor_storage_account_with_activity_logs_is_private: assert result[0].resource_name == "storageaccountname1" assert ( result[0].status_extended - == f"Blob public access enabled in storage account {storage_client.storage_accounts[AZURE_SUBSCRIPTION_ID][0].name} storing activity logs in subscription {AZURE_SUBSCRIPTION_ID}." + == f"Blob public access enabled in storage account {storage_client.storage_accounts[AZURE_SUBSCRIPTION_ID][0].name} storing activity logs in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) assert result[1].subscription == AZURE_SUBSCRIPTION_ID assert result[1].status == "PASS" @@ -202,5 +208,5 @@ class Test_monitor_storage_account_with_activity_logs_is_private: assert result[1].resource_name == "storageaccountname2" assert ( result[1].status_extended - == f"Blob public access disabled in storage account {storage_client.storage_accounts[AZURE_SUBSCRIPTION_ID][1].name} storing activity logs in subscription {AZURE_SUBSCRIPTION_ID}." + == f"Blob public access disabled in storage account {storage_client.storage_accounts[AZURE_SUBSCRIPTION_ID][1].name} storing activity logs in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) diff --git a/tests/providers/azure/services/mysql/mysql_flexible_server_audit_log_connection_activated/mysql_flexible_server_audit_log_connection_activated_test.py b/tests/providers/azure/services/mysql/mysql_flexible_server_audit_log_connection_activated/mysql_flexible_server_audit_log_connection_activated_test.py index 47ef92551b..e6eccbbd4a 100644 --- a/tests/providers/azure/services/mysql/mysql_flexible_server_audit_log_connection_activated/mysql_flexible_server_audit_log_connection_activated_test.py +++ b/tests/providers/azure/services/mysql/mysql_flexible_server_audit_log_connection_activated/mysql_flexible_server_audit_log_connection_activated_test.py @@ -6,7 +6,9 @@ from prowler.providers.azure.services.mysql.mysql_service import ( FlexibleServer, ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -14,6 +16,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_mysql_flexible_server_audit_log_connection_activated: def test_mysql_no_subscriptions(self): mysql_client = mock.MagicMock + mysql_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} mysql_client.flexible_servers = {} with ( @@ -36,6 +39,7 @@ class Test_mysql_flexible_server_audit_log_connection_activated: def test_mysql_no_servers(self): mysql_client = mock.MagicMock + mysql_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} mysql_client.flexible_servers = {AZURE_SUBSCRIPTION_ID: {}} with ( @@ -59,6 +63,7 @@ class Test_mysql_flexible_server_audit_log_connection_activated: def test_mysql_audit_log_connection_activated_lowercase(self): server_name = str(uuid4()) mysql_client = mock.MagicMock + mysql_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} mysql_client.flexible_servers = { AZURE_SUBSCRIPTION_ID: { "/subscriptions/resource_id": FlexibleServer( @@ -104,12 +109,13 @@ class Test_mysql_flexible_server_audit_log_connection_activated: ) assert ( result[0].status_extended - == f"Audit log is enabled for server {server_name} in subscription {AZURE_SUBSCRIPTION_ID}." + == f"Audit log is enabled for server {server_name} in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) def test_mysql_audit_log_connection_not_connection(self): server_name = str(uuid4()) mysql_client = mock.MagicMock + mysql_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} mysql_client.flexible_servers = { AZURE_SUBSCRIPTION_ID: { "/subscriptions/resource_id": FlexibleServer( @@ -155,12 +161,13 @@ class Test_mysql_flexible_server_audit_log_connection_activated: ) assert ( result[0].status_extended - == f"Audit log is disabled for server {server_name} in subscription {AZURE_SUBSCRIPTION_ID}." + == f"Audit log is disabled for server {server_name} in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) def test_mysql_audit_log_connection_activated(self): server_name = str(uuid4()) mysql_client = mock.MagicMock + mysql_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} mysql_client.flexible_servers = { AZURE_SUBSCRIPTION_ID: { "/subscriptions/resource_id": FlexibleServer( @@ -206,12 +213,13 @@ class Test_mysql_flexible_server_audit_log_connection_activated: ) assert ( result[0].status_extended - == f"Audit log is enabled for server {server_name} in subscription {AZURE_SUBSCRIPTION_ID}." + == f"Audit log is enabled for server {server_name} in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) def test_mysql_audit_log_connection_activated_with_other_options(self): server_name = str(uuid4()) mysql_client = mock.MagicMock + mysql_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} mysql_client.flexible_servers = { AZURE_SUBSCRIPTION_ID: { "/subscriptions/resource_id": FlexibleServer( @@ -257,5 +265,5 @@ class Test_mysql_flexible_server_audit_log_connection_activated: ) assert ( result[0].status_extended - == f"Audit log is enabled for server {server_name} in subscription {AZURE_SUBSCRIPTION_ID}." + == f"Audit log is enabled for server {server_name} in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) diff --git a/tests/providers/azure/services/mysql/mysql_flexible_server_audit_log_enabled/mysql_flexible_server_audit_log_enabled_test.py b/tests/providers/azure/services/mysql/mysql_flexible_server_audit_log_enabled/mysql_flexible_server_audit_log_enabled_test.py index 7c32f337fd..b4bb7c9925 100644 --- a/tests/providers/azure/services/mysql/mysql_flexible_server_audit_log_enabled/mysql_flexible_server_audit_log_enabled_test.py +++ b/tests/providers/azure/services/mysql/mysql_flexible_server_audit_log_enabled/mysql_flexible_server_audit_log_enabled_test.py @@ -6,7 +6,9 @@ from prowler.providers.azure.services.mysql.mysql_service import ( FlexibleServer, ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -14,6 +16,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_mysql_flexible_server_audit_log_enabled: def test_mysql_no_subscriptions(self): mysql_client = mock.MagicMock + mysql_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} mysql_client.flexible_servers = {} with ( @@ -36,6 +39,7 @@ class Test_mysql_flexible_server_audit_log_enabled: def test_mysql_no_servers(self): mysql_client = mock.MagicMock + mysql_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} mysql_client.flexible_servers = {AZURE_SUBSCRIPTION_ID: {}} with ( @@ -59,6 +63,7 @@ class Test_mysql_flexible_server_audit_log_enabled: def test_mysql_audit_log_enabled_lowercase(self): server_name = str(uuid4()) mysql_client = mock.MagicMock + mysql_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} mysql_client.flexible_servers = { AZURE_SUBSCRIPTION_ID: { "/subscriptions/resource_id": FlexibleServer( @@ -104,12 +109,13 @@ class Test_mysql_flexible_server_audit_log_enabled: ) assert ( result[0].status_extended - == f"Audit log is enabled for server {server_name} in subscription {AZURE_SUBSCRIPTION_ID}." + == f"Audit log is enabled for server {server_name} in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) def test_mysql_audit_log_disabled(self): server_name = str(uuid4()) mysql_client = mock.MagicMock + mysql_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} mysql_client.flexible_servers = { AZURE_SUBSCRIPTION_ID: { "/subscriptions/resource_id": FlexibleServer( @@ -155,12 +161,13 @@ class Test_mysql_flexible_server_audit_log_enabled: ) assert ( result[0].status_extended - == f"Audit log is disabled for server {server_name} in subscription {AZURE_SUBSCRIPTION_ID}." + == f"Audit log is disabled for server {server_name} in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) def test_mysql_audit_log_enabled(self): server_name = str(uuid4()) mysql_client = mock.MagicMock + mysql_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} mysql_client.flexible_servers = { AZURE_SUBSCRIPTION_ID: { "/subscriptions/resource_id": FlexibleServer( @@ -206,5 +213,5 @@ class Test_mysql_flexible_server_audit_log_enabled: ) assert ( result[0].status_extended - == f"Audit log is enabled for server {server_name} in subscription {AZURE_SUBSCRIPTION_ID}." + == f"Audit log is enabled for server {server_name} in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) diff --git a/tests/providers/azure/services/mysql/mysql_flexible_server_minimum_tls_version_12/mysql_flexible_server_minimum_tls_version_12_test.py b/tests/providers/azure/services/mysql/mysql_flexible_server_minimum_tls_version_12/mysql_flexible_server_minimum_tls_version_12_test.py index 8d277dad77..d8571f61e9 100644 --- a/tests/providers/azure/services/mysql/mysql_flexible_server_minimum_tls_version_12/mysql_flexible_server_minimum_tls_version_12_test.py +++ b/tests/providers/azure/services/mysql/mysql_flexible_server_minimum_tls_version_12/mysql_flexible_server_minimum_tls_version_12_test.py @@ -6,7 +6,9 @@ from prowler.providers.azure.services.mysql.mysql_service import ( FlexibleServer, ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -14,6 +16,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_mysql_flexible_server_minimum_tls_version_12: def test_mysql_no_subscriptions(self): mysql_client = mock.MagicMock + mysql_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} mysql_client.flexible_servers = {} with ( @@ -36,6 +39,7 @@ class Test_mysql_flexible_server_minimum_tls_version_12: def test_mysql_no_servers(self): mysql_client = mock.MagicMock + mysql_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} mysql_client.flexible_servers = {AZURE_SUBSCRIPTION_ID: {}} with ( @@ -59,6 +63,7 @@ class Test_mysql_flexible_server_minimum_tls_version_12: def test_mysql_no_tls_configuration(self): server_name = str(uuid4()) mysql_client = mock.MagicMock + mysql_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} mysql_client.flexible_servers = { AZURE_SUBSCRIPTION_ID: { "/subscriptions/resource_id": FlexibleServer( @@ -94,12 +99,13 @@ class Test_mysql_flexible_server_minimum_tls_version_12: assert result[0].location == "location" assert ( result[0].status_extended - == f"TLS version is not configured in server {server_name} in subscription {AZURE_SUBSCRIPTION_ID}." + == f"TLS version is not configured in server {server_name} in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) def test_mysql_flexible_server_minimum_tls_version_12(self): server_name = str(uuid4()) mysql_client = mock.MagicMock + mysql_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} mysql_client.flexible_servers = { AZURE_SUBSCRIPTION_ID: { "/subscriptions/resource_id": FlexibleServer( @@ -144,12 +150,13 @@ class Test_mysql_flexible_server_minimum_tls_version_12: ) assert ( result[0].status_extended - == f"TLS version is TLSv1.2 in server {server_name} in subscription {AZURE_SUBSCRIPTION_ID}. This version of TLS is considered secure." + == f"TLS version is TLSv1.2 in server {server_name} in subscription {AZURE_SUBSCRIPTION_DISPLAY}. This version of TLS is considered secure." ) def test_mysql_tls_version_is_1_3(self): server_name = str(uuid4()) mysql_client = mock.MagicMock + mysql_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} mysql_client.flexible_servers = { AZURE_SUBSCRIPTION_ID: { "/subscriptions/resource_id": FlexibleServer( @@ -194,12 +201,13 @@ class Test_mysql_flexible_server_minimum_tls_version_12: ) assert ( result[0].status_extended - == f"TLS version is TLSv1.3 in server {server_name} in subscription {AZURE_SUBSCRIPTION_ID}. This version of TLS is considered secure." + == f"TLS version is TLSv1.3 in server {server_name} in subscription {AZURE_SUBSCRIPTION_DISPLAY}. This version of TLS is considered secure." ) def test_mysql_tls_version_is_not_1_2(self): server_name = str(uuid4()) mysql_client = mock.MagicMock + mysql_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} mysql_client.flexible_servers = { AZURE_SUBSCRIPTION_ID: { "/subscriptions/resource_id": FlexibleServer( @@ -244,12 +252,13 @@ class Test_mysql_flexible_server_minimum_tls_version_12: ) assert ( result[0].status_extended - == f"TLS version is TLSv1.1 in server {server_name} in subscription {AZURE_SUBSCRIPTION_ID}. There is at leat one version of TLS that is considered insecure." + == f"TLS version is TLSv1.1 in server {server_name} in subscription {AZURE_SUBSCRIPTION_DISPLAY}. There is at leat one version of TLS that is considered insecure." ) def test_mysql_tls_version_is_1_1_and_1_3(self): server_name = str(uuid4()) mysql_client = mock.MagicMock + mysql_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} mysql_client.flexible_servers = { AZURE_SUBSCRIPTION_ID: { "/subscriptions/resource_id": FlexibleServer( @@ -294,5 +303,5 @@ class Test_mysql_flexible_server_minimum_tls_version_12: ) assert ( result[0].status_extended - == f"TLS version is TLSv1.1,TLSv1.3 in server {server_name} in subscription {AZURE_SUBSCRIPTION_ID}. There is at leat one version of TLS that is considered insecure." + == f"TLS version is TLSv1.1,TLSv1.3 in server {server_name} in subscription {AZURE_SUBSCRIPTION_DISPLAY}. There is at leat one version of TLS that is considered insecure." ) diff --git a/tests/providers/azure/services/mysql/mysql_flexible_server_ssl_connection_enabled/mysql_flexible_server_ssl_connection_enabled_test.py b/tests/providers/azure/services/mysql/mysql_flexible_server_ssl_connection_enabled/mysql_flexible_server_ssl_connection_enabled_test.py index 2b87a28d8f..0c521458f9 100644 --- a/tests/providers/azure/services/mysql/mysql_flexible_server_ssl_connection_enabled/mysql_flexible_server_ssl_connection_enabled_test.py +++ b/tests/providers/azure/services/mysql/mysql_flexible_server_ssl_connection_enabled/mysql_flexible_server_ssl_connection_enabled_test.py @@ -6,7 +6,9 @@ from prowler.providers.azure.services.mysql.mysql_service import ( FlexibleServer, ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -14,6 +16,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_mysql_flexible_server_ssl_connection_enabled: def test_mysql_no_subscriptions(self): mysql_client = mock.MagicMock + mysql_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} mysql_client.flexible_servers = {} with ( @@ -36,6 +39,7 @@ class Test_mysql_flexible_server_ssl_connection_enabled: def test_mysql_no_servers(self): mysql_client = mock.MagicMock + mysql_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} mysql_client.flexible_servers = {AZURE_SUBSCRIPTION_ID: {}} with ( @@ -59,6 +63,7 @@ class Test_mysql_flexible_server_ssl_connection_enabled: def test_mysql_connection_enabled(self): server_name = str(uuid4()) mysql_client = mock.MagicMock + mysql_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} mysql_client.flexible_servers = { AZURE_SUBSCRIPTION_ID: { "/subscriptions/resource_id": FlexibleServer( @@ -104,12 +109,13 @@ class Test_mysql_flexible_server_ssl_connection_enabled: ) assert ( result[0].status_extended - == f"SSL connection is enabled for server {server_name} in subscription {AZURE_SUBSCRIPTION_ID}." + == f"SSL connection is enabled for server {server_name} in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) def test_mysql_connection_enabled_lowercase(self): server_name = str(uuid4()) mysql_client = mock.MagicMock + mysql_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} mysql_client.flexible_servers = { AZURE_SUBSCRIPTION_ID: { "/subscriptions/resource_id": FlexibleServer( @@ -155,12 +161,13 @@ class Test_mysql_flexible_server_ssl_connection_enabled: ) assert ( result[0].status_extended - == f"SSL connection is enabled for server {server_name} in subscription {AZURE_SUBSCRIPTION_ID}." + == f"SSL connection is enabled for server {server_name} in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) def test_mysql_ssl_connection_disabled(self): server_name = str(uuid4()) mysql_client = mock.MagicMock + mysql_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} mysql_client.flexible_servers = { AZURE_SUBSCRIPTION_ID: { "/subscriptions/resource_id": FlexibleServer( @@ -206,12 +213,13 @@ class Test_mysql_flexible_server_ssl_connection_enabled: ) assert ( result[0].status_extended - == f"SSL connection is disabled for server {server_name} in subscription {AZURE_SUBSCRIPTION_ID}." + == f"SSL connection is disabled for server {server_name} in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) def test_mysql_ssl_connection_no_configuration(self): server_name = str(uuid4()) mysql_client = mock.MagicMock + mysql_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} mysql_client.flexible_servers = { AZURE_SUBSCRIPTION_ID: { "/subscriptions/resource_id": FlexibleServer( @@ -248,13 +256,14 @@ class Test_mysql_flexible_server_ssl_connection_enabled: assert result[0].location == "location" assert ( result[0].status_extended - == f"SSL connection is disabled for server {server_name} in subscription {AZURE_SUBSCRIPTION_ID}." + == f"SSL connection is disabled for server {server_name} in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) def test_mysql_ssl_connection_enabled_and_disabled(self): server_name_1 = str(uuid4()) server_name_2 = str(uuid4()) mysql_client = mock.MagicMock + mysql_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} mysql_client.flexible_servers = { AZURE_SUBSCRIPTION_ID: { "/subscriptions/resource_id1": FlexibleServer( @@ -313,7 +322,7 @@ class Test_mysql_flexible_server_ssl_connection_enabled: ) assert ( result[0].status_extended - == f"SSL connection is enabled for server {server_name_1} in subscription {AZURE_SUBSCRIPTION_ID}." + == f"SSL connection is enabled for server {server_name_1} in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) assert result[1].status == "FAIL" assert result[1].subscription == AZURE_SUBSCRIPTION_ID @@ -325,5 +334,5 @@ class Test_mysql_flexible_server_ssl_connection_enabled: ) assert ( result[1].status_extended - == f"SSL connection is disabled for server {server_name_2} in subscription {AZURE_SUBSCRIPTION_ID}." + == f"SSL connection is disabled for server {server_name_2} in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) diff --git a/tests/providers/azure/services/network/network_bastion_host_exists/network_bastion_host_exists_test.py b/tests/providers/azure/services/network/network_bastion_host_exists/network_bastion_host_exists_test.py index 4d5d1b49f1..0a5a2c7d46 100644 --- a/tests/providers/azure/services/network/network_bastion_host_exists/network_bastion_host_exists_test.py +++ b/tests/providers/azure/services/network/network_bastion_host_exists/network_bastion_host_exists_test.py @@ -3,7 +3,9 @@ from uuid import uuid4 from prowler.providers.azure.services.network.network_service import BastionHost from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -12,7 +14,7 @@ class Test_network_bastion_host_exists: def test_no_bastion_hosts(self): network_client = mock.MagicMock network_client.bastion_hosts = {AZURE_SUBSCRIPTION_ID: []} - network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_ID} + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( @@ -38,7 +40,7 @@ class Test_network_bastion_host_exists: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Bastion Host from subscription {AZURE_SUBSCRIPTION_ID} does not exist" + == f"Bastion Host from subscription {AZURE_SUBSCRIPTION_DISPLAY} does not exist" ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == AZURE_SUBSCRIPTION_ID @@ -46,6 +48,7 @@ class Test_network_bastion_host_exists: def test_network_bastion_host_exists(self): network_client = mock.MagicMock + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} bastion_host_name = "Bastion Host Name" bastion_host_id = str(uuid4()) @@ -83,7 +86,7 @@ class Test_network_bastion_host_exists: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Bastion Host {bastion_host_name} exists in subscription {AZURE_SUBSCRIPTION_ID}." + == f"Bastion Host {bastion_host_name} exists in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == bastion_host_name diff --git a/tests/providers/azure/services/network/network_flow_log_captured_sent/network_flow_log_captured_sent_test.py b/tests/providers/azure/services/network/network_flow_log_captured_sent/network_flow_log_captured_sent_test.py index 4965783b39..cbd29e1d33 100644 --- a/tests/providers/azure/services/network/network_flow_log_captured_sent/network_flow_log_captured_sent_test.py +++ b/tests/providers/azure/services/network/network_flow_log_captured_sent/network_flow_log_captured_sent_test.py @@ -6,15 +6,25 @@ from prowler.providers.azure.services.network.network_service import ( NetworkWatcher, RetentionPolicy, ) -from tests.providers.azure.azure_fixtures import AZURE_SUBSCRIPTION_ID +from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, + AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, + set_mocked_azure_provider, +) class Test_network_flow_log_captured_sent: def test_no_network_watchers(self): network_client = mock.MagicMock + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} network_client.network_watchers = {} with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), mock.patch( "prowler.providers.azure.services.network.network_service.Network", new=network_client, @@ -34,6 +44,7 @@ class Test_network_flow_log_captured_sent: def test_network_network_watchers_no_flow_logs(self): network_client = mock.MagicMock + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} network_watcher_name = "Network Watcher Name" network_watcher_id = str(uuid4()) @@ -49,6 +60,10 @@ class Test_network_flow_log_captured_sent: } with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), mock.patch( "prowler.providers.azure.services.network.network_service.Network", new=network_client, @@ -68,7 +83,7 @@ class Test_network_flow_log_captured_sent: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Network Watcher {network_watcher_name} from subscription {AZURE_SUBSCRIPTION_ID} has no flow logs" + == f"Network Watcher {network_watcher_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has no flow logs" ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == network_watcher_name @@ -77,6 +92,7 @@ class Test_network_flow_log_captured_sent: def test_network_network_watchers_flow_logs_disabled(self): network_client = mock.MagicMock + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} network_watcher_name = "Network Watcher Name" network_watcher_id = str(uuid4()) @@ -100,6 +116,10 @@ class Test_network_flow_log_captured_sent: } with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), mock.patch( "prowler.providers.azure.services.network.network_service.Network", new=network_client, @@ -119,7 +139,7 @@ class Test_network_flow_log_captured_sent: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Network Watcher {network_watcher_name} from subscription {AZURE_SUBSCRIPTION_ID} has flow logs disabled" + == f"Network Watcher {network_watcher_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has flow logs disabled" ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == network_watcher_name @@ -128,6 +148,7 @@ class Test_network_flow_log_captured_sent: def test_network_network_watchers_flow_logs_well_configured(self): network_client = mock.MagicMock + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} network_watcher_name = "Network Watcher Name" network_watcher_id = str(uuid4()) @@ -151,6 +172,10 @@ class Test_network_flow_log_captured_sent: } with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), mock.patch( "prowler.providers.azure.services.network.network_service.Network", new=network_client, @@ -174,11 +199,12 @@ class Test_network_flow_log_captured_sent: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Network Watcher {network_watcher_name} from subscription {AZURE_SUBSCRIPTION_ID} has enabled flow logs that are not configured to send traffic analytics to a Log Analytics workspace" + == f"Network Watcher {network_watcher_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has enabled flow logs that are not configured to send traffic analytics to a Log Analytics workspace" ) def test_network_network_watchers_traffic_analytics_without_workspace(self): network_client = mock.MagicMock + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} network_watcher_name = "Network Watcher Name" network_watcher_id = str(uuid4()) @@ -204,6 +230,10 @@ class Test_network_flow_log_captured_sent: } with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), mock.patch( "prowler.providers.azure.services.network.network_service.Network", new=network_client, @@ -223,11 +253,12 @@ class Test_network_flow_log_captured_sent: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Network Watcher {network_watcher_name} from subscription {AZURE_SUBSCRIPTION_ID} has enabled flow logs that are not configured to send traffic analytics to a Log Analytics workspace" + == f"Network Watcher {network_watcher_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has enabled flow logs that are not configured to send traffic analytics to a Log Analytics workspace" ) def test_network_network_watchers_mixed_flow_logs_fails(self): network_client = mock.MagicMock + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} network_watcher_name = "Network Watcher Name" network_watcher_id = str(uuid4()) @@ -262,6 +293,10 @@ class Test_network_flow_log_captured_sent: } with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), mock.patch( "prowler.providers.azure.services.network.network_service.Network", new=network_client, @@ -281,11 +316,12 @@ class Test_network_flow_log_captured_sent: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Network Watcher {network_watcher_name} from subscription {AZURE_SUBSCRIPTION_ID} has enabled flow logs that are not configured to send traffic analytics to a Log Analytics workspace" + == f"Network Watcher {network_watcher_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has enabled flow logs that are not configured to send traffic analytics to a Log Analytics workspace" ) def test_network_network_watchers_vnet_flow_logs_well_configured(self): network_client = mock.MagicMock + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} network_watcher_name = "Network Watcher Name" network_watcher_id = str(uuid4()) @@ -311,6 +347,10 @@ class Test_network_flow_log_captured_sent: } with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), mock.patch( "prowler.providers.azure.services.network.network_service.Network", new=network_client, @@ -331,7 +371,7 @@ class Test_network_flow_log_captured_sent: assert result[0].location == "location" assert ( result[0].status_extended - == f"Network Watcher {network_watcher_name} from subscription {AZURE_SUBSCRIPTION_ID} has flow logs that are captured and sent to Log Analytics workspace" + == f"Network Watcher {network_watcher_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has flow logs that are captured and sent to Log Analytics workspace" ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == network_watcher_name diff --git a/tests/providers/azure/services/network/network_flow_log_more_than_90_days/network_flow_log_more_than_90_days_test.py b/tests/providers/azure/services/network/network_flow_log_more_than_90_days/network_flow_log_more_than_90_days_test.py index a813bb3462..771a3e2809 100644 --- a/tests/providers/azure/services/network/network_flow_log_more_than_90_days/network_flow_log_more_than_90_days_test.py +++ b/tests/providers/azure/services/network/network_flow_log_more_than_90_days/network_flow_log_more_than_90_days_test.py @@ -7,7 +7,9 @@ from prowler.providers.azure.services.network.network_service import ( RetentionPolicy, ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -15,6 +17,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_network_flow_log_more_than_90_days: def test_no_network_watchers(self): network_client = mock.MagicMock + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} network_client.network_watchers = {} with ( @@ -41,6 +44,7 @@ class Test_network_flow_log_more_than_90_days: def test_network_network_watchers_no_flow_logs(self): network_client = mock.MagicMock + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} network_watcher_name = "Network Watcher Name" network_watcher_id = str(uuid4()) @@ -79,7 +83,7 @@ class Test_network_flow_log_more_than_90_days: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Network Watcher {network_watcher_name} from subscription {AZURE_SUBSCRIPTION_ID} has no flow logs" + == f"Network Watcher {network_watcher_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has no flow logs" ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == network_watcher_name @@ -88,6 +92,7 @@ class Test_network_flow_log_more_than_90_days: def test_network_network_watchers_flow_logs_disabled(self): network_client = mock.MagicMock + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} network_watcher_name = "Network Watcher Name" network_watcher_id = str(uuid4()) @@ -134,7 +139,7 @@ class Test_network_flow_log_more_than_90_days: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Network Watcher {network_watcher_name} from subscription {AZURE_SUBSCRIPTION_ID} has flow logs disabled" + == f"Network Watcher {network_watcher_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has flow logs disabled" ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == network_watcher_name @@ -143,6 +148,7 @@ class Test_network_flow_log_more_than_90_days: def test_network_network_watchers_flow_logs_retention_days_80(self): network_client = mock.MagicMock + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} network_watcher_name = "Network Watcher Name" network_watcher_id = str(uuid4()) @@ -189,7 +195,7 @@ class Test_network_flow_log_more_than_90_days: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Network Watcher {network_watcher_name} from subscription {AZURE_SUBSCRIPTION_ID} flow logs retention policy is less than 90 days" + == f"Network Watcher {network_watcher_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} flow logs retention policy is less than 90 days" ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == network_watcher_name @@ -198,6 +204,7 @@ class Test_network_flow_log_more_than_90_days: def test_network_network_watchers_flow_logs_retention_days_0(self): network_client = mock.MagicMock + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} network_watcher_name = "Network Watcher Name" network_watcher_id = str(uuid4()) @@ -244,7 +251,7 @@ class Test_network_flow_log_more_than_90_days: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Network Watcher {network_watcher_name} from subscription {AZURE_SUBSCRIPTION_ID} has flow logs enabled for more than 90 days" + == f"Network Watcher {network_watcher_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has flow logs enabled for more than 90 days" ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == network_watcher_name @@ -253,6 +260,7 @@ class Test_network_flow_log_more_than_90_days: def test_network_network_watchers_flow_logs_well_configured(self): network_client = mock.MagicMock + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} network_watcher_name = "Network Watcher Name" network_watcher_id = str(uuid4()) @@ -299,7 +307,7 @@ class Test_network_flow_log_more_than_90_days: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Network Watcher {network_watcher_name} from subscription {AZURE_SUBSCRIPTION_ID} has flow logs enabled for more than 90 days" + == f"Network Watcher {network_watcher_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has flow logs enabled for more than 90 days" ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == network_watcher_name diff --git a/tests/providers/azure/services/network/network_http_internet_access_restricted/network_http_internet_access_restricted_test.py b/tests/providers/azure/services/network/network_http_internet_access_restricted/network_http_internet_access_restricted_test.py index 9b8959777e..cd598f2f17 100644 --- a/tests/providers/azure/services/network/network_http_internet_access_restricted/network_http_internet_access_restricted_test.py +++ b/tests/providers/azure/services/network/network_http_internet_access_restricted/network_http_internet_access_restricted_test.py @@ -5,7 +5,9 @@ from azure.mgmt.network.models import SecurityRule from prowler.providers.azure.services.network.network_service import SecurityGroup from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -13,6 +15,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_network_http_internet_access_restricted: def test_no_security_groups(self): network_client = mock.MagicMock + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} network_client.security_groups = {} with ( @@ -39,6 +42,7 @@ class Test_network_http_internet_access_restricted: def test_network_security_groups_none_destination_port_range(self): network_client = mock.MagicMock + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} security_group_name = "Security Group Name" security_group_id = str(uuid4()) @@ -85,7 +89,7 @@ class Test_network_http_internet_access_restricted: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Security Group {security_group_name} from subscription {AZURE_SUBSCRIPTION_ID} has HTTP internet access restricted." + == f"Security Group {security_group_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has HTTP internet access restricted." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == security_group_name @@ -94,6 +98,7 @@ class Test_network_http_internet_access_restricted: def test_network_security_groups_invalid_security_rules(self): network_client = mock.MagicMock + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} security_group_name = "Security Group Name" security_group_id = str(uuid4()) @@ -140,7 +145,7 @@ class Test_network_http_internet_access_restricted: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Security Group {security_group_name} from subscription {AZURE_SUBSCRIPTION_ID} has HTTP internet access allowed." + == f"Security Group {security_group_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has HTTP internet access allowed." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == security_group_name @@ -149,6 +154,7 @@ class Test_network_http_internet_access_restricted: def test_network_security_groups_invalid_security_rules_range(self): network_client = mock.MagicMock + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} security_group_name = "Security Group Name" security_group_id = str(uuid4()) @@ -195,7 +201,7 @@ class Test_network_http_internet_access_restricted: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Security Group {security_group_name} from subscription {AZURE_SUBSCRIPTION_ID} has HTTP internet access allowed." + == f"Security Group {security_group_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has HTTP internet access allowed." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == security_group_name @@ -204,6 +210,7 @@ class Test_network_http_internet_access_restricted: def test_network_security_groups_valid_security_rules(self): network_client = mock.MagicMock + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} security_group_name = "Security Group Name" security_group_id = str(uuid4()) @@ -250,7 +257,7 @@ class Test_network_http_internet_access_restricted: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Security Group {security_group_name} from subscription {AZURE_SUBSCRIPTION_ID} has HTTP internet access restricted." + == f"Security Group {security_group_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has HTTP internet access restricted." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == security_group_name diff --git a/tests/providers/azure/services/network/network_public_ip_shodan/network_public_ip_shodan_test.py b/tests/providers/azure/services/network/network_public_ip_shodan/network_public_ip_shodan_test.py index a2d7f3753a..ab12877620 100644 --- a/tests/providers/azure/services/network/network_public_ip_shodan/network_public_ip_shodan_test.py +++ b/tests/providers/azure/services/network/network_public_ip_shodan/network_public_ip_shodan_test.py @@ -2,7 +2,9 @@ from unittest import mock from prowler.providers.azure.services.network.network_service import PublicIp from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -10,6 +12,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_network_public_ip_shodan: def test_no_public_ip_addresses(self): network_client = mock.MagicMock + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} network_client.public_ip_addresses = {} with ( @@ -38,6 +41,7 @@ class Test_network_public_ip_shodan: def test_network_ip_in_shodan(self): network_client = mock.MagicMock + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} public_ip_id = "id" public_ip_name = "name" ip_address = "ip_address" @@ -87,7 +91,7 @@ class Test_network_public_ip_shodan: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Public IP {ip_address} listed in Shodan with open ports {str(shodan_info['ports'])} and ISP {shodan_info['isp']} in {shodan_info['country_name']}. More info at https://www.shodan.io/host/{ip_address}." + == f"Public IP {ip_address} from subscription {AZURE_SUBSCRIPTION_DISPLAY} listed in Shodan with open ports {str(shodan_info['ports'])} and ISP {shodan_info['isp']} in {shodan_info['country_name']}. More info at https://www.shodan.io/host/{ip_address}." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == public_ip_name diff --git a/tests/providers/azure/services/network/network_rdp_internet_access_restricted/network_rdp_internet_access_restricted_test.py b/tests/providers/azure/services/network/network_rdp_internet_access_restricted/network_rdp_internet_access_restricted_test.py index 9f8c9b145a..3f75cfe051 100644 --- a/tests/providers/azure/services/network/network_rdp_internet_access_restricted/network_rdp_internet_access_restricted_test.py +++ b/tests/providers/azure/services/network/network_rdp_internet_access_restricted/network_rdp_internet_access_restricted_test.py @@ -5,7 +5,9 @@ from azure.mgmt.network.models import SecurityRule from prowler.providers.azure.services.network.network_service import SecurityGroup from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -13,6 +15,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_network_rdp_internet_access_restricted: def test_no_security_groups(self): network_client = mock.MagicMock + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} network_client.security_groups = {} with ( @@ -39,6 +42,7 @@ class Test_network_rdp_internet_access_restricted: def test_network_security_groups_none_destination_port_range(self): network_client = mock.MagicMock + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} security_group_name = "Security Group Name" security_group_id = str(uuid4()) @@ -85,7 +89,7 @@ class Test_network_rdp_internet_access_restricted: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Security Group {security_group_name} from subscription {AZURE_SUBSCRIPTION_ID} has HTTP internet access restricted." + == f"Security Group {security_group_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has HTTP internet access restricted." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == security_group_name @@ -94,6 +98,7 @@ class Test_network_rdp_internet_access_restricted: def test_network_security_groups_no_security_rules(self): network_client = mock.MagicMock + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} security_group_name = "Security Group Name" security_group_id = str(uuid4()) @@ -132,7 +137,7 @@ class Test_network_rdp_internet_access_restricted: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Security Group {security_group_name} from subscription {AZURE_SUBSCRIPTION_ID} has RDP internet access restricted." + == f"Security Group {security_group_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has RDP internet access restricted." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == security_group_name @@ -141,6 +146,7 @@ class Test_network_rdp_internet_access_restricted: def test_network_security_groups_valid_security_rules(self): network_client = mock.MagicMock + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} security_group_name = "Security Group Name" security_group_id = str(uuid4()) @@ -187,7 +193,7 @@ class Test_network_rdp_internet_access_restricted: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Security Group {security_group_name} from subscription {AZURE_SUBSCRIPTION_ID} has RDP internet access restricted." + == f"Security Group {security_group_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has RDP internet access restricted." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == security_group_name @@ -196,6 +202,7 @@ class Test_network_rdp_internet_access_restricted: def test_network_security_groups_invalid_security_rules_range(self): network_client = mock.MagicMock + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} security_group_name = "Security Group Name" security_group_id = str(uuid4()) @@ -242,7 +249,7 @@ class Test_network_rdp_internet_access_restricted: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Security Group {security_group_name} from subscription {AZURE_SUBSCRIPTION_ID} has RDP internet access allowed." + == f"Security Group {security_group_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has RDP internet access allowed." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == security_group_name diff --git a/tests/providers/azure/services/network/network_ssh_internet_access_restricted/network_ssh_internet_access_restricted_test.py b/tests/providers/azure/services/network/network_ssh_internet_access_restricted/network_ssh_internet_access_restricted_test.py index 4472055075..f2112d72de 100644 --- a/tests/providers/azure/services/network/network_ssh_internet_access_restricted/network_ssh_internet_access_restricted_test.py +++ b/tests/providers/azure/services/network/network_ssh_internet_access_restricted/network_ssh_internet_access_restricted_test.py @@ -5,7 +5,9 @@ from azure.mgmt.network.models import SecurityRule from prowler.providers.azure.services.network.network_service import SecurityGroup from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -13,6 +15,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_network_ssh_internet_access_restricted: def test_no_security_groups(self): network_client = mock.MagicMock + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} network_client.security_groups = {} with ( @@ -39,6 +42,7 @@ class Test_network_ssh_internet_access_restricted: def test_network_security_groups_none_destination_port_range(self): network_client = mock.MagicMock + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} security_group_name = "Security Group Name" security_group_id = str(uuid4()) @@ -85,7 +89,7 @@ class Test_network_ssh_internet_access_restricted: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Security Group {security_group_name} from subscription {AZURE_SUBSCRIPTION_ID} has HTTP internet access restricted." + == f"Security Group {security_group_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has HTTP internet access restricted." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == security_group_name @@ -94,6 +98,7 @@ class Test_network_ssh_internet_access_restricted: def test_network_security_groups_no_security_rules(self): network_client = mock.MagicMock + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} security_group_name = "Security Group Name" security_group_id = str(uuid4()) @@ -132,7 +137,7 @@ class Test_network_ssh_internet_access_restricted: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Security Group {security_group_name} from subscription {AZURE_SUBSCRIPTION_ID} has SSH internet access restricted." + == f"Security Group {security_group_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has SSH internet access restricted." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == security_group_name @@ -141,6 +146,7 @@ class Test_network_ssh_internet_access_restricted: def test_network_security_groups_invalid_security_rules(self): network_client = mock.MagicMock + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} security_group_name = "Security Group Name" security_group_id = str(uuid4()) @@ -187,7 +193,7 @@ class Test_network_ssh_internet_access_restricted: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Security Group {security_group_name} from subscription {AZURE_SUBSCRIPTION_ID} has SSH internet access allowed." + == f"Security Group {security_group_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has SSH internet access allowed." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == security_group_name @@ -196,6 +202,7 @@ class Test_network_ssh_internet_access_restricted: def test_network_security_groups_invalid_security_rules_range(self): network_client = mock.MagicMock + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} security_group_name = "Security Group Name" security_group_id = str(uuid4()) @@ -242,7 +249,7 @@ class Test_network_ssh_internet_access_restricted: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Security Group {security_group_name} from subscription {AZURE_SUBSCRIPTION_ID} has SSH internet access allowed." + == f"Security Group {security_group_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has SSH internet access allowed." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == security_group_name @@ -251,6 +258,7 @@ class Test_network_ssh_internet_access_restricted: def test_network_security_groups_valid_security_rules(self): network_client = mock.MagicMock + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} security_group_name = "Security Group Name" security_group_id = str(uuid4()) @@ -297,7 +305,7 @@ class Test_network_ssh_internet_access_restricted: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Security Group {security_group_name} from subscription {AZURE_SUBSCRIPTION_ID} has SSH internet access restricted." + == f"Security Group {security_group_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has SSH internet access restricted." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == security_group_name diff --git a/tests/providers/azure/services/network/network_udp_internet_access_restricted/network_udp_internet_access_restricted_test.py b/tests/providers/azure/services/network/network_udp_internet_access_restricted/network_udp_internet_access_restricted_test.py index 7d519df326..18fd523657 100644 --- a/tests/providers/azure/services/network/network_udp_internet_access_restricted/network_udp_internet_access_restricted_test.py +++ b/tests/providers/azure/services/network/network_udp_internet_access_restricted/network_udp_internet_access_restricted_test.py @@ -5,7 +5,9 @@ from azure.mgmt.network.models import SecurityRule from prowler.providers.azure.services.network.network_service import SecurityGroup from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -13,6 +15,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_network_udp_internet_access_restricted: def test_no_security_groups(self): network_client = mock.MagicMock + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} network_client.security_groups = {} with ( @@ -39,6 +42,7 @@ class Test_network_udp_internet_access_restricted: def test_network_security_groups_none_source_address_prefix(self): network_client = mock.MagicMock + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} security_group_name = "Security Group Name" security_group_id = str(uuid4()) @@ -85,7 +89,7 @@ class Test_network_udp_internet_access_restricted: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Security Group {security_group_name} from subscription {AZURE_SUBSCRIPTION_ID} has HTTP internet access restricted." + == f"Security Group {security_group_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has HTTP internet access restricted." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == security_group_name @@ -94,6 +98,7 @@ class Test_network_udp_internet_access_restricted: def test_network_security_groups_no_security_rules(self): network_client = mock.MagicMock + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} security_group_name = "Security Group Name" security_group_id = str(uuid4()) @@ -132,7 +137,7 @@ class Test_network_udp_internet_access_restricted: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Security Group {security_group_name} from subscription {AZURE_SUBSCRIPTION_ID} has UDP internet access restricted." + == f"Security Group {security_group_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has UDP internet access restricted." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == security_group_name @@ -141,6 +146,7 @@ class Test_network_udp_internet_access_restricted: def test_network_security_groups_invalid_security_rules(self): network_client = mock.MagicMock + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} security_group_name = "Security Group Name" security_group_id = str(uuid4()) @@ -186,7 +192,7 @@ class Test_network_udp_internet_access_restricted: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Security Group {security_group_name} from subscription {AZURE_SUBSCRIPTION_ID} has UDP internet access allowed." + == f"Security Group {security_group_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has UDP internet access allowed." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == security_group_name @@ -195,6 +201,7 @@ class Test_network_udp_internet_access_restricted: def test_network_security_groups_valid_security_rules(self): network_client = mock.MagicMock + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} security_group_name = "Security Group Name" security_group_id = str(uuid4()) @@ -240,7 +247,7 @@ class Test_network_udp_internet_access_restricted: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Security Group {security_group_name} from subscription {AZURE_SUBSCRIPTION_ID} has UDP internet access restricted." + == f"Security Group {security_group_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has UDP internet access restricted." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == security_group_name diff --git a/tests/providers/azure/services/network/network_watcher_enabled/network_watcher_enabled_test.py b/tests/providers/azure/services/network/network_watcher_enabled/network_watcher_enabled_test.py index aca77ea13e..f309a21e15 100644 --- a/tests/providers/azure/services/network/network_watcher_enabled/network_watcher_enabled_test.py +++ b/tests/providers/azure/services/network/network_watcher_enabled/network_watcher_enabled_test.py @@ -2,6 +2,7 @@ from unittest import mock from prowler.providers.azure.services.network.network_service import NetworkWatcher from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, @@ -11,6 +12,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_network_watcher_enabled: def test_no_network_watchers(self): network_client = mock.MagicMock + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} locations = [] network_client.locations = {AZURE_SUBSCRIPTION_ID: locations} network_client.security_groups = {} @@ -41,13 +43,13 @@ class Test_network_watcher_enabled: def test_network_invalid_network_watchers(self): network_client = mock.MagicMock locations = ["location"] - network_client.locations = {AZURE_SUBSCRIPTION_NAME: locations} - network_client.subscriptions = {AZURE_SUBSCRIPTION_NAME: AZURE_SUBSCRIPTION_ID} + network_client.locations = {AZURE_SUBSCRIPTION_ID: locations} + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} network_watcher_name = "Network Watcher" network_watcher_id = f"/subscriptions/{AZURE_SUBSCRIPTION_ID}/resourceGroups/NetworkWatcherRG/providers/Microsoft.Network/networkWatchers/NetworkWatcher_*" network_client.network_watchers = { - AZURE_SUBSCRIPTION_NAME: [ + AZURE_SUBSCRIPTION_ID: [ NetworkWatcher( id=network_watcher_id, name=network_watcher_name, @@ -81,23 +83,23 @@ class Test_network_watcher_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Network Watcher is not enabled for the following locations in subscription '{AZURE_SUBSCRIPTION_NAME}': location." + == f"Network Watcher is not enabled for the following locations in subscription '{AZURE_SUBSCRIPTION_DISPLAY}': location." ) - assert result[0].subscription == AZURE_SUBSCRIPTION_NAME - assert result[0].resource_name == AZURE_SUBSCRIPTION_NAME + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == AZURE_SUBSCRIPTION_ID assert result[0].resource_id == f"/subscriptions/{AZURE_SUBSCRIPTION_ID}" assert result[0].location == "global" def test_network_valid_network_watchers(self): network_client = mock.MagicMock locations = ["location"] - network_client.locations = {AZURE_SUBSCRIPTION_NAME: locations} - network_client.subscriptions = {AZURE_SUBSCRIPTION_NAME: AZURE_SUBSCRIPTION_ID} + network_client.locations = {AZURE_SUBSCRIPTION_ID: locations} + network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} network_watcher_name = "Network Watcher" network_watcher_id = f"/subscriptions/{AZURE_SUBSCRIPTION_ID}/resourceGroups/NetworkWatcherRG/providers/Microsoft.Network/networkWatchers/NetworkWatcher_*" network_client.network_watchers = { - AZURE_SUBSCRIPTION_NAME: [ + AZURE_SUBSCRIPTION_ID: [ NetworkWatcher( id=network_watcher_id, name=network_watcher_name, @@ -131,8 +133,8 @@ class Test_network_watcher_enabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Network Watcher {network_watcher_name} is enabled in location location in subscription '{AZURE_SUBSCRIPTION_NAME}'." + == f"Network Watcher {network_watcher_name} is enabled in location location in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'." ) - assert result[0].subscription == AZURE_SUBSCRIPTION_NAME + assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == network_watcher_name assert result[0].resource_id == network_watcher_id diff --git a/tests/providers/azure/services/policy/policy_ensure_asc_enforcement_enabled/policy_ensure_asc_enforcement_enabled_test.py b/tests/providers/azure/services/policy/policy_ensure_asc_enforcement_enabled/policy_ensure_asc_enforcement_enabled_test.py index f61a25b7d8..b763e7cb79 100644 --- a/tests/providers/azure/services/policy/policy_ensure_asc_enforcement_enabled/policy_ensure_asc_enforcement_enabled_test.py +++ b/tests/providers/azure/services/policy/policy_ensure_asc_enforcement_enabled/policy_ensure_asc_enforcement_enabled_test.py @@ -3,7 +3,9 @@ from uuid import uuid4 from prowler.providers.azure.services.policy.policy_service import PolicyAssigment from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_policy_ensure_asc_enforcement_enabled: def test_policy_no_subscriptions(self): policy_client = mock.MagicMock + policy_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} policy_client.policy_assigments = {} with ( @@ -33,6 +36,7 @@ class Test_policy_ensure_asc_enforcement_enabled: def test_policy_subscription_empty(self): policy_client = mock.MagicMock + policy_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} policy_client.policy_assigments = {AZURE_SUBSCRIPTION_ID: {}} with ( @@ -55,6 +59,7 @@ class Test_policy_ensure_asc_enforcement_enabled: def test_policy_subscription_no_asc(self): policy_client = mock.MagicMock + policy_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} resource_id = uuid4() policy_client.policy_assigments = { AZURE_SUBSCRIPTION_ID: { @@ -84,6 +89,7 @@ class Test_policy_ensure_asc_enforcement_enabled: def test_policy_subscription_asc_default(self): policy_client = mock.MagicMock + policy_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} resource_id = str(uuid4()) policy_client.policy_assigments = { AZURE_SUBSCRIPTION_ID: { @@ -115,7 +121,7 @@ class Test_policy_ensure_asc_enforcement_enabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Policy assigment '{resource_id}' is configured with enforcement mode 'Default'." + == f"Policy assigment '{resource_id}' from subscription {AZURE_SUBSCRIPTION_DISPLAY} is configured with enforcement mode 'Default'." ) assert result[0].resource_id == resource_id assert result[0].resource_name == "SecurityCenterBuiltIn" @@ -123,6 +129,7 @@ class Test_policy_ensure_asc_enforcement_enabled: def test_policy_subscription_asc_not_default(self): policy_client = mock.MagicMock + policy_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} resource_id = str(uuid4()) policy_client.policy_assigments = { AZURE_SUBSCRIPTION_ID: { @@ -154,7 +161,7 @@ class Test_policy_ensure_asc_enforcement_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Policy assigment '{resource_id}' is not configured with enforcement mode Default." + == f"Policy assigment '{resource_id}' from subscription {AZURE_SUBSCRIPTION_DISPLAY} is not configured with enforcement mode Default." ) assert result[0].resource_id == resource_id assert result[0].resource_name == "SecurityCenterBuiltIn" diff --git a/tests/providers/azure/services/postgresql/postgresql_flexible_server_allow_access_services_disabled/postgresql_flexible_server_allow_access_services_disabled_test.py b/tests/providers/azure/services/postgresql/postgresql_flexible_server_allow_access_services_disabled/postgresql_flexible_server_allow_access_services_disabled_test.py index 3f21f8a200..9d1afcdbba 100644 --- a/tests/providers/azure/services/postgresql/postgresql_flexible_server_allow_access_services_disabled/postgresql_flexible_server_allow_access_services_disabled_test.py +++ b/tests/providers/azure/services/postgresql/postgresql_flexible_server_allow_access_services_disabled/postgresql_flexible_server_allow_access_services_disabled_test.py @@ -6,7 +6,9 @@ from prowler.providers.azure.services.postgresql.postgresql_service import ( Server, ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -14,6 +16,9 @@ from tests.providers.azure.azure_fixtures import ( class Test_postgresql_flexible_server_allow_access_services_disabled: def test_no_postgresql_flexible_servers(self): postgresql_client = mock.MagicMock + postgresql_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } postgresql_client.flexible_servers = {} with ( @@ -36,6 +41,9 @@ class Test_postgresql_flexible_server_allow_access_services_disabled: def test_flexible_servers_allow_public_access(self): postgresql_client = mock.MagicMock + postgresql_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } postgresql_server_name = "Postgres Flexible Server Name" postgresql_server_id = str(uuid4()) firewall = Firewall( @@ -84,7 +92,7 @@ class Test_postgresql_flexible_server_allow_access_services_disabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Flexible Postgresql server {postgresql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has allow public access from any Azure service enabled" + == f"Flexible Postgresql server {postgresql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has allow public access from any Azure service enabled" ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == postgresql_server_name @@ -93,6 +101,9 @@ class Test_postgresql_flexible_server_allow_access_services_disabled: def test_flexible_servers_dont_allow_public_access(self): postgresql_client = mock.MagicMock + postgresql_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } postgresql_server_name = "Postgres Flexible Server Name" postgresql_server_id = str(uuid4()) firewall = Firewall( @@ -141,7 +152,7 @@ class Test_postgresql_flexible_server_allow_access_services_disabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Flexible Postgresql server {postgresql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has allow public access from any Azure service disabled" + == f"Flexible Postgresql server {postgresql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has allow public access from any Azure service disabled" ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == postgresql_server_name diff --git a/tests/providers/azure/services/postgresql/postgresql_flexible_server_connection_throttling_on/postgresql_flexible_server_connection_throttling_on_test.py b/tests/providers/azure/services/postgresql/postgresql_flexible_server_connection_throttling_on/postgresql_flexible_server_connection_throttling_on_test.py index 8a0d65d27d..f027dc44a6 100644 --- a/tests/providers/azure/services/postgresql/postgresql_flexible_server_connection_throttling_on/postgresql_flexible_server_connection_throttling_on_test.py +++ b/tests/providers/azure/services/postgresql/postgresql_flexible_server_connection_throttling_on/postgresql_flexible_server_connection_throttling_on_test.py @@ -3,7 +3,9 @@ from uuid import uuid4 from prowler.providers.azure.services.postgresql.postgresql_service import Server from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,9 @@ from tests.providers.azure.azure_fixtures import ( class Test_postgresql_flexible_server_connection_throttling_on: def test_no_postgresql_flexible_servers(self): postgresql_client = mock.MagicMock + postgresql_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } postgresql_client.flexible_servers = {} with ( @@ -33,6 +38,9 @@ class Test_postgresql_flexible_server_connection_throttling_on: def test_flexible_servers_connection_throttling_off(self): postgresql_client = mock.MagicMock + postgresql_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } postgresql_server_name = "Postgres Flexible Server Name" postgresql_server_id = str(uuid4()) postgresql_client.flexible_servers = { @@ -75,7 +83,7 @@ class Test_postgresql_flexible_server_connection_throttling_on: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Flexible Postgresql server {postgresql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has connection_throttling disabled" + == f"Flexible Postgresql server {postgresql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has connection_throttling disabled" ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == postgresql_server_name @@ -84,6 +92,9 @@ class Test_postgresql_flexible_server_connection_throttling_on: def test_flexible_servers_connection_throttling_on(self): postgresql_client = mock.MagicMock + postgresql_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } postgresql_server_name = "Postgres Flexible Server Name" postgresql_server_id = str(uuid4()) postgresql_client.flexible_servers = { @@ -126,7 +137,7 @@ class Test_postgresql_flexible_server_connection_throttling_on: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Flexible Postgresql server {postgresql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has connection_throttling enabled" + == f"Flexible Postgresql server {postgresql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has connection_throttling enabled" ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == postgresql_server_name diff --git a/tests/providers/azure/services/postgresql/postgresql_flexible_server_enforce_ssl_enabled/postgresql_flexible_server_enforce_ssl_enabled_test.py b/tests/providers/azure/services/postgresql/postgresql_flexible_server_enforce_ssl_enabled/postgresql_flexible_server_enforce_ssl_enabled_test.py index abe971b89d..55ac9c6f3d 100644 --- a/tests/providers/azure/services/postgresql/postgresql_flexible_server_enforce_ssl_enabled/postgresql_flexible_server_enforce_ssl_enabled_test.py +++ b/tests/providers/azure/services/postgresql/postgresql_flexible_server_enforce_ssl_enabled/postgresql_flexible_server_enforce_ssl_enabled_test.py @@ -3,7 +3,9 @@ from uuid import uuid4 from prowler.providers.azure.services.postgresql.postgresql_service import Server from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,9 @@ from tests.providers.azure.azure_fixtures import ( class Test_postgresql_flexible_server_enforce_ssl_enabled: def test_no_postgresql_flexible_servers(self): postgresql_client = mock.MagicMock + postgresql_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } postgresql_client.flexible_servers = {} with ( @@ -33,6 +38,9 @@ class Test_postgresql_flexible_server_enforce_ssl_enabled: def test_flexible_servers_require_secure_transport_off(self): postgresql_client = mock.MagicMock + postgresql_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } postgresql_server_name = "Postgres Flexible Server Name" postgresql_server_id = str(uuid4()) postgresql_client.flexible_servers = { @@ -75,7 +83,7 @@ class Test_postgresql_flexible_server_enforce_ssl_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Flexible Postgresql server {postgresql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has enforce ssl disabled" + == f"Flexible Postgresql server {postgresql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has enforce ssl disabled" ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == postgresql_server_name @@ -84,6 +92,9 @@ class Test_postgresql_flexible_server_enforce_ssl_enabled: def test_flexible_servers_require_secure_transport_on(self): postgresql_client = mock.MagicMock + postgresql_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } postgresql_server_name = "Postgres Flexible Server Name" postgresql_server_id = str(uuid4()) postgresql_client.flexible_servers = { @@ -126,7 +137,7 @@ class Test_postgresql_flexible_server_enforce_ssl_enabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Flexible Postgresql server {postgresql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has enforce ssl enabled" + == f"Flexible Postgresql server {postgresql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has enforce ssl enabled" ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == postgresql_server_name diff --git a/tests/providers/azure/services/postgresql/postgresql_flexible_server_entra_id_authentication_enabled/postgresql_flexible_server_entra_id_authentication_enabled_test.py b/tests/providers/azure/services/postgresql/postgresql_flexible_server_entra_id_authentication_enabled/postgresql_flexible_server_entra_id_authentication_enabled_test.py index 6ee413b15b..4785799245 100644 --- a/tests/providers/azure/services/postgresql/postgresql_flexible_server_entra_id_authentication_enabled/postgresql_flexible_server_entra_id_authentication_enabled_test.py +++ b/tests/providers/azure/services/postgresql/postgresql_flexible_server_entra_id_authentication_enabled/postgresql_flexible_server_entra_id_authentication_enabled_test.py @@ -6,7 +6,9 @@ from prowler.providers.azure.services.postgresql.postgresql_service import ( Server, ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -14,6 +16,9 @@ from tests.providers.azure.azure_fixtures import ( class Test_postgresql_flexible_server_entra_id_authentication_enabled: def test_no_postgresql_flexible_servers(self): postgresql_client = mock.MagicMock + postgresql_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } postgresql_client.flexible_servers = {} with ( @@ -36,6 +41,9 @@ class Test_postgresql_flexible_server_entra_id_authentication_enabled: def test_flexible_servers_entra_id_auth_disabled(self): postgresql_client = mock.MagicMock + postgresql_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } postgresql_server_name = "Postgres Flexible Server Name" postgresql_server_id = str(uuid4()) postgresql_client.flexible_servers = { @@ -78,7 +86,7 @@ class Test_postgresql_flexible_server_entra_id_authentication_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Flexible Postgresql server {postgresql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has Microsoft Entra ID authentication disabled" + == f"Flexible Postgresql server {postgresql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has Microsoft Entra ID authentication disabled" ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == postgresql_server_name @@ -87,6 +95,9 @@ class Test_postgresql_flexible_server_entra_id_authentication_enabled: def test_flexible_servers_entra_id_auth_enabled_no_admins(self): postgresql_client = mock.MagicMock + postgresql_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } postgresql_server_name = "Postgres Flexible Server Name" postgresql_server_id = str(uuid4()) postgresql_client.flexible_servers = { @@ -129,7 +140,7 @@ class Test_postgresql_flexible_server_entra_id_authentication_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Flexible Postgresql server {postgresql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has Microsoft Entra ID authentication enabled but no Entra ID administrators configured" + == f"Flexible Postgresql server {postgresql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has Microsoft Entra ID authentication enabled but no Entra ID administrators configured" ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == postgresql_server_name @@ -138,6 +149,9 @@ class Test_postgresql_flexible_server_entra_id_authentication_enabled: def test_flexible_servers_entra_id_auth_enabled(self): postgresql_client = mock.MagicMock + postgresql_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } postgresql_server_name = "Postgres Flexible Server Name" postgresql_server_id = str(uuid4()) postgresql_client.flexible_servers = { @@ -187,7 +201,7 @@ class Test_postgresql_flexible_server_entra_id_authentication_enabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Flexible Postgresql server {postgresql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has Microsoft Entra ID authentication enabled with 1 administrator configured" + == f"Flexible Postgresql server {postgresql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has Microsoft Entra ID authentication enabled with 1 administrator configured" ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == postgresql_server_name diff --git a/tests/providers/azure/services/postgresql/postgresql_flexible_server_log_checkpoints_on/postgresql_flexible_server_log_checkpoints_on_test.py b/tests/providers/azure/services/postgresql/postgresql_flexible_server_log_checkpoints_on/postgresql_flexible_server_log_checkpoints_on_test.py index 13644730b3..ee4bcb346d 100644 --- a/tests/providers/azure/services/postgresql/postgresql_flexible_server_log_checkpoints_on/postgresql_flexible_server_log_checkpoints_on_test.py +++ b/tests/providers/azure/services/postgresql/postgresql_flexible_server_log_checkpoints_on/postgresql_flexible_server_log_checkpoints_on_test.py @@ -3,7 +3,9 @@ from uuid import uuid4 from prowler.providers.azure.services.postgresql.postgresql_service import Server from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,9 @@ from tests.providers.azure.azure_fixtures import ( class Test_postgresql_flexible_server_log_checkpoints_on: def test_no_postgresql_flexible_servers(self): postgresql_client = mock.MagicMock + postgresql_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } postgresql_client.flexible_servers = {} with ( @@ -33,6 +38,9 @@ class Test_postgresql_flexible_server_log_checkpoints_on: def test_flexible_servers_log_checkpoints_off(self): postgresql_client = mock.MagicMock + postgresql_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } postgresql_server_name = "Postgres Flexible Server Name" postgresql_server_id = str(uuid4()) postgresql_client.flexible_servers = { @@ -75,7 +83,7 @@ class Test_postgresql_flexible_server_log_checkpoints_on: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Flexible Postgresql server {postgresql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has log_checkpoints disabled" + == f"Flexible Postgresql server {postgresql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has log_checkpoints disabled" ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == postgresql_server_name @@ -84,6 +92,9 @@ class Test_postgresql_flexible_server_log_checkpoints_on: def test_flexible_servers_log_checkpoints_on(self): postgresql_client = mock.MagicMock + postgresql_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } postgresql_server_name = "Postgres Flexible Server Name" postgresql_server_id = str(uuid4()) postgresql_client.flexible_servers = { @@ -126,7 +137,7 @@ class Test_postgresql_flexible_server_log_checkpoints_on: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Flexible Postgresql server {postgresql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has log_checkpoints enabled" + == f"Flexible Postgresql server {postgresql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has log_checkpoints enabled" ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == postgresql_server_name diff --git a/tests/providers/azure/services/postgresql/postgresql_flexible_server_log_connections_on/postgresql_flexible_server_log_connections_on_test.py b/tests/providers/azure/services/postgresql/postgresql_flexible_server_log_connections_on/postgresql_flexible_server_log_connections_on_test.py index 0377cb172f..d48f12b53a 100644 --- a/tests/providers/azure/services/postgresql/postgresql_flexible_server_log_connections_on/postgresql_flexible_server_log_connections_on_test.py +++ b/tests/providers/azure/services/postgresql/postgresql_flexible_server_log_connections_on/postgresql_flexible_server_log_connections_on_test.py @@ -3,7 +3,9 @@ from uuid import uuid4 from prowler.providers.azure.services.postgresql.postgresql_service import Server from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,9 @@ from tests.providers.azure.azure_fixtures import ( class Test_postgresql_flexible_server_log_connections_on: def test_no_postgresql_flexible_servers(self): postgresql_client = mock.MagicMock + postgresql_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } postgresql_client.flexible_servers = {} with ( @@ -33,6 +38,9 @@ class Test_postgresql_flexible_server_log_connections_on: def test_flexible_servers_log_connections_off(self): postgresql_client = mock.MagicMock + postgresql_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } postgresql_server_name = "Postgres Flexible Server Name" postgresql_server_id = str(uuid4()) postgresql_client.flexible_servers = { @@ -75,7 +83,7 @@ class Test_postgresql_flexible_server_log_connections_on: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Flexible Postgresql server {postgresql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has log_connections disabled" + == f"Flexible Postgresql server {postgresql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has log_connections disabled" ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == postgresql_server_name @@ -84,6 +92,9 @@ class Test_postgresql_flexible_server_log_connections_on: def test_flexible_servers_log_connections_on(self): postgresql_client = mock.MagicMock + postgresql_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } postgresql_server_name = "Postgres Flexible Server Name" postgresql_server_id = str(uuid4()) postgresql_client.flexible_servers = { @@ -126,7 +137,7 @@ class Test_postgresql_flexible_server_log_connections_on: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Flexible Postgresql server {postgresql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has log_connections enabled" + == f"Flexible Postgresql server {postgresql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has log_connections enabled" ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == postgresql_server_name diff --git a/tests/providers/azure/services/postgresql/postgresql_flexible_server_log_disconnections_on/postgresql_flexible_server_log_disconnections_on_test.py b/tests/providers/azure/services/postgresql/postgresql_flexible_server_log_disconnections_on/postgresql_flexible_server_log_disconnections_on_test.py index 91f80e53d0..f860723dfc 100644 --- a/tests/providers/azure/services/postgresql/postgresql_flexible_server_log_disconnections_on/postgresql_flexible_server_log_disconnections_on_test.py +++ b/tests/providers/azure/services/postgresql/postgresql_flexible_server_log_disconnections_on/postgresql_flexible_server_log_disconnections_on_test.py @@ -3,7 +3,9 @@ from uuid import uuid4 from prowler.providers.azure.services.postgresql.postgresql_service import Server from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,9 @@ from tests.providers.azure.azure_fixtures import ( class Test_postgresql_flexible_server_log_disconnections_on: def test_no_postgresql_flexible_servers(self): postgresql_client = mock.MagicMock + postgresql_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } postgresql_client.flexible_servers = {} with ( @@ -33,6 +38,9 @@ class Test_postgresql_flexible_server_log_disconnections_on: def test_flexible_servers_log_connections_off(self): postgresql_client = mock.MagicMock + postgresql_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } postgresql_server_name = "Postgres Flexible Server Name" postgresql_server_id = str(uuid4()) postgresql_client.flexible_servers = { @@ -75,7 +83,7 @@ class Test_postgresql_flexible_server_log_disconnections_on: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Flexible Postgresql server {postgresql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has log_disconnections disabled" + == f"Flexible Postgresql server {postgresql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has log_disconnections disabled" ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == postgresql_server_name @@ -84,6 +92,9 @@ class Test_postgresql_flexible_server_log_disconnections_on: def test_flexible_servers_log_connections_on(self): postgresql_client = mock.MagicMock + postgresql_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } postgresql_server_name = "Postgres Flexible Server Name" postgresql_server_id = str(uuid4()) postgresql_client.flexible_servers = { @@ -126,7 +137,7 @@ class Test_postgresql_flexible_server_log_disconnections_on: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Flexible Postgresql server {postgresql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has log_disconnections enabled" + == f"Flexible Postgresql server {postgresql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has log_disconnections enabled" ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == postgresql_server_name diff --git a/tests/providers/azure/services/postgresql/postgresql_flexible_server_log_retention_days_greater_3/postgresql_flexible_server_log_retention_days_greater_3_test.py b/tests/providers/azure/services/postgresql/postgresql_flexible_server_log_retention_days_greater_3/postgresql_flexible_server_log_retention_days_greater_3_test.py index 005969eb4a..046b1f9062 100644 --- a/tests/providers/azure/services/postgresql/postgresql_flexible_server_log_retention_days_greater_3/postgresql_flexible_server_log_retention_days_greater_3_test.py +++ b/tests/providers/azure/services/postgresql/postgresql_flexible_server_log_retention_days_greater_3/postgresql_flexible_server_log_retention_days_greater_3_test.py @@ -3,7 +3,9 @@ from uuid import uuid4 from prowler.providers.azure.services.postgresql.postgresql_service import Server from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,9 @@ from tests.providers.azure.azure_fixtures import ( class Test_postgresql_flexible_server_log_retention_days_greater_3: def test_no_postgresql_flexible_servers(self): postgresql_client = mock.MagicMock + postgresql_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } postgresql_client.flexible_servers = {} with ( @@ -33,6 +38,9 @@ class Test_postgresql_flexible_server_log_retention_days_greater_3: def test_flexible_servers_no_log_retention_days(self): postgresql_client = mock.MagicMock + postgresql_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } postgresql_server_name = "Postgres Flexible Server Name" postgresql_server_id = str(uuid4()) postgresql_client.flexible_servers = { @@ -75,7 +83,7 @@ class Test_postgresql_flexible_server_log_retention_days_greater_3: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Flexible Postgresql server {postgresql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has log_retention disabled" + == f"Flexible Postgresql server {postgresql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has log_retention disabled" ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == postgresql_server_name @@ -84,6 +92,9 @@ class Test_postgresql_flexible_server_log_retention_days_greater_3: def test_flexible_servers_log_retention_days_3(self): postgresql_client = mock.MagicMock + postgresql_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } postgresql_server_name = "Postgres Flexible Server Name" postgresql_server_id = str(uuid4()) log_retention_days = "3" @@ -127,7 +138,7 @@ class Test_postgresql_flexible_server_log_retention_days_greater_3: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Flexible Postgresql server {postgresql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has log_retention set to {log_retention_days}" + == f"Flexible Postgresql server {postgresql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has log_retention set to {log_retention_days}" ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == postgresql_server_name @@ -136,6 +147,9 @@ class Test_postgresql_flexible_server_log_retention_days_greater_3: def test_flexible_servers_log_retention_days_4(self): postgresql_client = mock.MagicMock + postgresql_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } postgresql_server_name = "Postgres Flexible Server Name" postgresql_server_id = str(uuid4()) log_retention_days = "4" @@ -179,7 +193,7 @@ class Test_postgresql_flexible_server_log_retention_days_greater_3: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Flexible Postgresql server {postgresql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has log_retention set to {log_retention_days}" + == f"Flexible Postgresql server {postgresql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has log_retention set to {log_retention_days}" ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == postgresql_server_name @@ -188,6 +202,9 @@ class Test_postgresql_flexible_server_log_retention_days_greater_3: def test_flexible_servers_log_retention_days_8(self): postgresql_client = mock.MagicMock + postgresql_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } postgresql_server_name = "Postgres Flexible Server Name" postgresql_server_id = str(uuid4()) log_retention_days = "8" @@ -231,7 +248,7 @@ class Test_postgresql_flexible_server_log_retention_days_greater_3: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Flexible Postgresql server {postgresql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has log_retention set to {log_retention_days}" + == f"Flexible Postgresql server {postgresql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has log_retention set to {log_retention_days}" ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == postgresql_server_name diff --git a/tests/providers/azure/services/sqlserver/sqlserver_auditing_enabled/sqlserver_auditing_enabled_test.py b/tests/providers/azure/services/sqlserver/sqlserver_auditing_enabled/sqlserver_auditing_enabled_test.py index e11294a778..e8152ab260 100644 --- a/tests/providers/azure/services/sqlserver/sqlserver_auditing_enabled/sqlserver_auditing_enabled_test.py +++ b/tests/providers/azure/services/sqlserver/sqlserver_auditing_enabled/sqlserver_auditing_enabled_test.py @@ -9,7 +9,9 @@ from azure.mgmt.sql.models import ( from prowler.providers.azure.services.sqlserver.sqlserver_service import Server from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -17,6 +19,9 @@ from tests.providers.azure.azure_fixtures import ( class Test_sqlserver_auditing_enabled: def test_no_sql_servers(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sqlserver_client.sql_servers = {} with ( @@ -39,6 +44,9 @@ class Test_sqlserver_auditing_enabled: def test_sql_servers_auditing_disabled(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) sqlserver_client.sql_servers = { @@ -76,7 +84,7 @@ class Test_sqlserver_auditing_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} does not have any auditing policy configured." + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} does not have any auditing policy configured." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == sql_server_name @@ -85,6 +93,9 @@ class Test_sqlserver_auditing_enabled: def test_sql_servers_auditing_enabled(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) sqlserver_client.sql_servers = { @@ -122,7 +133,7 @@ class Test_sqlserver_auditing_enabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has an auditing policy configured." + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has an auditing policy configured." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == sql_server_name diff --git a/tests/providers/azure/services/sqlserver/sqlserver_auditing_retention_90_days/sqlserver_auditing_retention_90_days_test.py b/tests/providers/azure/services/sqlserver/sqlserver_auditing_retention_90_days/sqlserver_auditing_retention_90_days_test.py index d74632d357..fe3b8e9d3e 100644 --- a/tests/providers/azure/services/sqlserver/sqlserver_auditing_retention_90_days/sqlserver_auditing_retention_90_days_test.py +++ b/tests/providers/azure/services/sqlserver/sqlserver_auditing_retention_90_days/sqlserver_auditing_retention_90_days_test.py @@ -5,7 +5,9 @@ from azure.mgmt.sql.models import ServerBlobAuditingPolicy from prowler.providers.azure.services.sqlserver.sqlserver_service import Server from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -13,6 +15,9 @@ from tests.providers.azure.azure_fixtures import ( class Test_sqlserver_auditing_retention_90_days: def test_no_sql_servers(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sqlserver_client.sql_servers = {} with ( @@ -35,6 +40,9 @@ class Test_sqlserver_auditing_retention_90_days: def test_sql_servers_auditing_policy_disabled(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) sqlserver_client.sql_servers = { @@ -74,7 +82,7 @@ class Test_sqlserver_auditing_retention_90_days: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has auditing disabled." + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has auditing disabled." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == sql_server_name @@ -83,6 +91,9 @@ class Test_sqlserver_auditing_retention_90_days: def test_sql_servers_auditing_retention_less_than_90_days(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) sqlserver_client.sql_servers = { @@ -124,7 +135,7 @@ class Test_sqlserver_auditing_retention_90_days: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has auditing retention less than 91 days." + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has auditing retention less than 91 days." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == sql_server_name @@ -133,6 +144,9 @@ class Test_sqlserver_auditing_retention_90_days: def test_sql_servers_auditing_retention_greater_than_90_days(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) sqlserver_client.sql_servers = { @@ -174,7 +188,7 @@ class Test_sqlserver_auditing_retention_90_days: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has auditing retention greater than 90 days." + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has auditing retention greater than 90 days." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == sql_server_name @@ -185,6 +199,9 @@ class Test_sqlserver_auditing_retention_90_days: self, ): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) sqlserver_client.sql_servers = { @@ -227,7 +244,7 @@ class Test_sqlserver_auditing_retention_90_days: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has auditing retention greater than 90 days." + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has auditing retention greater than 90 days." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == sql_server_name @@ -238,6 +255,9 @@ class Test_sqlserver_auditing_retention_90_days: self, ): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) sqlserver_client.sql_servers = { @@ -280,7 +300,7 @@ class Test_sqlserver_auditing_retention_90_days: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has auditing retention less than 91 days." + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has auditing retention less than 91 days." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == sql_server_name diff --git a/tests/providers/azure/services/sqlserver/sqlserver_azuread_administrator_enabled/sqlserver_azuread_administrator_enabled_test.py b/tests/providers/azure/services/sqlserver/sqlserver_azuread_administrator_enabled/sqlserver_azuread_administrator_enabled_test.py index 823455d385..7699a7a9ee 100644 --- a/tests/providers/azure/services/sqlserver/sqlserver_azuread_administrator_enabled/sqlserver_azuread_administrator_enabled_test.py +++ b/tests/providers/azure/services/sqlserver/sqlserver_azuread_administrator_enabled/sqlserver_azuread_administrator_enabled_test.py @@ -5,7 +5,9 @@ from azure.mgmt.sql.models import ServerExternalAdministrator from prowler.providers.azure.services.sqlserver.sqlserver_service import Server from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -13,6 +15,9 @@ from tests.providers.azure.azure_fixtures import ( class Test_sqlserver_azuread_administrator_enabled: def test_no_sql_servers(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sqlserver_client.sql_servers = {} with ( @@ -35,6 +40,9 @@ class Test_sqlserver_azuread_administrator_enabled: def test_sql_servers_azuread_no_administrator(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) sqlserver_client.sql_servers = { @@ -72,7 +80,7 @@ class Test_sqlserver_azuread_administrator_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} does not have an Active Directory administrator." + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} does not have an Active Directory administrator." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == sql_server_name @@ -81,6 +89,9 @@ class Test_sqlserver_azuread_administrator_enabled: def test_sql_servers_azuread_administrator_no_active_directory(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) sqlserver_client.sql_servers = { @@ -120,7 +131,7 @@ class Test_sqlserver_azuread_administrator_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} does not have an Active Directory administrator." + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} does not have an Active Directory administrator." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == sql_server_name @@ -129,6 +140,9 @@ class Test_sqlserver_azuread_administrator_enabled: def test_sql_servers_azuread_administrator_active_directory(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) sqlserver_client.sql_servers = { @@ -168,7 +182,7 @@ class Test_sqlserver_azuread_administrator_enabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has an Active Directory administrator." + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has an Active Directory administrator." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == sql_server_name diff --git a/tests/providers/azure/services/sqlserver/sqlserver_microsoft_defender_enabled/sqlserver_microsoft_defender_enabled_test.py b/tests/providers/azure/services/sqlserver/sqlserver_microsoft_defender_enabled/sqlserver_microsoft_defender_enabled_test.py index 41bf400be6..73474a51da 100644 --- a/tests/providers/azure/services/sqlserver/sqlserver_microsoft_defender_enabled/sqlserver_microsoft_defender_enabled_test.py +++ b/tests/providers/azure/services/sqlserver/sqlserver_microsoft_defender_enabled/sqlserver_microsoft_defender_enabled_test.py @@ -5,7 +5,9 @@ from azure.mgmt.sql.models import ServerSecurityAlertPolicy from prowler.providers.azure.services.sqlserver.sqlserver_service import Server from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -13,6 +15,9 @@ from tests.providers.azure.azure_fixtures import ( class Test_sqlserver_microsoft_defender_enabled: def test_no_sql_servers(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sqlserver_client.sql_servers = {} with ( @@ -35,6 +40,9 @@ class Test_sqlserver_microsoft_defender_enabled: def test_sql_servers_no_security_alert_policies(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) sqlserver_client.sql_servers = { @@ -73,6 +81,9 @@ class Test_sqlserver_microsoft_defender_enabled: def test_sql_servers_microsoft_defender_disabled(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) sqlserver_client.sql_servers = { @@ -111,7 +122,7 @@ class Test_sqlserver_microsoft_defender_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has microsoft defender disabled." + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has microsoft defender disabled." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == sql_server_name @@ -120,6 +131,9 @@ class Test_sqlserver_microsoft_defender_enabled: def test_sql_servers_microsoft_defender_enabled(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) sqlserver_client.sql_servers = { @@ -158,7 +172,7 @@ class Test_sqlserver_microsoft_defender_enabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has microsoft defender enabled." + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has microsoft defender enabled." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == sql_server_name diff --git a/tests/providers/azure/services/sqlserver/sqlserver_recommended_minimal_tls_version/sqlserver_recommended_minimal_tls_version_test.py b/tests/providers/azure/services/sqlserver/sqlserver_recommended_minimal_tls_version/sqlserver_recommended_minimal_tls_version_test.py index 0c6a7649b5..df7a4d6eb0 100644 --- a/tests/providers/azure/services/sqlserver/sqlserver_recommended_minimal_tls_version/sqlserver_recommended_minimal_tls_version_test.py +++ b/tests/providers/azure/services/sqlserver/sqlserver_recommended_minimal_tls_version/sqlserver_recommended_minimal_tls_version_test.py @@ -8,7 +8,9 @@ from prowler.providers.azure.services.sqlserver.sqlserver_service import ( Server, ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -16,6 +18,9 @@ from tests.providers.azure.azure_fixtures import ( class Test_sqlserver_recommended_minimal_tls_version: def test_no_sql_servers(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sqlserver_client.sql_servers = {} with ( @@ -42,6 +47,9 @@ class Test_sqlserver_recommended_minimal_tls_version: def test_sql_servers_deprecated_minimal_tls_version(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) database_name = "Database Name" @@ -95,7 +103,7 @@ class Test_sqlserver_recommended_minimal_tls_version: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} is using TLS version 1.0 as minimal accepted which is not recommended. Please use one of the recommended versions: {', '.join(sqlserver_client.audit_config['recommended_minimal_tls_versions'])}." + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} is using TLS version 1.0 as minimal accepted which is not recommended. Please use one of the recommended versions: {', '.join(sqlserver_client.audit_config['recommended_minimal_tls_versions'])}." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == sql_server_name @@ -104,6 +112,9 @@ class Test_sqlserver_recommended_minimal_tls_version: def test_sql_servers_no_minimal_tls_version(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) database_name = "Database Name" @@ -157,7 +168,7 @@ class Test_sqlserver_recommended_minimal_tls_version: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} is using TLS version as minimal accepted which is not recommended. Please use one of the recommended versions: {', '.join(sqlserver_client.audit_config['recommended_minimal_tls_versions'])}." + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} is using TLS version as minimal accepted which is not recommended. Please use one of the recommended versions: {', '.join(sqlserver_client.audit_config['recommended_minimal_tls_versions'])}." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == sql_server_name @@ -166,6 +177,9 @@ class Test_sqlserver_recommended_minimal_tls_version: def test_sql_servers_minimal_tls_version(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) database_name = "Database Name" @@ -219,7 +233,7 @@ class Test_sqlserver_recommended_minimal_tls_version: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} is using version 1.2 as minimal accepted which is recommended." + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} is using version 1.2 as minimal accepted which is recommended." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == sql_server_name diff --git a/tests/providers/azure/services/sqlserver/sqlserver_tde_encrypted_with_cmk/sqlserver_tde_encrypted_with_cmk_test.py b/tests/providers/azure/services/sqlserver/sqlserver_tde_encrypted_with_cmk/sqlserver_tde_encrypted_with_cmk_test.py index 73c9046940..4dcdffc0e7 100644 --- a/tests/providers/azure/services/sqlserver/sqlserver_tde_encrypted_with_cmk/sqlserver_tde_encrypted_with_cmk_test.py +++ b/tests/providers/azure/services/sqlserver/sqlserver_tde_encrypted_with_cmk/sqlserver_tde_encrypted_with_cmk_test.py @@ -8,7 +8,9 @@ from prowler.providers.azure.services.sqlserver.sqlserver_service import ( Server, ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -16,6 +18,9 @@ from tests.providers.azure.azure_fixtures import ( class Test_sqlserver_tde_encrypted_with_cmk: def test_no_sql_servers(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sqlserver_client.sql_servers = {} with ( @@ -38,6 +43,9 @@ class Test_sqlserver_tde_encrypted_with_cmk: def test_no_sql_servers_databases(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) sqlserver_client.sql_servers = { @@ -76,6 +84,9 @@ class Test_sqlserver_tde_encrypted_with_cmk: def test_sql_servers_encryption_protector_service_managed(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) database = Database( @@ -125,7 +136,7 @@ class Test_sqlserver_tde_encrypted_with_cmk: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has TDE disabled without CMK." + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has TDE disabled without CMK." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == sql_server_name @@ -134,6 +145,9 @@ class Test_sqlserver_tde_encrypted_with_cmk: def test_sql_servers_database_encryption_disabled(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) database = Database( @@ -183,7 +197,7 @@ class Test_sqlserver_tde_encrypted_with_cmk: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has TDE disabled with CMK." + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has TDE disabled with CMK." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == sql_server_name @@ -192,6 +206,9 @@ class Test_sqlserver_tde_encrypted_with_cmk: def test_sql_servers_database_encryption_enabled(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) database = Database( @@ -241,7 +258,204 @@ class Test_sqlserver_tde_encrypted_with_cmk: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has TDE enabled with CMK." + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has TDE enabled with CMK." + ) + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == sql_server_name + assert result[0].resource_id == sql_server_id + assert result[0].location == "location" + + def test_sql_servers_master_database_disabled_user_database_enabled(self): + # System "master" database always reports TDE Disabled in Azure SQL + # and is not customer-controllable. It must not fail a server whose + # user databases are correctly encrypted with CMK (PROWLER-1760). + sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } + sql_server_name = "SQL Server Name" + sql_server_id = str(uuid4()) + master_database = Database( + id="master_id", + name="master", + type="type", + location="location", + managed_by="managed_by", + tde_encryption=TransparentDataEncryption(status="Disabled"), + ) + user_database = Database( + id="user_id", + name="DynamicBudgets_Intacct", + type="type", + location="location", + managed_by="managed_by", + tde_encryption=TransparentDataEncryption(status="Enabled"), + ) + sqlserver_client.sql_servers = { + AZURE_SUBSCRIPTION_ID: [ + Server( + id=sql_server_id, + name=sql_server_name, + location="location", + public_network_access="", + minimal_tls_version="", + administrators=None, + auditing_policies=None, + firewall_rules=None, + databases=[master_database, user_database], + encryption_protector=EncryptionProtector( + server_key_type="AzureKeyVault" + ), + ) + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.sqlserver.sqlserver_tde_encrypted_with_cmk.sqlserver_tde_encrypted_with_cmk.sqlserver_client", + new=sqlserver_client, + ), + ): + from prowler.providers.azure.services.sqlserver.sqlserver_tde_encrypted_with_cmk.sqlserver_tde_encrypted_with_cmk import ( + sqlserver_tde_encrypted_with_cmk, + ) + + check = sqlserver_tde_encrypted_with_cmk() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has TDE enabled with CMK." + ) + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == sql_server_name + assert result[0].resource_id == sql_server_id + assert result[0].location == "location" + + def test_sql_servers_only_master_database(self): + # A server whose only database is the system "master" has no user + # databases to evaluate, so it must not produce a finding. + sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } + sql_server_name = "SQL Server Name" + sql_server_id = str(uuid4()) + master_database = Database( + id="master_id", + name="MASTER", + type="type", + location="location", + managed_by="managed_by", + tde_encryption=TransparentDataEncryption(status="Disabled"), + ) + sqlserver_client.sql_servers = { + AZURE_SUBSCRIPTION_ID: [ + Server( + id=sql_server_id, + name=sql_server_name, + location="location", + public_network_access="", + minimal_tls_version="", + administrators=None, + auditing_policies=None, + firewall_rules=None, + databases=[master_database], + encryption_protector=EncryptionProtector( + server_key_type="AzureKeyVault" + ), + ) + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.sqlserver.sqlserver_tde_encrypted_with_cmk.sqlserver_tde_encrypted_with_cmk.sqlserver_client", + new=sqlserver_client, + ), + ): + from prowler.providers.azure.services.sqlserver.sqlserver_tde_encrypted_with_cmk.sqlserver_tde_encrypted_with_cmk import ( + sqlserver_tde_encrypted_with_cmk, + ) + + check = sqlserver_tde_encrypted_with_cmk() + result = check.execute() + assert len(result) == 0 + + def test_sql_servers_master_disabled_user_database_disabled(self): + # Filtering out "master" must not mask a genuinely failing user + # database: a disabled user DB still fails even with CMK. + sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } + sql_server_name = "SQL Server Name" + sql_server_id = str(uuid4()) + master_database = Database( + id="master_id", + name="master", + type="type", + location="location", + managed_by="managed_by", + tde_encryption=TransparentDataEncryption(status="Disabled"), + ) + user_database = Database( + id="user_id", + name="DynamicBudgets_Intacct", + type="type", + location="location", + managed_by="managed_by", + tde_encryption=TransparentDataEncryption(status="Disabled"), + ) + sqlserver_client.sql_servers = { + AZURE_SUBSCRIPTION_ID: [ + Server( + id=sql_server_id, + name=sql_server_name, + location="location", + public_network_access="", + minimal_tls_version="", + administrators=None, + auditing_policies=None, + firewall_rules=None, + databases=[master_database, user_database], + encryption_protector=EncryptionProtector( + server_key_type="AzureKeyVault" + ), + ) + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.sqlserver.sqlserver_tde_encrypted_with_cmk.sqlserver_tde_encrypted_with_cmk.sqlserver_client", + new=sqlserver_client, + ), + ): + from prowler.providers.azure.services.sqlserver.sqlserver_tde_encrypted_with_cmk.sqlserver_tde_encrypted_with_cmk import ( + sqlserver_tde_encrypted_with_cmk, + ) + + check = sqlserver_tde_encrypted_with_cmk() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has TDE disabled with CMK." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == sql_server_name diff --git a/tests/providers/azure/services/sqlserver/sqlserver_tde_encryption_enabled/sqlserver_tde_encryption_enabled_test.py b/tests/providers/azure/services/sqlserver/sqlserver_tde_encryption_enabled/sqlserver_tde_encryption_enabled_test.py index ff782535e5..3de0dae8aa 100644 --- a/tests/providers/azure/services/sqlserver/sqlserver_tde_encryption_enabled/sqlserver_tde_encryption_enabled_test.py +++ b/tests/providers/azure/services/sqlserver/sqlserver_tde_encryption_enabled/sqlserver_tde_encryption_enabled_test.py @@ -8,7 +8,9 @@ from prowler.providers.azure.services.sqlserver.sqlserver_service import ( Server, ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -16,6 +18,9 @@ from tests.providers.azure.azure_fixtures import ( class Test_sqlserver_tde_encryption_enabled: def test_no_sql_servers(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sqlserver_client.sql_servers = {} with ( @@ -38,6 +43,9 @@ class Test_sqlserver_tde_encryption_enabled: def test_no_sql_servers_databases(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) sqlserver_client.sql_servers = { @@ -76,6 +84,9 @@ class Test_sqlserver_tde_encryption_enabled: def test_sql_servers_database_encryption_disabled(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) database_name = "Database Name" @@ -125,7 +136,7 @@ class Test_sqlserver_tde_encryption_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Database {database_name} from SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has TDE disabled" + == f"Database {database_name} from SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has TDE disabled" ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == database_name @@ -134,6 +145,9 @@ class Test_sqlserver_tde_encryption_enabled: def test_sql_servers_database_encryption_enabled(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) database_name = "Database Name" @@ -183,7 +197,7 @@ class Test_sqlserver_tde_encryption_enabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Database {database_name} from SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has TDE enabled" + == f"Database {database_name} from SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has TDE enabled" ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == database_name @@ -192,6 +206,9 @@ class Test_sqlserver_tde_encryption_enabled: def test_sql_servers_database_encryption_disabled_on_master_db(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) database_master_name = "MASTER" @@ -251,7 +268,7 @@ class Test_sqlserver_tde_encryption_enabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Database {database_name} from SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has TDE enabled" + == f"Database {database_name} from SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has TDE enabled" ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == database_name diff --git a/tests/providers/azure/services/sqlserver/sqlserver_unrestricted_inbound_access/sqlserver_unrestricted_inbound_access_test.py b/tests/providers/azure/services/sqlserver/sqlserver_unrestricted_inbound_access/sqlserver_unrestricted_inbound_access_test.py index d3c951e6b3..744460a3f1 100644 --- a/tests/providers/azure/services/sqlserver/sqlserver_unrestricted_inbound_access/sqlserver_unrestricted_inbound_access_test.py +++ b/tests/providers/azure/services/sqlserver/sqlserver_unrestricted_inbound_access/sqlserver_unrestricted_inbound_access_test.py @@ -5,7 +5,9 @@ from azure.mgmt.sql.models import FirewallRule from prowler.providers.azure.services.sqlserver.sqlserver_service import Server from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -13,6 +15,9 @@ from tests.providers.azure.azure_fixtures import ( class Test_sqlserver_unrestricted_inbound_access: def test_no_sql_servers(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sqlserver_client.sql_servers = {} with ( @@ -35,6 +40,9 @@ class Test_sqlserver_unrestricted_inbound_access: def test_sql_servers_unrestricted_inbound_access(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) sqlserver_client.sql_servers = { @@ -76,7 +84,7 @@ class Test_sqlserver_unrestricted_inbound_access: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has firewall rules allowing 0.0.0.0-255.255.255.255." + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has firewall rules allowing 0.0.0.0-255.255.255.255." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == sql_server_name @@ -85,6 +93,9 @@ class Test_sqlserver_unrestricted_inbound_access: def test_sql_servers_restricted_inbound_access(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) sqlserver_client.sql_servers = { @@ -126,7 +137,7 @@ class Test_sqlserver_unrestricted_inbound_access: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} does not have firewall rules allowing 0.0.0.0-255.255.255.255." + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} does not have firewall rules allowing 0.0.0.0-255.255.255.255." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == sql_server_name diff --git a/tests/providers/azure/services/sqlserver/sqlserver_va_emails_notifications_admins_enabled/sqlserver_va_emails_notifications_admins_enabled_test.py b/tests/providers/azure/services/sqlserver/sqlserver_va_emails_notifications_admins_enabled/sqlserver_va_emails_notifications_admins_enabled_test.py index 917c7c9651..4c5f59e54c 100644 --- a/tests/providers/azure/services/sqlserver/sqlserver_va_emails_notifications_admins_enabled/sqlserver_va_emails_notifications_admins_enabled_test.py +++ b/tests/providers/azure/services/sqlserver/sqlserver_va_emails_notifications_admins_enabled/sqlserver_va_emails_notifications_admins_enabled_test.py @@ -8,7 +8,9 @@ from azure.mgmt.sql.models import ( from prowler.providers.azure.services.sqlserver.sqlserver_service import Server from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -16,6 +18,9 @@ from tests.providers.azure.azure_fixtures import ( class Test_sqlserver_va_emails_notifications_admins_enabled: def test_no_sql_servers(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sqlserver_client.sql_servers = {} with ( @@ -38,6 +43,9 @@ class Test_sqlserver_va_emails_notifications_admins_enabled: def test_sql_servers_no_vulnerability_assessment(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) sqlserver_client.sql_servers = { @@ -78,7 +86,7 @@ class Test_sqlserver_va_emails_notifications_admins_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has vulnerability assessment disabled." + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has vulnerability assessment disabled." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == sql_server_name @@ -87,6 +95,9 @@ class Test_sqlserver_va_emails_notifications_admins_enabled: def test_sql_servers_no_vulnerability_assessment_no_admin_emails(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) sqlserver_client.sql_servers = { @@ -132,7 +143,7 @@ class Test_sqlserver_va_emails_notifications_admins_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has vulnerability assessment enabled but no scan reports configured for subscription admins." + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has vulnerability assessment enabled but no scan reports configured for subscription admins." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == sql_server_name @@ -141,6 +152,9 @@ class Test_sqlserver_va_emails_notifications_admins_enabled: def test_sql_servers_vulnerability_assessment_admin_emails_false(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) sqlserver_client.sql_servers = { @@ -186,7 +200,7 @@ class Test_sqlserver_va_emails_notifications_admins_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has vulnerability assessment enabled but no scan reports configured for subscription admins." + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has vulnerability assessment enabled but no scan reports configured for subscription admins." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == sql_server_name @@ -195,6 +209,9 @@ class Test_sqlserver_va_emails_notifications_admins_enabled: def test_sql_servers_vulnerability_assessment_no_email_subscription_admins(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) sqlserver_client.sql_servers = { @@ -240,7 +257,7 @@ class Test_sqlserver_va_emails_notifications_admins_enabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has vulnerability assessment enabled and scan reports configured for subscription admins." + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has vulnerability assessment enabled and scan reports configured for subscription admins." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == sql_server_name diff --git a/tests/providers/azure/services/sqlserver/sqlserver_va_periodic_recurring_scans_enabled/sqlserver_va_periodic_recurring_scans_enabled_test.py b/tests/providers/azure/services/sqlserver/sqlserver_va_periodic_recurring_scans_enabled/sqlserver_va_periodic_recurring_scans_enabled_test.py index e9af2d8b23..dcdbad7aee 100644 --- a/tests/providers/azure/services/sqlserver/sqlserver_va_periodic_recurring_scans_enabled/sqlserver_va_periodic_recurring_scans_enabled_test.py +++ b/tests/providers/azure/services/sqlserver/sqlserver_va_periodic_recurring_scans_enabled/sqlserver_va_periodic_recurring_scans_enabled_test.py @@ -8,7 +8,9 @@ from azure.mgmt.sql.models import ( from prowler.providers.azure.services.sqlserver.sqlserver_service import Server from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -16,6 +18,9 @@ from tests.providers.azure.azure_fixtures import ( class Test_sqlserver_va_periodic_recurring_scans_enabled: def test_no_sql_servers(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sqlserver_client.sql_servers = {} with ( @@ -38,6 +43,9 @@ class Test_sqlserver_va_periodic_recurring_scans_enabled: def test_sql_servers_no_vulnerability_assessment(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) sqlserver_client.sql_servers = { @@ -78,7 +86,7 @@ class Test_sqlserver_va_periodic_recurring_scans_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has vulnerability assessment disabled." + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has vulnerability assessment disabled." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == sql_server_name @@ -87,6 +95,9 @@ class Test_sqlserver_va_periodic_recurring_scans_enabled: def test_sql_servers_no_vulnerability_assessment_storage_container_path(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) sqlserver_client.sql_servers = { @@ -129,7 +140,7 @@ class Test_sqlserver_va_periodic_recurring_scans_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has vulnerability assessment disabled." + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has vulnerability assessment disabled." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == sql_server_name @@ -138,6 +149,9 @@ class Test_sqlserver_va_periodic_recurring_scans_enabled: def test_sql_servers_vulnerability_assessment_recuring_scans_disabled(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) sqlserver_client.sql_servers = { @@ -183,7 +197,7 @@ class Test_sqlserver_va_periodic_recurring_scans_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has vulnerability assessment enabled but no recurring scans." + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has vulnerability assessment enabled but no recurring scans." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == sql_server_name @@ -192,6 +206,9 @@ class Test_sqlserver_va_periodic_recurring_scans_enabled: def test_sql_servers_vulnerability_assessment_recuring_scans_enabled(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) sqlserver_client.sql_servers = { @@ -237,7 +254,7 @@ class Test_sqlserver_va_periodic_recurring_scans_enabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has periodic recurring scans enabled." + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has periodic recurring scans enabled." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == sql_server_name diff --git a/tests/providers/azure/services/sqlserver/sqlserver_va_scan_reports_configured/sqlserver_va_scan_reports_configured_test.py b/tests/providers/azure/services/sqlserver/sqlserver_va_scan_reports_configured/sqlserver_va_scan_reports_configured_test.py index ee1c15cc68..b085147446 100644 --- a/tests/providers/azure/services/sqlserver/sqlserver_va_scan_reports_configured/sqlserver_va_scan_reports_configured_test.py +++ b/tests/providers/azure/services/sqlserver/sqlserver_va_scan_reports_configured/sqlserver_va_scan_reports_configured_test.py @@ -8,7 +8,9 @@ from azure.mgmt.sql.models import ( from prowler.providers.azure.services.sqlserver.sqlserver_service import Server from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -16,6 +18,9 @@ from tests.providers.azure.azure_fixtures import ( class Test_sqlserver_va_scan_reports_configured: def test_no_sql_servers(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sqlserver_client.sql_servers = {} with ( @@ -38,6 +43,9 @@ class Test_sqlserver_va_scan_reports_configured: def test_sql_servers_no_vulnerability_assessment(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) sqlserver_client.sql_servers = { @@ -78,7 +86,7 @@ class Test_sqlserver_va_scan_reports_configured: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has vulnerability assessment disabled." + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has vulnerability assessment disabled." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == sql_server_name @@ -87,6 +95,9 @@ class Test_sqlserver_va_scan_reports_configured: def test_sql_servers_no_vulnerability_assessment_emails(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) sqlserver_client.sql_servers = { @@ -132,7 +143,7 @@ class Test_sqlserver_va_scan_reports_configured: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has vulnerability assessment enabled but no scan reports configured." + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has vulnerability assessment enabled but no scan reports configured." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == sql_server_name @@ -141,6 +152,9 @@ class Test_sqlserver_va_scan_reports_configured: def test_sql_servers_vulnerability_assessment_emails_none(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) sqlserver_client.sql_servers = { @@ -186,7 +200,7 @@ class Test_sqlserver_va_scan_reports_configured: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has vulnerability assessment enabled and scan reports configured." + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has vulnerability assessment enabled and scan reports configured." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == sql_server_name @@ -195,6 +209,9 @@ class Test_sqlserver_va_scan_reports_configured: def test_sql_servers_vulnerability_assessment_no_email_subscription_admins(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) sqlserver_client.sql_servers = { @@ -240,7 +257,7 @@ class Test_sqlserver_va_scan_reports_configured: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has vulnerability assessment enabled and scan reports configured." + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has vulnerability assessment enabled and scan reports configured." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == sql_server_name @@ -249,6 +266,9 @@ class Test_sqlserver_va_scan_reports_configured: def test_sql_servers_vulnerability_assessment_both_emails(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) sqlserver_client.sql_servers = { @@ -294,7 +314,7 @@ class Test_sqlserver_va_scan_reports_configured: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has vulnerability assessment enabled and scan reports configured." + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has vulnerability assessment enabled and scan reports configured." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == sql_server_name diff --git a/tests/providers/azure/services/sqlserver/sqlserver_vulnerability_assessment_enabled/sqlserver_vulnerability_assessment_enabled_test.py b/tests/providers/azure/services/sqlserver/sqlserver_vulnerability_assessment_enabled/sqlserver_vulnerability_assessment_enabled_test.py index cd0f881d0e..148b34c07b 100644 --- a/tests/providers/azure/services/sqlserver/sqlserver_vulnerability_assessment_enabled/sqlserver_vulnerability_assessment_enabled_test.py +++ b/tests/providers/azure/services/sqlserver/sqlserver_vulnerability_assessment_enabled/sqlserver_vulnerability_assessment_enabled_test.py @@ -12,7 +12,9 @@ from prowler.providers.azure.services.sqlserver.sqlserver_service import ( Server, ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -20,6 +22,9 @@ from tests.providers.azure.azure_fixtures import ( class Test_sqlserver_vulnerability_assessment_enabled: def test_no_sql_servers(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sqlserver_client.sql_servers = {} with ( @@ -42,6 +47,9 @@ class Test_sqlserver_vulnerability_assessment_enabled: def test_sql_servers_no_vulnerability_assessment(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) database = Database( @@ -92,7 +100,7 @@ class Test_sqlserver_vulnerability_assessment_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has vulnerability assessment disabled." + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has vulnerability assessment disabled." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == sql_server_name @@ -101,6 +109,9 @@ class Test_sqlserver_vulnerability_assessment_enabled: def test_sql_servers_no_vulnerability_assessment_path(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) database = Database( @@ -153,7 +164,7 @@ class Test_sqlserver_vulnerability_assessment_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has vulnerability assessment disabled." + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has vulnerability assessment disabled." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == sql_server_name @@ -162,6 +173,9 @@ class Test_sqlserver_vulnerability_assessment_enabled: def test_sql_servers_vulnerability_assessment_enabled(self): sqlserver_client = mock.MagicMock + sqlserver_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } sql_server_name = "SQL Server Name" sql_server_id = str(uuid4()) database = Database( @@ -214,7 +228,7 @@ class Test_sqlserver_vulnerability_assessment_enabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has vulnerability assessment enabled." + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has vulnerability assessment enabled." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == sql_server_name diff --git a/tests/providers/azure/services/storage/storage_account_key_access_disabled/storage_account_key_access_disabled_test.py b/tests/providers/azure/services/storage/storage_account_key_access_disabled/storage_account_key_access_disabled_test.py index 6593e90a6c..9eb232a456 100644 --- a/tests/providers/azure/services/storage/storage_account_key_access_disabled/storage_account_key_access_disabled_test.py +++ b/tests/providers/azure/services/storage/storage_account_key_access_disabled/storage_account_key_access_disabled_test.py @@ -6,7 +6,9 @@ from prowler.providers.azure.services.storage.storage_service import ( NetworkRuleSet, ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -14,6 +16,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_storage_account_key_access_disabled: def test_no_storage_accounts(self): storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = {} with ( @@ -38,6 +41,7 @@ class Test_storage_account_key_access_disabled: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account" storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( @@ -80,7 +84,7 @@ class Test_storage_account_key_access_disabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} has shared key access enabled." + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has shared key access enabled." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name @@ -91,6 +95,7 @@ class Test_storage_account_key_access_disabled: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account" storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( @@ -133,7 +138,7 @@ class Test_storage_account_key_access_disabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} has shared key access disabled." + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has shared key access disabled." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name diff --git a/tests/providers/azure/services/storage/storage_account_public_network_access_disabled/storage_account_public_network_access_disabled_test.py b/tests/providers/azure/services/storage/storage_account_public_network_access_disabled/storage_account_public_network_access_disabled_test.py new file mode 100644 index 0000000000..e66338f231 --- /dev/null +++ b/tests/providers/azure/services/storage/storage_account_public_network_access_disabled/storage_account_public_network_access_disabled_test.py @@ -0,0 +1,147 @@ +from unittest import mock +from uuid import uuid4 + +from prowler.providers.azure.services.storage.storage_service import ( + Account, + NetworkRuleSet, +) +from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, + AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, + set_mocked_azure_provider, +) + + +class Test_storage_account_public_network_access_disabled: + def test_no_storage_accounts(self): + storage_client = mock.MagicMock() + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} + storage_client.storage_accounts = {} + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.storage.storage_account_public_network_access_disabled.storage_account_public_network_access_disabled.storage_client", + new=storage_client, + ), + ): + from prowler.providers.azure.services.storage.storage_account_public_network_access_disabled.storage_account_public_network_access_disabled import ( + storage_account_public_network_access_disabled, + ) + + check = storage_account_public_network_access_disabled() + result = check.execute() + assert len(result) == 0 + + def _account(self, name, public_network_access): + return Account( + id=str(uuid4()), + name=name, + resouce_group_name="rg", + enable_https_traffic_only=False, + infrastructure_encryption=False, + allow_blob_public_access=False, + public_network_access=public_network_access, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), + encryption_type="None", + minimum_tls_version="TLS1_2", + private_endpoint_connections=[], + key_expiration_period_in_days=None, + location="westeurope", + ) + + def test_public_network_access_disabled(self): + storage_account_name = "Test Storage Account" + account = self._account(storage_account_name, "Disabled") + storage_client = mock.MagicMock() + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} + storage_client.storage_accounts = {AZURE_SUBSCRIPTION_ID: [account]} + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.storage.storage_account_public_network_access_disabled.storage_account_public_network_access_disabled.storage_client", + new=storage_client, + ), + ): + from prowler.providers.azure.services.storage.storage_account_public_network_access_disabled.storage_account_public_network_access_disabled import ( + storage_account_public_network_access_disabled, + ) + + check = storage_account_public_network_access_disabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].status_extended == ( + f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has public network access disabled." + ) + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == storage_account_name + assert result[0].resource_id == account.id + + def test_public_network_access_enabled(self): + storage_account_name = "Test Storage Account" + account = self._account(storage_account_name, "Enabled") + storage_client = mock.MagicMock() + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} + storage_client.storage_accounts = {AZURE_SUBSCRIPTION_ID: [account]} + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.storage.storage_account_public_network_access_disabled.storage_account_public_network_access_disabled.storage_client", + new=storage_client, + ), + ): + from prowler.providers.azure.services.storage.storage_account_public_network_access_disabled.storage_account_public_network_access_disabled import ( + storage_account_public_network_access_disabled, + ) + + check = storage_account_public_network_access_disabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].status_extended == ( + f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has public network access enabled." + ) + + def test_public_network_access_unset_fails(self): + storage_account_name = "Test Storage Account" + account = self._account(storage_account_name, None) + storage_client = mock.MagicMock() + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} + storage_client.storage_accounts = {AZURE_SUBSCRIPTION_ID: [account]} + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.storage.storage_account_public_network_access_disabled.storage_account_public_network_access_disabled.storage_client", + new=storage_client, + ), + ): + from prowler.providers.azure.services.storage.storage_account_public_network_access_disabled.storage_account_public_network_access_disabled import ( + storage_account_public_network_access_disabled, + ) + + check = storage_account_public_network_access_disabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].status_extended == ( + f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has public network access enabled." + ) diff --git a/tests/providers/azure/services/storage/storage_blob_public_access_level_is_disabled/storage_blob_public_access_level_is_disabled_test.py b/tests/providers/azure/services/storage/storage_blob_public_access_level_is_disabled/storage_blob_public_access_level_is_disabled_test.py index 8aaa2768d5..12765c37a5 100644 --- a/tests/providers/azure/services/storage/storage_blob_public_access_level_is_disabled/storage_blob_public_access_level_is_disabled_test.py +++ b/tests/providers/azure/services/storage/storage_blob_public_access_level_is_disabled/storage_blob_public_access_level_is_disabled_test.py @@ -6,7 +6,9 @@ from prowler.providers.azure.services.storage.storage_service import ( NetworkRuleSet, ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -14,6 +16,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_storage_blob_public_access_level_is_disabled: def test_storage_no_storage_accounts(self): storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = {} with ( @@ -38,6 +41,7 @@ class Test_storage_blob_public_access_level_is_disabled: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account" storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( @@ -79,7 +83,7 @@ class Test_storage_blob_public_access_level_is_disabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} has allow blob public access enabled." + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has allow blob public access enabled." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name @@ -90,6 +94,7 @@ class Test_storage_blob_public_access_level_is_disabled: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account" storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( @@ -131,7 +136,7 @@ class Test_storage_blob_public_access_level_is_disabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} has allow blob public access disabled." + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has allow blob public access disabled." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name diff --git a/tests/providers/azure/services/storage/storage_blob_versioning_is_enabled/storage_blob_versioning_is_enabled_test.py b/tests/providers/azure/services/storage/storage_blob_versioning_is_enabled/storage_blob_versioning_is_enabled_test.py index 357c63a935..b3b800a225 100644 --- a/tests/providers/azure/services/storage/storage_blob_versioning_is_enabled/storage_blob_versioning_is_enabled_test.py +++ b/tests/providers/azure/services/storage/storage_blob_versioning_is_enabled/storage_blob_versioning_is_enabled_test.py @@ -2,7 +2,9 @@ from unittest import mock from uuid import uuid4 from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -10,6 +12,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_storage_blob_versioning_is_enabled: def test_storage_no_storage_accounts(self): storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = {} with ( @@ -34,6 +37,7 @@ class Test_storage_blob_versioning_is_enabled: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account" storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_account_blob_properties = None with ( mock.patch( @@ -83,6 +87,7 @@ class Test_storage_blob_versioning_is_enabled: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account" storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -141,7 +146,7 @@ class Test_storage_blob_versioning_is_enabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} has blob versioning enabled." + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has blob versioning enabled." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name @@ -152,6 +157,7 @@ class Test_storage_blob_versioning_is_enabled: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account" storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -210,7 +216,7 @@ class Test_storage_blob_versioning_is_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} does not have blob versioning enabled." + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} does not have blob versioning enabled." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name diff --git a/tests/providers/azure/services/storage/storage_cross_tenant_replication_disabled/storage_cross_tenant_replication_disabled_test.py b/tests/providers/azure/services/storage/storage_cross_tenant_replication_disabled/storage_cross_tenant_replication_disabled_test.py index e90665d613..e9c433afb4 100644 --- a/tests/providers/azure/services/storage/storage_cross_tenant_replication_disabled/storage_cross_tenant_replication_disabled_test.py +++ b/tests/providers/azure/services/storage/storage_cross_tenant_replication_disabled/storage_cross_tenant_replication_disabled_test.py @@ -6,7 +6,9 @@ from prowler.providers.azure.services.storage.storage_service import ( NetworkRuleSet, ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -14,6 +16,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_storage_cross_tenant_replication_disabled: def test_no_storage_accounts(self): storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = {} with ( @@ -38,6 +41,7 @@ class Test_storage_cross_tenant_replication_disabled: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account" storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( @@ -80,7 +84,7 @@ class Test_storage_cross_tenant_replication_disabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} has cross-tenant replication enabled." + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has cross-tenant replication enabled." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name @@ -91,6 +95,7 @@ class Test_storage_cross_tenant_replication_disabled: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account" storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( @@ -133,7 +138,7 @@ class Test_storage_cross_tenant_replication_disabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} has cross-tenant replication disabled." + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has cross-tenant replication disabled." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name diff --git a/tests/providers/azure/services/storage/storage_default_network_access_rule_is_denied/storage_default_network_access_rule_is_denied_test.py b/tests/providers/azure/services/storage/storage_default_network_access_rule_is_denied/storage_default_network_access_rule_is_denied_test.py index 9c667b372d..68be9d87c6 100644 --- a/tests/providers/azure/services/storage/storage_default_network_access_rule_is_denied/storage_default_network_access_rule_is_denied_test.py +++ b/tests/providers/azure/services/storage/storage_default_network_access_rule_is_denied/storage_default_network_access_rule_is_denied_test.py @@ -6,7 +6,9 @@ from prowler.providers.azure.services.storage.storage_service import ( NetworkRuleSet, ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -14,6 +16,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_storage_default_network_access_rule_is_denied: def test_storage_no_storage_accounts(self): storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = {} with ( @@ -38,6 +41,7 @@ class Test_storage_default_network_access_rule_is_denied: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account" storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( @@ -79,7 +83,7 @@ class Test_storage_default_network_access_rule_is_denied: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} has network access rule set to Allow." + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has network access rule set to Allow." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name @@ -90,6 +94,7 @@ class Test_storage_default_network_access_rule_is_denied: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account" storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( @@ -131,7 +136,7 @@ class Test_storage_default_network_access_rule_is_denied: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} has network access rule set to Deny." + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has network access rule set to Deny." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name diff --git a/tests/providers/azure/services/storage/storage_default_to_entra_authorization_enabled/storage_default_to_entra_authorization_enabled_test.py b/tests/providers/azure/services/storage/storage_default_to_entra_authorization_enabled/storage_default_to_entra_authorization_enabled_test.py index 99b7874250..33b20f0900 100644 --- a/tests/providers/azure/services/storage/storage_default_to_entra_authorization_enabled/storage_default_to_entra_authorization_enabled_test.py +++ b/tests/providers/azure/services/storage/storage_default_to_entra_authorization_enabled/storage_default_to_entra_authorization_enabled_test.py @@ -6,7 +6,9 @@ from prowler.providers.azure.services.storage.storage_service import ( NetworkRuleSet, ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -14,6 +16,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_storage_default_to_entra_authorization_enabled: def test_no_storage_accounts(self): storage_client = mock.MagicMock() + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = {} with ( @@ -38,6 +41,7 @@ class Test_storage_default_to_entra_authorization_enabled: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account Entra Auth Enabled" storage_client = mock.MagicMock() + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( @@ -80,7 +84,7 @@ class Test_storage_default_to_entra_authorization_enabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Default to Microsoft Entra authorization is enabled for storage account {storage_account_name}." + == f"Default to Microsoft Entra authorization is enabled for storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name @@ -91,6 +95,7 @@ class Test_storage_default_to_entra_authorization_enabled: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account Entra Auth Disabled" storage_client = mock.MagicMock() + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( @@ -133,7 +138,7 @@ class Test_storage_default_to_entra_authorization_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Default to Microsoft Entra authorization is not enabled for storage account {storage_account_name}." + == f"Default to Microsoft Entra authorization is not enabled for storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name diff --git a/tests/providers/azure/services/storage/storage_ensure_azure_services_are_trusted_to_access_is_enabled/storage_ensure_azure_services_are_trusted_to_access_is_enabled_test.py b/tests/providers/azure/services/storage/storage_ensure_azure_services_are_trusted_to_access_is_enabled/storage_ensure_azure_services_are_trusted_to_access_is_enabled_test.py index d65978c2ff..8b2c19f41d 100644 --- a/tests/providers/azure/services/storage/storage_ensure_azure_services_are_trusted_to_access_is_enabled/storage_ensure_azure_services_are_trusted_to_access_is_enabled_test.py +++ b/tests/providers/azure/services/storage/storage_ensure_azure_services_are_trusted_to_access_is_enabled/storage_ensure_azure_services_are_trusted_to_access_is_enabled_test.py @@ -6,7 +6,9 @@ from prowler.providers.azure.services.storage.storage_service import ( NetworkRuleSet, ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -14,6 +16,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_storage_ensure_azure_services_are_trusted_to_access_is_enabled: def test_storage_no_storage_accounts(self): storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = {} with ( @@ -38,6 +41,7 @@ class Test_storage_ensure_azure_services_are_trusted_to_access_is_enabled: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account" storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( @@ -79,7 +83,7 @@ class Test_storage_ensure_azure_services_are_trusted_to_access_is_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} does not allow trusted Microsoft services to access this storage account." + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} does not allow trusted Microsoft services to access this storage account." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name @@ -90,6 +94,7 @@ class Test_storage_ensure_azure_services_are_trusted_to_access_is_enabled: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account" storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( @@ -131,7 +136,7 @@ class Test_storage_ensure_azure_services_are_trusted_to_access_is_enabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} allows trusted Microsoft services to access this storage account." + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} allows trusted Microsoft services to access this storage account." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name diff --git a/tests/providers/azure/services/storage/storage_ensure_encryption_with_customer_managed_keys/storage_ensure_encryption_with_customer_managed_keys_test.py b/tests/providers/azure/services/storage/storage_ensure_encryption_with_customer_managed_keys/storage_ensure_encryption_with_customer_managed_keys_test.py index 7f9803800c..305f840729 100644 --- a/tests/providers/azure/services/storage/storage_ensure_encryption_with_customer_managed_keys/storage_ensure_encryption_with_customer_managed_keys_test.py +++ b/tests/providers/azure/services/storage/storage_ensure_encryption_with_customer_managed_keys/storage_ensure_encryption_with_customer_managed_keys_test.py @@ -6,7 +6,9 @@ from prowler.providers.azure.services.storage.storage_service import ( NetworkRuleSet, ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -14,6 +16,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_storage_ensure_encryption_with_customer_managed_keys: def test_storage_no_storage_accounts(self): storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = {} with ( @@ -38,6 +41,7 @@ class Test_storage_ensure_encryption_with_customer_managed_keys: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account" storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( @@ -79,7 +83,7 @@ class Test_storage_ensure_encryption_with_customer_managed_keys: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} does not encrypt with CMKs." + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} does not encrypt with CMKs." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name @@ -90,6 +94,7 @@ class Test_storage_ensure_encryption_with_customer_managed_keys: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account" storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( @@ -131,7 +136,7 @@ class Test_storage_ensure_encryption_with_customer_managed_keys: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} encrypts with CMKs." + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} encrypts with CMKs." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name diff --git a/tests/providers/azure/services/storage/storage_ensure_file_shares_soft_delete_is_enabled/storage_ensure_file_shares_soft_delete_is_enabled_test.py b/tests/providers/azure/services/storage/storage_ensure_file_shares_soft_delete_is_enabled/storage_ensure_file_shares_soft_delete_is_enabled_test.py index e2c97b7e2e..56517e9e05 100644 --- a/tests/providers/azure/services/storage/storage_ensure_file_shares_soft_delete_is_enabled/storage_ensure_file_shares_soft_delete_is_enabled_test.py +++ b/tests/providers/azure/services/storage/storage_ensure_file_shares_soft_delete_is_enabled/storage_ensure_file_shares_soft_delete_is_enabled_test.py @@ -9,7 +9,9 @@ from prowler.providers.azure.services.storage.storage_service import ( SMBProtocolSettings, ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -17,6 +19,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_storage_ensure_file_shares_soft_delete_is_enabled: def test_no_storage_accounts(self): storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = {} with ( @@ -41,6 +44,7 @@ class Test_storage_ensure_file_shares_soft_delete_is_enabled: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account" storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( @@ -85,6 +89,7 @@ class Test_storage_ensure_file_shares_soft_delete_is_enabled: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account" storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} retention_policy = DeleteRetentionPolicy(enabled=False, days=0) file_service_properties = FileServiceProperties( id=f"/subscriptions/{AZURE_SUBSCRIPTION_ID}/resourceGroups/prowler-resource-group/providers/Microsoft.Storage/storageAccounts/{storage_account_name}/fileServices/default", @@ -137,7 +142,7 @@ class Test_storage_ensure_file_shares_soft_delete_is_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"File share soft delete is not enabled for storage account {storage_account_name}." + == f"File share soft delete is not enabled for storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name @@ -148,6 +153,7 @@ class Test_storage_ensure_file_shares_soft_delete_is_enabled: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account" storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} retention_policy = DeleteRetentionPolicy(enabled=True, days=7) file_service_properties = FileServiceProperties( id=f"/subscriptions/{AZURE_SUBSCRIPTION_ID}/resourceGroups/prowler-resource-group/providers/Microsoft.Storage/storageAccounts/{storage_account_name}/fileServices/default", @@ -200,7 +206,7 @@ class Test_storage_ensure_file_shares_soft_delete_is_enabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"File share soft delete is enabled for storage account {storage_account_name} with a retention period of {retention_policy.days} days." + == f"File share soft delete is enabled for storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} with a retention period of {retention_policy.days} days." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name diff --git a/tests/providers/azure/services/storage/storage_ensure_minimum_tls_version_12/storage_ensure_minimum_tls_version_12_test.py b/tests/providers/azure/services/storage/storage_ensure_minimum_tls_version_12/storage_ensure_minimum_tls_version_12_test.py index 16ffe488bb..c3ea126e22 100644 --- a/tests/providers/azure/services/storage/storage_ensure_minimum_tls_version_12/storage_ensure_minimum_tls_version_12_test.py +++ b/tests/providers/azure/services/storage/storage_ensure_minimum_tls_version_12/storage_ensure_minimum_tls_version_12_test.py @@ -6,7 +6,9 @@ from prowler.providers.azure.services.storage.storage_service import ( NetworkRuleSet, ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -14,6 +16,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_storage_ensure_minimum_tls_version_12: def test_storage_no_storage_accounts(self): storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = {} with ( @@ -38,6 +41,7 @@ class Test_storage_ensure_minimum_tls_version_12: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account" storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( @@ -79,7 +83,7 @@ class Test_storage_ensure_minimum_tls_version_12: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} does not have TLS version set to 1.2." + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} does not have TLS version set to 1.2." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name @@ -90,6 +94,7 @@ class Test_storage_ensure_minimum_tls_version_12: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account" storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( @@ -131,7 +136,7 @@ class Test_storage_ensure_minimum_tls_version_12: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} has TLS version set to 1.2." + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has TLS version set to 1.2." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name diff --git a/tests/providers/azure/services/storage/storage_ensure_private_endpoints_in_storage_accounts/storage_ensure_private_endpoints_in_storage_accounts_test.py b/tests/providers/azure/services/storage/storage_ensure_private_endpoints_in_storage_accounts/storage_ensure_private_endpoints_in_storage_accounts_test.py index 2652085907..5f3d2581fd 100644 --- a/tests/providers/azure/services/storage/storage_ensure_private_endpoints_in_storage_accounts/storage_ensure_private_endpoints_in_storage_accounts_test.py +++ b/tests/providers/azure/services/storage/storage_ensure_private_endpoints_in_storage_accounts/storage_ensure_private_endpoints_in_storage_accounts_test.py @@ -7,7 +7,9 @@ from prowler.providers.azure.services.storage.storage_service import ( PrivateEndpointConnection, ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -15,6 +17,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_storage_ensure_private_endpoints_in_storage_accounts: def test_storage_ensure_private_endpoints_in_storage_accounts(self): storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = {} with ( @@ -41,6 +44,7 @@ class Test_storage_ensure_private_endpoints_in_storage_accounts: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account" storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( @@ -82,7 +86,7 @@ class Test_storage_ensure_private_endpoints_in_storage_accounts: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} does not have private endpoint connections." + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} does not have private endpoint connections." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name @@ -95,6 +99,7 @@ class Test_storage_ensure_private_endpoints_in_storage_accounts: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account" storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( @@ -142,7 +147,7 @@ class Test_storage_ensure_private_endpoints_in_storage_accounts: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} has private endpoint connections." + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has private endpoint connections." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name diff --git a/tests/providers/azure/services/storage/storage_ensure_soft_delete_is_enabled/storage_ensure_soft_delete_is_enabled_test.py b/tests/providers/azure/services/storage/storage_ensure_soft_delete_is_enabled/storage_ensure_soft_delete_is_enabled_test.py index acb5920815..c6f2d27e0f 100644 --- a/tests/providers/azure/services/storage/storage_ensure_soft_delete_is_enabled/storage_ensure_soft_delete_is_enabled_test.py +++ b/tests/providers/azure/services/storage/storage_ensure_soft_delete_is_enabled/storage_ensure_soft_delete_is_enabled_test.py @@ -8,7 +8,9 @@ from prowler.providers.azure.services.storage.storage_service import ( NetworkRuleSet, ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -16,6 +18,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_storage_ensure_soft_delete_is_enabled: def test_storage_no_storage_accounts(self): storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = {} with ( @@ -40,6 +43,7 @@ class Test_storage_ensure_soft_delete_is_enabled: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account" storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_account_blob_properties = None storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ @@ -87,6 +91,7 @@ class Test_storage_ensure_soft_delete_is_enabled: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account" storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_account_blob_properties = BlobProperties( id="id", name="name", @@ -139,7 +144,7 @@ class Test_storage_ensure_soft_delete_is_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} has soft delete disabled." + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has soft delete disabled." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name @@ -152,6 +157,7 @@ class Test_storage_ensure_soft_delete_is_enabled: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account" storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_account_blob_properties = BlobProperties( id="id", name="name", @@ -204,7 +210,7 @@ class Test_storage_ensure_soft_delete_is_enabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} has soft delete enabled." + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has soft delete enabled." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name diff --git a/tests/providers/azure/services/storage/storage_geo_redundant_enabled/storage_geo_redundant_enabled_test.py b/tests/providers/azure/services/storage/storage_geo_redundant_enabled/storage_geo_redundant_enabled_test.py index cfe2f5a00b..cabf9bbd5c 100644 --- a/tests/providers/azure/services/storage/storage_geo_redundant_enabled/storage_geo_redundant_enabled_test.py +++ b/tests/providers/azure/services/storage/storage_geo_redundant_enabled/storage_geo_redundant_enabled_test.py @@ -6,7 +6,9 @@ from prowler.providers.azure.services.storage.storage_service import ( NetworkRuleSet, ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -14,6 +16,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_storage_geo_redundant_enabled: def test_no_storage_accounts(self): storage_client = mock.MagicMock() + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = {} with ( @@ -38,6 +41,7 @@ class Test_storage_geo_redundant_enabled: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account GRS" storage_client = mock.MagicMock() + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} replication_setting = "Standard_GRS" storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ @@ -81,7 +85,7 @@ class Test_storage_geo_redundant_enabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} has Geo-redundant storage {replication_setting} enabled." + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has Geo-redundant storage {replication_setting} enabled." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name @@ -92,6 +96,7 @@ class Test_storage_geo_redundant_enabled: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account RAGRS" storage_client = mock.MagicMock() + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} replication_setting = "Standard_RAGRS" storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ @@ -135,7 +140,7 @@ class Test_storage_geo_redundant_enabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} has Geo-redundant storage {replication_setting} enabled." + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has Geo-redundant storage {replication_setting} enabled." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name @@ -146,6 +151,7 @@ class Test_storage_geo_redundant_enabled: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account GZRS" storage_client = mock.MagicMock() + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} replication_setting = "Standard_GZRS" storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ @@ -189,7 +195,7 @@ class Test_storage_geo_redundant_enabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} has Geo-redundant storage {replication_setting} enabled." + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has Geo-redundant storage {replication_setting} enabled." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name @@ -200,6 +206,7 @@ class Test_storage_geo_redundant_enabled: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account RAGZRS" storage_client = mock.MagicMock() + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} replication_setting = "Standard_RAGZRS" storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ @@ -243,7 +250,7 @@ class Test_storage_geo_redundant_enabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} has Geo-redundant storage {replication_setting} enabled." + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has Geo-redundant storage {replication_setting} enabled." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name @@ -254,6 +261,7 @@ class Test_storage_geo_redundant_enabled: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account LRS" storage_client = mock.MagicMock() + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} replication_setting = "Standard_LRS" storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ @@ -297,7 +305,7 @@ class Test_storage_geo_redundant_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} does not have Geo-redundant storage enabled, it has {replication_setting} instead." + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} does not have Geo-redundant storage enabled, it has {replication_setting} instead." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name @@ -308,6 +316,7 @@ class Test_storage_geo_redundant_enabled: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account ZRS" storage_client = mock.MagicMock() + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} replication_setting = "Standard_ZRS" storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ @@ -351,7 +360,7 @@ class Test_storage_geo_redundant_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} does not have Geo-redundant storage enabled, it has {replication_setting} instead." + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} does not have Geo-redundant storage enabled, it has {replication_setting} instead." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name @@ -362,6 +371,7 @@ class Test_storage_geo_redundant_enabled: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account Premium LRS" storage_client = mock.MagicMock() + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} replication_setting = "Premium_LRS" storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ @@ -405,7 +415,7 @@ class Test_storage_geo_redundant_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} does not have Geo-redundant storage enabled, it has {replication_setting} instead." + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} does not have Geo-redundant storage enabled, it has {replication_setting} instead." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name @@ -416,6 +426,7 @@ class Test_storage_geo_redundant_enabled: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account Premium ZRS" storage_client = mock.MagicMock() + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} replication_setting = "Premium_ZRS" storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ @@ -459,7 +470,7 @@ class Test_storage_geo_redundant_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} does not have Geo-redundant storage enabled, it has {replication_setting} instead." + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} does not have Geo-redundant storage enabled, it has {replication_setting} instead." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name diff --git a/tests/providers/azure/services/storage/storage_infrastructure_encryption_is_enabled/storage_infrastructure_encryption_is_enabled_test.py b/tests/providers/azure/services/storage/storage_infrastructure_encryption_is_enabled/storage_infrastructure_encryption_is_enabled_test.py index c66fe2dcfd..91a59f101f 100644 --- a/tests/providers/azure/services/storage/storage_infrastructure_encryption_is_enabled/storage_infrastructure_encryption_is_enabled_test.py +++ b/tests/providers/azure/services/storage/storage_infrastructure_encryption_is_enabled/storage_infrastructure_encryption_is_enabled_test.py @@ -6,7 +6,9 @@ from prowler.providers.azure.services.storage.storage_service import ( NetworkRuleSet, ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -14,6 +16,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_storage_infrastructure_encryption_is_enabled: def test_storage_no_storage_accounts(self): storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = {} with ( @@ -38,6 +41,7 @@ class Test_storage_infrastructure_encryption_is_enabled: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account" storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( @@ -79,7 +83,7 @@ class Test_storage_infrastructure_encryption_is_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} has infrastructure encryption disabled." + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has infrastructure encryption disabled." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name @@ -90,6 +94,7 @@ class Test_storage_infrastructure_encryption_is_enabled: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account" storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( @@ -131,7 +136,7 @@ class Test_storage_infrastructure_encryption_is_enabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} has infrastructure encryption enabled." + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has infrastructure encryption enabled." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name diff --git a/tests/providers/azure/services/storage/storage_key_rotation_90_days/storage_key_rotation_90_days_test.py b/tests/providers/azure/services/storage/storage_key_rotation_90_days/storage_key_rotation_90_days_test.py index 480a0737dc..8f5df69f72 100644 --- a/tests/providers/azure/services/storage/storage_key_rotation_90_days/storage_key_rotation_90_days_test.py +++ b/tests/providers/azure/services/storage/storage_key_rotation_90_days/storage_key_rotation_90_days_test.py @@ -6,7 +6,9 @@ from prowler.providers.azure.services.storage.storage_service import ( NetworkRuleSet, ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -14,6 +16,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_storage_key_rotation_90_dayss: def test_storage_no_storage_accounts(self): storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = {} with ( @@ -39,6 +42,7 @@ class Test_storage_key_rotation_90_dayss: storage_account_name = "Test Storage Account" expiration_days = 91 storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( @@ -80,7 +84,7 @@ class Test_storage_key_rotation_90_dayss: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} has an invalid key expiration period of {expiration_days} days." + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has an invalid key expiration period of {expiration_days} days." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name @@ -92,6 +96,7 @@ class Test_storage_key_rotation_90_dayss: storage_account_name = "Test Storage Account" expiration_days = 90 storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( @@ -133,7 +138,7 @@ class Test_storage_key_rotation_90_dayss: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} has a key expiration period of {expiration_days} days." + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has a key expiration period of {expiration_days} days." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name @@ -144,6 +149,7 @@ class Test_storage_key_rotation_90_dayss: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account" storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( @@ -185,7 +191,7 @@ class Test_storage_key_rotation_90_dayss: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} has no key expiration period set." + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has no key expiration period set." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name diff --git a/tests/providers/azure/services/storage/storage_secure_transfer_required_is_enabled/storage_secure_transfer_required_is_enabled_test.py b/tests/providers/azure/services/storage/storage_secure_transfer_required_is_enabled/storage_secure_transfer_required_is_enabled_test.py index cd3c8ab408..0143153caf 100644 --- a/tests/providers/azure/services/storage/storage_secure_transfer_required_is_enabled/storage_secure_transfer_required_is_enabled_test.py +++ b/tests/providers/azure/services/storage/storage_secure_transfer_required_is_enabled/storage_secure_transfer_required_is_enabled_test.py @@ -6,7 +6,9 @@ from prowler.providers.azure.services.storage.storage_service import ( NetworkRuleSet, ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -14,6 +16,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_storage_secure_transfer_required_is_enabled: def test_storage_no_storage_accounts(self): storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = {} with ( @@ -38,6 +41,7 @@ class Test_storage_secure_transfer_required_is_enabled: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account" storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( @@ -79,7 +83,7 @@ class Test_storage_secure_transfer_required_is_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} has secure transfer required disabled." + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has secure transfer required disabled." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name @@ -90,6 +94,7 @@ class Test_storage_secure_transfer_required_is_enabled: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account" storage_client = mock.MagicMock + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( @@ -131,7 +136,7 @@ class Test_storage_secure_transfer_required_is_enabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} has secure transfer required enabled." + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has secure transfer required enabled." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name diff --git a/tests/providers/azure/services/storage/storage_service_test.py b/tests/providers/azure/services/storage/storage_service_test.py index 3d75fa5000..67fba33877 100644 --- a/tests/providers/azure/services/storage/storage_service_test.py +++ b/tests/providers/azure/services/storage/storage_service_test.py @@ -42,6 +42,7 @@ def mock_storage_get_storage_accounts(_): enable_https_traffic_only=False, infrastructure_encryption=False, allow_blob_public_access=False, + public_network_access="Disabled", network_rule_set=NetworkRuleSet( bypass="AzureServices", default_action="Allow" ), @@ -97,6 +98,10 @@ class Test_Storage_Service: storage.storage_accounts[AZURE_SUBSCRIPTION_ID][0].allow_blob_public_access is False ) + assert ( + storage.storage_accounts[AZURE_SUBSCRIPTION_ID][0].public_network_access + == "Disabled" + ) assert ( storage.storage_accounts[AZURE_SUBSCRIPTION_ID][0].network_rule_set is not None diff --git a/tests/providers/azure/services/storage/storage_smb_channel_encryption_with_secure_algorithm/storage_smb_channel_encryption_with_secure_algorithm_test.py b/tests/providers/azure/services/storage/storage_smb_channel_encryption_with_secure_algorithm/storage_smb_channel_encryption_with_secure_algorithm_test.py index db82c09df4..e213080d05 100644 --- a/tests/providers/azure/services/storage/storage_smb_channel_encryption_with_secure_algorithm/storage_smb_channel_encryption_with_secure_algorithm_test.py +++ b/tests/providers/azure/services/storage/storage_smb_channel_encryption_with_secure_algorithm/storage_smb_channel_encryption_with_secure_algorithm_test.py @@ -9,7 +9,9 @@ from prowler.providers.azure.services.storage.storage_service import ( SMBProtocolSettings, ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -17,6 +19,8 @@ from tests.providers.azure.azure_fixtures import ( class Test_storage_smb_channel_encryption_with_secure_algorithm: def test_no_storage_accounts(self): storage_client = mock.MagicMock() + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} + storage_client.audit_config = {} storage_client.storage_accounts = {} with ( mock.patch( @@ -40,6 +44,8 @@ class Test_storage_smb_channel_encryption_with_secure_algorithm: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account" storage_client = mock.MagicMock() + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} + storage_client.audit_config = {} storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( @@ -92,6 +98,8 @@ class Test_storage_smb_channel_encryption_with_secure_algorithm: ), ) storage_client = mock.MagicMock() + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} + storage_client.audit_config = {} storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( @@ -132,7 +140,7 @@ class Test_storage_smb_channel_encryption_with_secure_algorithm: assert len(result) == 1 assert result[0].status == "FAIL" assert result[0].status_extended == ( - f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} does not have SMB channel encryption enabled for file shares." + f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} does not have SMB channel encryption enabled for file shares." ) def test_not_recommended_encryption(self): @@ -148,6 +156,8 @@ class Test_storage_smb_channel_encryption_with_secure_algorithm: ), ) storage_client = mock.MagicMock() + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} + storage_client.audit_config = {} storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( @@ -188,7 +198,7 @@ class Test_storage_smb_channel_encryption_with_secure_algorithm: assert len(result) == 1 assert result[0].status == "FAIL" assert result[0].status_extended == ( - f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} does not have SMB channel encryption with a secure algorithm for file shares since it supports AES-128-GCM." + f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} allows insecure algorithms for SMB channel encryption on file shares since it supports AES-128-GCM and only AES-256-GCM is recommended." ) def test_recommended_encryption(self): @@ -204,6 +214,8 @@ class Test_storage_smb_channel_encryption_with_secure_algorithm: ), ) storage_client = mock.MagicMock() + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} + storage_client.audit_config = {} storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( @@ -244,5 +256,126 @@ class Test_storage_smb_channel_encryption_with_secure_algorithm: assert len(result) == 1 assert result[0].status == "PASS" assert result[0].status_extended == ( - f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} has a secure algorithm for SMB channel encryption (AES-256-GCM) enabled for file shares since it supports AES-256-GCM." + f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} only allows secure algorithms for SMB channel encryption on file shares since it supports AES-256-GCM." + ) + + def test_recommended_algorithm_mixed_with_weak_algorithm(self): + storage_account_id = str(uuid4()) + storage_account_name = "Test Storage Account" + file_service_properties = FileServiceProperties( + id="id1", + name="fs1", + type="type1", + share_delete_retention_policy=DeleteRetentionPolicy(enabled=True, days=7), + smb_protocol_settings=SMBProtocolSettings( + channel_encryption=["AES-128-CCM", "AES-256-GCM"], supported_versions=[] + ), + ) + storage_client = mock.MagicMock() + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} + storage_client.audit_config = {} + storage_client.storage_accounts = { + AZURE_SUBSCRIPTION_ID: [ + Account( + id=storage_account_id, + name=storage_account_name, + resouce_group_name="rg", + enable_https_traffic_only=False, + infrastructure_encryption=False, + allow_blob_public_access=False, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), + encryption_type="None", + minimum_tls_version="TLS1_2", + key_expiration_period_in_days=None, + location="westeurope", + private_endpoint_connections=[], + file_service_properties=file_service_properties, + ) + ] + } + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.storage.storage_smb_channel_encryption_with_secure_algorithm.storage_smb_channel_encryption_with_secure_algorithm.storage_client", + new=storage_client, + ), + ): + from prowler.providers.azure.services.storage.storage_smb_channel_encryption_with_secure_algorithm.storage_smb_channel_encryption_with_secure_algorithm import ( + storage_smb_channel_encryption_with_secure_algorithm, + ) + + check = storage_smb_channel_encryption_with_secure_algorithm() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].status_extended == ( + f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} allows insecure algorithms for SMB channel encryption on file shares since it supports AES-128-CCM, AES-256-GCM and only AES-256-GCM is recommended." + ) + + def test_custom_recommended_algorithms_from_config(self): + storage_account_id = str(uuid4()) + storage_account_name = "Test Storage Account" + file_service_properties = FileServiceProperties( + id="id1", + name="fs1", + type="type1", + share_delete_retention_policy=DeleteRetentionPolicy(enabled=True, days=7), + smb_protocol_settings=SMBProtocolSettings( + channel_encryption=["AES-128-GCM", "AES-256-GCM"], supported_versions=[] + ), + ) + storage_client = mock.MagicMock() + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} + storage_client.audit_config = { + "recommended_smb_channel_encryption_algorithms": [ + "AES-128-GCM", + "AES-256-GCM", + ] + } + storage_client.storage_accounts = { + AZURE_SUBSCRIPTION_ID: [ + Account( + id=storage_account_id, + name=storage_account_name, + resouce_group_name="rg", + enable_https_traffic_only=False, + infrastructure_encryption=False, + allow_blob_public_access=False, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), + encryption_type="None", + minimum_tls_version="TLS1_2", + key_expiration_period_in_days=None, + location="westeurope", + private_endpoint_connections=[], + file_service_properties=file_service_properties, + ) + ] + } + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.storage.storage_smb_channel_encryption_with_secure_algorithm.storage_smb_channel_encryption_with_secure_algorithm.storage_client", + new=storage_client, + ), + ): + from prowler.providers.azure.services.storage.storage_smb_channel_encryption_with_secure_algorithm.storage_smb_channel_encryption_with_secure_algorithm import ( + storage_smb_channel_encryption_with_secure_algorithm, + ) + + check = storage_smb_channel_encryption_with_secure_algorithm() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].status_extended == ( + f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} only allows secure algorithms for SMB channel encryption on file shares since it supports AES-128-GCM, AES-256-GCM." ) diff --git a/tests/providers/azure/services/storage/storage_smb_protocol_version_is_latest/storage_smb_protocol_version_is_latest_test.py b/tests/providers/azure/services/storage/storage_smb_protocol_version_is_latest/storage_smb_protocol_version_is_latest_test.py index 4194c7ae55..33b83fcca8 100644 --- a/tests/providers/azure/services/storage/storage_smb_protocol_version_is_latest/storage_smb_protocol_version_is_latest_test.py +++ b/tests/providers/azure/services/storage/storage_smb_protocol_version_is_latest/storage_smb_protocol_version_is_latest_test.py @@ -9,7 +9,9 @@ from prowler.providers.azure.services.storage.storage_service import ( SMBProtocolSettings, ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -17,6 +19,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_storage_smb_protocol_version_is_latest: def test_no_storage_accounts(self): storage_client = mock.MagicMock() + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = {} with ( mock.patch( @@ -40,6 +43,7 @@ class Test_storage_smb_protocol_version_is_latest: storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account" storage_client = mock.MagicMock() + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( @@ -92,6 +96,7 @@ class Test_storage_smb_protocol_version_is_latest: ), ) storage_client = mock.MagicMock() + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( @@ -132,7 +137,7 @@ class Test_storage_smb_protocol_version_is_latest: assert len(result) == 1 assert result[0].status == "PASS" assert ( - f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} allows only the latest SMB protocol version (SMB3.1.1) for file shares." + f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} allows only the latest SMB protocol version (SMB3.1.1) for file shares." in result[0].status_extended ) @@ -149,6 +154,7 @@ class Test_storage_smb_protocol_version_is_latest: ), ) storage_client = mock.MagicMock() + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( @@ -189,7 +195,7 @@ class Test_storage_smb_protocol_version_is_latest: assert len(result) == 1 assert result[0].status == "FAIL" assert ( - f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} allows SMB protocol versions: SMB2.1, SMB3.1.1. Only the latest SMB protocol version (SMB3.1.1) should be allowed." + f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} allows SMB protocol versions: SMB2.1, SMB3.1.1. Only the latest SMB protocol version (SMB3.1.1) should be allowed." in result[0].status_extended ) @@ -206,6 +212,7 @@ class Test_storage_smb_protocol_version_is_latest: ), ) storage_client = mock.MagicMock() + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( @@ -258,6 +265,7 @@ class Test_storage_smb_protocol_version_is_latest: ), ) storage_client = mock.MagicMock() + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( @@ -298,6 +306,6 @@ class Test_storage_smb_protocol_version_is_latest: assert len(result) == 1 assert result[0].status == "FAIL" assert ( - f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} allows SMB protocol versions: SMB3.1.1, SMB3.0. Only the latest SMB protocol version (SMB3.1.1) should be allowed." + f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} allows SMB protocol versions: SMB3.1.1, SMB3.0. Only the latest SMB protocol version (SMB3.1.1) should be allowed." in result[0].status_extended ) diff --git a/tests/providers/azure/services/vm/vm_backup_enabled/vm_backup_enabled_test.py b/tests/providers/azure/services/vm/vm_backup_enabled/vm_backup_enabled_test.py index a99be2ea54..0992055930 100644 --- a/tests/providers/azure/services/vm/vm_backup_enabled/vm_backup_enabled_test.py +++ b/tests/providers/azure/services/vm/vm_backup_enabled/vm_backup_enabled_test.py @@ -2,7 +2,9 @@ from unittest import mock from uuid import uuid4 from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -10,7 +12,9 @@ from tests.providers.azure.azure_fixtures import ( class Test_vm_backup_enabled: def test_vm_backup_enabled_no_subscriptions(self): vm_client = mock.MagicMock + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} recovery_client = mock.MagicMock + recovery_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = {} recovery_client.vaults = {} @@ -38,8 +42,12 @@ class Test_vm_backup_enabled: def test_no_vms(self): mock_vm_client = mock.MagicMock() + mock_vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} mock_vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {}} mock_recovery_client = mock.MagicMock() + mock_recovery_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } mock_recovery_client.vaults = {AZURE_SUBSCRIPTION_ID: {}} with ( mock.patch( @@ -69,7 +77,11 @@ class Test_vm_backup_enabled: vault_id = str(uuid4()) vault_name = "vault1" mock_vm_client = mock.MagicMock() + mock_vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} mock_recovery_client = mock.MagicMock() + mock_recovery_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } with ( mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -139,7 +151,7 @@ class Test_vm_backup_enabled: assert result[0].resource_id == vm_id assert ( result[0].status_extended - == f"VM {vm_name} in subscription {AZURE_SUBSCRIPTION_ID} is protected by Azure Backup (vault: {vault_name})." + == f"VM {vm_name} in subscription {AZURE_SUBSCRIPTION_DISPLAY} is protected by Azure Backup (vault: {vault_name})." ) def test_vm_not_protected_by_backup(self): @@ -148,7 +160,11 @@ class Test_vm_backup_enabled: vault_id = str(uuid4()) vault_name = "vault1" mock_vm_client = mock.MagicMock() + mock_vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} mock_recovery_client = mock.MagicMock() + mock_recovery_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } with ( mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -218,7 +234,7 @@ class Test_vm_backup_enabled: assert result[0].resource_id == vm_id assert ( result[0].status_extended - == f"VM {vm_name} in subscription {AZURE_SUBSCRIPTION_ID} is not protected by Azure Backup." + == f"VM {vm_name} in subscription {AZURE_SUBSCRIPTION_DISPLAY} is not protected by Azure Backup." ) def test_vm_protected_by_backup_case_insensitive(self): @@ -227,7 +243,11 @@ class Test_vm_backup_enabled: vault_id = str(uuid4()) vault_name = "vault1" mock_vm_client = mock.MagicMock() + mock_vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} mock_recovery_client = mock.MagicMock() + mock_recovery_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } with ( mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -297,7 +317,7 @@ class Test_vm_backup_enabled: assert result[0].resource_id == vm_id assert ( result[0].status_extended - == f"VM {vm_name} in subscription {AZURE_SUBSCRIPTION_ID} is protected by Azure Backup (vault: {vault_name})." + == f"VM {vm_name} in subscription {AZURE_SUBSCRIPTION_DISPLAY} is protected by Azure Backup (vault: {vault_name})." ) def test_vm_protected_by_backup_non_vm_workload(self): @@ -306,7 +326,11 @@ class Test_vm_backup_enabled: vault_id = str(uuid4()) vault_name = "vault1" mock_vm_client = mock.MagicMock() + mock_vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} mock_recovery_client = mock.MagicMock() + mock_recovery_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } with ( mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -376,5 +400,5 @@ class Test_vm_backup_enabled: assert result[0].resource_id == vm_id assert ( result[0].status_extended - == f"VM {vm_name} in subscription {AZURE_SUBSCRIPTION_ID} is not protected by Azure Backup." + == f"VM {vm_name} in subscription {AZURE_SUBSCRIPTION_DISPLAY} is not protected by Azure Backup." ) diff --git a/tests/providers/azure/services/vm/vm_desired_sku_size/vm_desired_sku_size_test.py b/tests/providers/azure/services/vm/vm_desired_sku_size/vm_desired_sku_size_test.py index 26f548bbc1..ca86d36b0e 100644 --- a/tests/providers/azure/services/vm/vm_desired_sku_size/vm_desired_sku_size_test.py +++ b/tests/providers/azure/services/vm/vm_desired_sku_size/vm_desired_sku_size_test.py @@ -8,7 +8,9 @@ from prowler.providers.azure.services.vm.vm_service import ( VirtualMachine, ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -17,6 +19,7 @@ class Test_vm_desired_sku_size: def test_vm_no_subscriptions(self): """Test when there are no subscriptions.""" vm_client = mock.MagicMock + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = {} vm_client.audit_config = {} @@ -41,6 +44,7 @@ class Test_vm_desired_sku_size: def test_vm_subscriptions_empty(self): """Test when subscriptions exist but have no VMs.""" vm_client = mock.MagicMock + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {}} vm_client.audit_config = {} @@ -66,6 +70,7 @@ class Test_vm_desired_sku_size: """Test VM using a SKU size that is in the default configuration.""" vm_id = str(uuid4()) vm_client = mock.MagicMock + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = { AZURE_SUBSCRIPTION_ID: { vm_id: VirtualMachine( @@ -113,13 +118,14 @@ class Test_vm_desired_sku_size: assert result[0].resource_id == vm_id assert ( result[0].status_extended - == f"VM VMTest is using desired SKU size Standard_A8_v2 in subscription {AZURE_SUBSCRIPTION_ID}." + == f"VM VMTest is using desired SKU size Standard_A8_v2 in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) def test_vm_using_desired_sku_size_custom_config(self): """Test VM using a SKU size that is in the custom configuration.""" vm_id = str(uuid4()) vm_client = mock.MagicMock + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = { AZURE_SUBSCRIPTION_ID: { vm_id: VirtualMachine( @@ -169,13 +175,14 @@ class Test_vm_desired_sku_size: assert result[0].resource_id == vm_id assert ( result[0].status_extended - == f"VM VMTest is using desired SKU size Standard_B1s in subscription {AZURE_SUBSCRIPTION_ID}." + == f"VM VMTest is using desired SKU size Standard_B1s in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) def test_vm_using_non_desired_sku_size_default_config(self): """Test VM using a SKU size that is not in the default configuration.""" vm_id = str(uuid4()) vm_client = mock.MagicMock + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = { AZURE_SUBSCRIPTION_ID: { vm_id: VirtualMachine( @@ -223,13 +230,14 @@ class Test_vm_desired_sku_size: assert result[0].resource_id == vm_id assert ( result[0].status_extended - == f"VM VMTest is using Standard_B1s which is not a desired SKU size in subscription {AZURE_SUBSCRIPTION_ID}." + == f"VM VMTest is using Standard_B1s which is not a desired SKU size in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) def test_vm_using_non_desired_sku_size_custom_config(self): """Test VM using a SKU size that is not in the custom configuration.""" vm_id = str(uuid4()) vm_client = mock.MagicMock + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = { AZURE_SUBSCRIPTION_ID: { vm_id: VirtualMachine( @@ -279,13 +287,14 @@ class Test_vm_desired_sku_size: assert result[0].resource_id == vm_id assert ( result[0].status_extended - == f"VM VMTest is using Standard_A8_v2 which is not a desired SKU size in subscription {AZURE_SUBSCRIPTION_ID}." + == f"VM VMTest is using Standard_A8_v2 which is not a desired SKU size in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) def test_vm_with_none_vm_size(self): """Test VM with None vm_size.""" vm_id = str(uuid4()) vm_client = mock.MagicMock + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = { AZURE_SUBSCRIPTION_ID: { vm_id: VirtualMachine( @@ -333,7 +342,7 @@ class Test_vm_desired_sku_size: assert result[0].resource_id == vm_id assert ( result[0].status_extended - == f"VM VMTest is using None which is not a desired SKU size in subscription {AZURE_SUBSCRIPTION_ID}." + == f"VM VMTest is using None which is not a desired SKU size in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) def test_multiple_vms_different_statuses(self): @@ -343,6 +352,7 @@ class Test_vm_desired_sku_size: vm_id_3 = str(uuid4()) vm_client = mock.MagicMock + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = { AZURE_SUBSCRIPTION_ID: { vm_id_1: VirtualMachine( @@ -433,7 +443,7 @@ class Test_vm_desired_sku_size: assert pass_result.resource_id == vm_id_1 assert ( pass_result.status_extended - == f"VM VMApproved is using desired SKU size Standard_A8_v2 in subscription {AZURE_SUBSCRIPTION_ID}." + == f"VM VMApproved is using desired SKU size Standard_A8_v2 in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) # Find the FAIL result @@ -446,7 +456,7 @@ class Test_vm_desired_sku_size: assert fail_result.resource_id == vm_id_2 assert ( fail_result.status_extended - == f"VM VMNotApproved is using Standard_B1s which is not a desired SKU size in subscription {AZURE_SUBSCRIPTION_ID}." + == f"VM VMNotApproved is using Standard_B1s which is not a desired SKU size in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) # Find the second PASS result @@ -459,7 +469,7 @@ class Test_vm_desired_sku_size: assert pass_result_2.resource_id == vm_id_3 assert ( pass_result_2.status_extended - == f"VM VMAnotherApproved is using desired SKU size Standard_DS3_v2 in subscription {AZURE_SUBSCRIPTION_ID}." + == f"VM VMAnotherApproved is using desired SKU size Standard_DS3_v2 in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) def test_multiple_subscriptions(self): @@ -469,6 +479,7 @@ class Test_vm_desired_sku_size: subscription_2 = "subscription-2" vm_client = mock.MagicMock + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = { AZURE_SUBSCRIPTION_ID: { vm_id_1: VirtualMachine( @@ -553,6 +564,7 @@ class Test_vm_desired_sku_size: """Test when the desired SKU sizes configuration is empty.""" vm_id = str(uuid4()) vm_client = mock.MagicMock + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = { AZURE_SUBSCRIPTION_ID: { vm_id: VirtualMachine( @@ -600,13 +612,14 @@ class Test_vm_desired_sku_size: assert result[0].resource_id == vm_id assert ( result[0].status_extended - == f"VM VMTest is using Standard_A8_v2 which is not a desired SKU size in subscription {AZURE_SUBSCRIPTION_ID}." + == f"VM VMTest is using Standard_A8_v2 which is not a desired SKU size in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) def test_case_sensitive_sku_size_matching(self): """Test that SKU size matching is case sensitive.""" vm_id = str(uuid4()) vm_client = mock.MagicMock + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = { AZURE_SUBSCRIPTION_ID: { vm_id: VirtualMachine( @@ -656,5 +669,5 @@ class Test_vm_desired_sku_size: assert result[0].resource_id == vm_id assert ( result[0].status_extended - == f"VM VMTest is using standard_a8_v2 which is not a desired SKU size in subscription {AZURE_SUBSCRIPTION_ID}." + == f"VM VMTest is using standard_a8_v2 which is not a desired SKU size in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) diff --git a/tests/providers/azure/services/vm/vm_ensure_attached_disks_encrypted_with_cmk/vm_ensure_attached_disks_encrypted_with_cmk_test.py b/tests/providers/azure/services/vm/vm_ensure_attached_disks_encrypted_with_cmk/vm_ensure_attached_disks_encrypted_with_cmk_test.py index 1eb8da64c4..98b14125bf 100644 --- a/tests/providers/azure/services/vm/vm_ensure_attached_disks_encrypted_with_cmk/vm_ensure_attached_disks_encrypted_with_cmk_test.py +++ b/tests/providers/azure/services/vm/vm_ensure_attached_disks_encrypted_with_cmk/vm_ensure_attached_disks_encrypted_with_cmk_test.py @@ -3,7 +3,9 @@ from uuid import uuid4 from prowler.providers.azure.services.vm.vm_service import Disk from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_vm_ensure_attached_disks_encrypted_with_cmk: def test_vm_no_subscriptions(self): vm_client = mock.MagicMock + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.disks = {} with ( @@ -33,6 +36,7 @@ class Test_vm_ensure_attached_disks_encrypted_with_cmk: def test_vm_subscription_empty(self): vm_client = mock.MagicMock + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.disks = {AZURE_SUBSCRIPTION_ID: {}} with ( @@ -57,6 +61,7 @@ class Test_vm_ensure_attached_disks_encrypted_with_cmk: disk_id = str(uuid4()) resource_id = str(uuid4()) vm_client = mock.MagicMock + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.disks = { AZURE_SUBSCRIPTION_ID: { disk_id: Disk( @@ -93,13 +98,14 @@ class Test_vm_ensure_attached_disks_encrypted_with_cmk: assert result[0].location == "location" assert ( result[0].status_extended - == f"Disk '{disk_id}' is not encrypted with a customer-managed key in subscription {AZURE_SUBSCRIPTION_ID}." + == f"Disk '{disk_id}' is not encrypted with a customer-managed key in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) def test_vm_subscription_one_disk_attached_encrypt_cmk(self): disk_id = str(uuid4()) resource_id = str(uuid4()) vm_client = mock.MagicMock + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.disks = { AZURE_SUBSCRIPTION_ID: { disk_id: Disk( @@ -136,7 +142,7 @@ class Test_vm_ensure_attached_disks_encrypted_with_cmk: assert result[0].location == "location" assert ( result[0].status_extended - == f"Disk '{disk_id}' is encrypted with a customer-managed key in subscription {AZURE_SUBSCRIPTION_ID}." + == f"Disk '{disk_id}' is encrypted with a customer-managed key in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) def test_vm_subscription_two_disk_attached_encrypt_cmk_and_pk(self): @@ -145,6 +151,7 @@ class Test_vm_ensure_attached_disks_encrypted_with_cmk: disk_id_2 = str(uuid4()) resource_id_2 = str(uuid4()) vm_client = mock.MagicMock + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.disks = { AZURE_SUBSCRIPTION_ID: { disk_id_1: Disk( @@ -188,7 +195,7 @@ class Test_vm_ensure_attached_disks_encrypted_with_cmk: assert result[0].location == "location" assert ( result[0].status_extended - == f"Disk '{disk_id_1}' is not encrypted with a customer-managed key in subscription {AZURE_SUBSCRIPTION_ID}." + == f"Disk '{disk_id_1}' is not encrypted with a customer-managed key in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) assert result[1].status == "PASS" assert result[1].resource_id == resource_id_2 @@ -196,13 +203,14 @@ class Test_vm_ensure_attached_disks_encrypted_with_cmk: assert result[1].location == "location2" assert ( result[1].status_extended - == f"Disk '{disk_id_2}' is encrypted with a customer-managed key in subscription {AZURE_SUBSCRIPTION_ID}." + == f"Disk '{disk_id_2}' is encrypted with a customer-managed key in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) def test_vm_unattached_disk_encrypt_cmk(self): disk_id = str(uuid4()) resource_id = str(uuid4()) vm_client = mock.MagicMock + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.disks = { AZURE_SUBSCRIPTION_ID: { disk_id: Disk( diff --git a/tests/providers/azure/services/vm/vm_ensure_unattached_disks_encrypted_with_cmk/vm_ensure_unattached_disks_encrypted_with_cmk_test.py b/tests/providers/azure/services/vm/vm_ensure_unattached_disks_encrypted_with_cmk/vm_ensure_unattached_disks_encrypted_with_cmk_test.py index 78d7920666..1ac8b72500 100644 --- a/tests/providers/azure/services/vm/vm_ensure_unattached_disks_encrypted_with_cmk/vm_ensure_unattached_disks_encrypted_with_cmk_test.py +++ b/tests/providers/azure/services/vm/vm_ensure_unattached_disks_encrypted_with_cmk/vm_ensure_unattached_disks_encrypted_with_cmk_test.py @@ -3,7 +3,9 @@ from uuid import uuid4 from prowler.providers.azure.services.vm.vm_service import Disk from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_vm_ensure_unattached_disks_encrypted_with_cmk: def test_vm_no_subscriptions(self): vm_client = mock.MagicMock + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.disks = {} with ( @@ -33,6 +36,7 @@ class Test_vm_ensure_unattached_disks_encrypted_with_cmk: def test_vm_subscription_empty(self): vm_client = mock.MagicMock + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.disks = {AZURE_SUBSCRIPTION_ID: {}} with ( @@ -57,6 +61,7 @@ class Test_vm_ensure_unattached_disks_encrypted_with_cmk: disk_id = str(uuid4()) resource_id = str(uuid4()) vm_client = mock.MagicMock + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.disks = { AZURE_SUBSCRIPTION_ID: { disk_id: Disk( @@ -93,13 +98,14 @@ class Test_vm_ensure_unattached_disks_encrypted_with_cmk: assert result[0].location == "location" assert ( result[0].status_extended - == f"Disk '{disk_id}' is not encrypted with a customer-managed key in subscription {AZURE_SUBSCRIPTION_ID}." + == f"Disk '{disk_id}' is not encrypted with a customer-managed key in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) def test_vm_one_unattached_disk_encrypt_cmk(self): disk_id = str(uuid4()) resource_id = str(uuid4()) vm_client = mock.MagicMock + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.disks = { AZURE_SUBSCRIPTION_ID: { disk_id: Disk( @@ -136,7 +142,7 @@ class Test_vm_ensure_unattached_disks_encrypted_with_cmk: assert result[0].location == "location" assert ( result[0].status_extended - == f"Disk '{disk_id}' is encrypted with a customer-managed key in subscription {AZURE_SUBSCRIPTION_ID}." + == f"Disk '{disk_id}' is encrypted with a customer-managed key in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) def test_vm_subscription_two_unattached_disk_encrypt_cmk_and_pk(self): @@ -145,6 +151,7 @@ class Test_vm_ensure_unattached_disks_encrypted_with_cmk: disk_id_2 = str(uuid4()) resource_id_2 = str(uuid4()) vm_client = mock.MagicMock + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.disks = { AZURE_SUBSCRIPTION_ID: { disk_id_1: Disk( @@ -188,7 +195,7 @@ class Test_vm_ensure_unattached_disks_encrypted_with_cmk: assert result[0].location == "location" assert ( result[0].status_extended - == f"Disk '{disk_id_1}' is not encrypted with a customer-managed key in subscription {AZURE_SUBSCRIPTION_ID}." + == f"Disk '{disk_id_1}' is not encrypted with a customer-managed key in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) assert result[1].status == "PASS" assert result[1].resource_id == resource_id_2 @@ -196,13 +203,14 @@ class Test_vm_ensure_unattached_disks_encrypted_with_cmk: assert result[1].location == "location2" assert ( result[1].status_extended - == f"Disk '{disk_id_2}' is encrypted with a customer-managed key in subscription {AZURE_SUBSCRIPTION_ID}." + == f"Disk '{disk_id_2}' is encrypted with a customer-managed key in subscription {AZURE_SUBSCRIPTION_DISPLAY}." ) def test_vm_attached_disk_encrypt_cmk(self): disk_id = str(uuid4()) resource_id = str(uuid4()) vm_client = mock.MagicMock + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.disks = { AZURE_SUBSCRIPTION_ID: { disk_id: Disk( diff --git a/tests/providers/azure/services/vm/vm_ensure_using_approved_images/vm_ensure_using_approved_images_test.py b/tests/providers/azure/services/vm/vm_ensure_using_approved_images/vm_ensure_using_approved_images_test.py index 035ec5db3b..582e952374 100644 --- a/tests/providers/azure/services/vm/vm_ensure_using_approved_images/vm_ensure_using_approved_images_test.py +++ b/tests/providers/azure/services/vm/vm_ensure_using_approved_images/vm_ensure_using_approved_images_test.py @@ -3,7 +3,9 @@ from uuid import uuid4 from prowler.providers.azure.services.vm.vm_service import VirtualMachine from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -11,6 +13,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_vm_ensure_using_approved_images: def test_no_subscriptions(self): vm_client = mock.MagicMock() + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = {} with ( mock.patch( @@ -32,6 +35,7 @@ class Test_vm_ensure_using_approved_images: def test_empty_vms_in_subscription(self): vm_client = mock.MagicMock() + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {}} with ( mock.patch( @@ -64,6 +68,7 @@ class Test_vm_ensure_using_approved_images: image_reference=approved_image_id, ) vm_client = mock.MagicMock() + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {vm_id: vm}} with ( mock.patch( @@ -86,7 +91,7 @@ class Test_vm_ensure_using_approved_images: assert result[0].resource_name == "VMTestApproved" assert result[0].resource_id == vm_id assert result[0].subscription == AZURE_SUBSCRIPTION_ID - expected_status_extended = f"VM VMTestApproved in subscription {AZURE_SUBSCRIPTION_ID} is using an approved machine image: custom-image." + expected_status_extended = f"VM VMTestApproved in subscription {AZURE_SUBSCRIPTION_DISPLAY} is using an approved machine image: custom-image." assert result[0].status_extended == expected_status_extended def test_vm_with_not_approved_image(self): @@ -102,6 +107,7 @@ class Test_vm_ensure_using_approved_images: image_reference=not_approved_image_id, ) vm_client = mock.MagicMock() + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {vm_id: vm}} with ( mock.patch( @@ -124,7 +130,7 @@ class Test_vm_ensure_using_approved_images: assert result[0].resource_name == "VMTestNotApproved" assert result[0].resource_id == vm_id assert result[0].subscription == AZURE_SUBSCRIPTION_ID - expected_status_extended = f"VM VMTestNotApproved in subscription {AZURE_SUBSCRIPTION_ID} is not using an approved machine image." + expected_status_extended = f"VM VMTestNotApproved in subscription {AZURE_SUBSCRIPTION_DISPLAY} is not using an approved machine image." assert result[0].status_extended == expected_status_extended def test_vm_with_missing_image_reference(self): @@ -139,6 +145,7 @@ class Test_vm_ensure_using_approved_images: image_reference=None, ) vm_client = mock.MagicMock() + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {vm_id: vm}} with ( mock.patch( @@ -161,5 +168,5 @@ class Test_vm_ensure_using_approved_images: assert result[0].resource_name == "VMTestNoImageRef" assert result[0].resource_id == vm_id assert result[0].subscription == AZURE_SUBSCRIPTION_ID - expected_status_extended = f"VM VMTestNoImageRef in subscription {AZURE_SUBSCRIPTION_ID} is not using an approved machine image." + expected_status_extended = f"VM VMTestNoImageRef in subscription {AZURE_SUBSCRIPTION_DISPLAY} is not using an approved machine image." assert result[0].status_extended == expected_status_extended diff --git a/tests/providers/azure/services/vm/vm_ensure_using_managed_disks/vm_ensure_using_managed_disks_test.py b/tests/providers/azure/services/vm/vm_ensure_using_managed_disks/vm_ensure_using_managed_disks_test.py index 3c494d861c..46b15ac994 100644 --- a/tests/providers/azure/services/vm/vm_ensure_using_managed_disks/vm_ensure_using_managed_disks_test.py +++ b/tests/providers/azure/services/vm/vm_ensure_using_managed_disks/vm_ensure_using_managed_disks_test.py @@ -11,7 +11,9 @@ from prowler.providers.azure.services.vm.vm_service import ( VirtualMachine, ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -19,6 +21,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_vm_ensure_using_managed_disks: def test_vm_no_subscriptions(self): vm_client = mock.MagicMock + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = {} with ( @@ -41,6 +44,7 @@ class Test_vm_ensure_using_managed_disks: def test_vm_subscriptions(self): vm_client = mock.MagicMock + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {}} with ( @@ -64,6 +68,7 @@ class Test_vm_ensure_using_managed_disks: def test_vm_ensure_using_managed_disks(self): vm_id = str(uuid4()) vm_client = mock.MagicMock + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = { AZURE_SUBSCRIPTION_ID: { vm_id: VirtualMachine( @@ -115,12 +120,13 @@ class Test_vm_ensure_using_managed_disks: assert result[0].resource_id == vm_id assert ( result[0].status_extended - == f"VM VMTest is using managed disks in subscription {AZURE_SUBSCRIPTION_ID}" + == f"VM VMTest is using managed disks in subscription {AZURE_SUBSCRIPTION_DISPLAY}" ) def test_vm_using_not_managed_os_disk(self): vm_id = str(uuid4()) vm_client = mock.MagicMock + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = { AZURE_SUBSCRIPTION_ID: { vm_id: VirtualMachine( @@ -172,12 +178,13 @@ class Test_vm_ensure_using_managed_disks: assert result[0].location == "location" assert ( result[0].status_extended - == f"VM VMTest is not using managed disks in subscription {AZURE_SUBSCRIPTION_ID}" + == f"VM VMTest is not using managed disks in subscription {AZURE_SUBSCRIPTION_DISPLAY}" ) def test_vm_using_not_managed_data_disks(self): vm_id = str(uuid4()) vm_client = mock.MagicMock + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = { AZURE_SUBSCRIPTION_ID: { vm_id: VirtualMachine( @@ -231,5 +238,5 @@ class Test_vm_ensure_using_managed_disks: assert result[0].location == "location" assert ( result[0].status_extended - == f"VM VMTest is not using managed disks in subscription {AZURE_SUBSCRIPTION_ID}" + == f"VM VMTest is not using managed disks in subscription {AZURE_SUBSCRIPTION_DISPLAY}" ) diff --git a/tests/providers/azure/services/vm/vm_jit_access_enabled/vm_jit_access_enabled_test.py b/tests/providers/azure/services/vm/vm_jit_access_enabled/vm_jit_access_enabled_test.py index 03991f8049..eb37546cec 100644 --- a/tests/providers/azure/services/vm/vm_jit_access_enabled/vm_jit_access_enabled_test.py +++ b/tests/providers/azure/services/vm/vm_jit_access_enabled/vm_jit_access_enabled_test.py @@ -5,6 +5,7 @@ from prowler.providers.azure.services.defender.defender_service import JITPolicy from prowler.providers.azure.services.vm.vm_service import VirtualMachine from tests.providers.azure.azure_fixtures import ( AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -12,8 +13,10 @@ from tests.providers.azure.azure_fixtures import ( class Test_vm_jit_access_enabled: def test_no_subscriptions(self): vm_client = mock.MagicMock() + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = {} defender_client = mock.MagicMock() + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.jit_policies = {} with ( mock.patch( @@ -39,8 +42,10 @@ class Test_vm_jit_access_enabled: def test_no_vms(self): vm_client = mock.MagicMock() + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {}} defender_client = mock.MagicMock() + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} defender_client.jit_policies = {AZURE_SUBSCRIPTION_ID: {}} with ( mock.patch( @@ -77,8 +82,10 @@ class Test_vm_jit_access_enabled: storage_profile=None, ) vm_client = mock.MagicMock() + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {vm_id: vm}} defender_client = mock.MagicMock() + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} jit_policy = JITPolicy( id="policy1", name="JITPolicy1", @@ -128,8 +135,10 @@ class Test_vm_jit_access_enabled: storage_profile=None, ) vm_client = mock.MagicMock() + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {vm_id: vm}} defender_client = mock.MagicMock() + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} # JIT policy does not include this VM jit_policy = JITPolicy( id="policy1", @@ -184,8 +193,10 @@ class Test_vm_jit_access_enabled: storage_profile=None, ) vm_client = mock.MagicMock() + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {upper_vm_id: vm}} defender_client = mock.MagicMock() + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} jit_policy = JITPolicy( id="policy1", name="JITPolicy1", @@ -240,10 +251,12 @@ class Test_vm_jit_access_enabled: storage_profile=None, ) vm_client = mock.MagicMock() + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = { AZURE_SUBSCRIPTION_ID: {vm_id_1: vm1, vm_id_2: vm2} } defender_client = mock.MagicMock() + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} jit_policy_1 = JITPolicy( id="policy1", name="JITPolicy1", diff --git a/tests/providers/azure/services/vm/vm_linux_enforce_ssh_authentication/vm_linux_enforce_ssh_authentication_test.py b/tests/providers/azure/services/vm/vm_linux_enforce_ssh_authentication/vm_linux_enforce_ssh_authentication_test.py index 5d400ac7bf..428c6adc85 100644 --- a/tests/providers/azure/services/vm/vm_linux_enforce_ssh_authentication/vm_linux_enforce_ssh_authentication_test.py +++ b/tests/providers/azure/services/vm/vm_linux_enforce_ssh_authentication/vm_linux_enforce_ssh_authentication_test.py @@ -7,6 +7,7 @@ from prowler.providers.azure.services.vm.vm_service import ( ) from tests.providers.azure.azure_fixtures import ( AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -14,6 +15,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_vm_linux_enforce_ssh_authentication: def test_no_subscriptions(self): vm_client = mock.MagicMock + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = {} with ( @@ -36,6 +38,7 @@ class Test_vm_linux_enforce_ssh_authentication: def test_empty_subscription(self): vm_client = mock.MagicMock + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {}} with ( @@ -59,6 +62,7 @@ class Test_vm_linux_enforce_ssh_authentication: def test_linux_vm_password_auth_disabled(self): vm_id = str(uuid4()) vm_client = mock.MagicMock + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = { AZURE_SUBSCRIPTION_ID: { vm_id: VirtualMachine( @@ -100,6 +104,7 @@ class Test_vm_linux_enforce_ssh_authentication: def test_linux_vm_password_auth_enabled(self): vm_id = str(uuid4()) vm_client = mock.MagicMock + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = { AZURE_SUBSCRIPTION_ID: { vm_id: VirtualMachine( @@ -141,6 +146,7 @@ class Test_vm_linux_enforce_ssh_authentication: def test_non_linux_vm(self): vm_id = str(uuid4()) vm_client = mock.MagicMock + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = { AZURE_SUBSCRIPTION_ID: { vm_id: VirtualMachine( diff --git a/tests/providers/azure/services/vm/vm_scaleset_associated_with_load_balancer/vm_scaleset_associated_with_load_balancer_test.py b/tests/providers/azure/services/vm/vm_scaleset_associated_with_load_balancer/vm_scaleset_associated_with_load_balancer_test.py index 532e1b0b63..22dd59ce27 100644 --- a/tests/providers/azure/services/vm/vm_scaleset_associated_with_load_balancer/vm_scaleset_associated_with_load_balancer_test.py +++ b/tests/providers/azure/services/vm/vm_scaleset_associated_with_load_balancer/vm_scaleset_associated_with_load_balancer_test.py @@ -3,6 +3,7 @@ from uuid import uuid4 from prowler.providers.azure.services.vm.vm_service import VirtualMachineScaleSet from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, set_mocked_azure_provider, ) @@ -85,7 +86,7 @@ class Test_vm_scaleset_associated_with_load_balancer: assert result[0].resource_name == "compliant-vmss" assert result[0].location == "eastus" expected_status_extended = ( - f"Scale set 'compliant-vmss' in subscription '{AZURE_SUBSCRIPTION_ID}' " + f"Scale set 'compliant-vmss' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}' " f"is associated with load balancer backend pool(s): bepool." ) assert result[0].status_extended == expected_status_extended @@ -125,7 +126,7 @@ class Test_vm_scaleset_associated_with_load_balancer: assert result[0].resource_name == "noncompliant-vmss" assert result[0].location == "westeurope" expected_status_extended = ( - f"Scale set 'noncompliant-vmss' in subscription '{AZURE_SUBSCRIPTION_ID}' " + f"Scale set 'noncompliant-vmss' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}' " f"is not associated with any load balancer backend pool." ) assert result[0].status_extended == expected_status_extended @@ -172,14 +173,14 @@ class Test_vm_scaleset_associated_with_load_balancer: for r in result: if r.resource_name == "compliant-vmss": expected_status_extended = ( - f"Scale set 'compliant-vmss' in subscription '{AZURE_SUBSCRIPTION_ID}' " + f"Scale set 'compliant-vmss' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}' " f"is associated with load balancer backend pool(s): bepool." ) assert r.status == "PASS" assert r.status_extended == expected_status_extended elif r.resource_name == "noncompliant-vmss": expected_status_extended = ( - f"Scale set 'noncompliant-vmss' in subscription '{AZURE_SUBSCRIPTION_ID}' " + f"Scale set 'noncompliant-vmss' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}' " f"is not associated with any load balancer backend pool." ) assert r.status == "FAIL" @@ -216,6 +217,6 @@ class Test_vm_scaleset_associated_with_load_balancer: check = vm_scaleset_associated_with_load_balancer() result = check.execute() assert len(result) == 1 - expected_status_extended = f"Scale set '' in subscription '{AZURE_SUBSCRIPTION_ID}' is not associated with any load balancer backend pool." + expected_status_extended = f"Scale set '' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}' is not associated with any load balancer backend pool." assert result[0].status == "FAIL" assert result[0].status_extended == expected_status_extended diff --git a/tests/providers/azure/services/vm/vm_scaleset_not_empty/vm_scaleset_not_empty_test.py b/tests/providers/azure/services/vm/vm_scaleset_not_empty/vm_scaleset_not_empty_test.py index 27d36f0697..6d28175066 100644 --- a/tests/providers/azure/services/vm/vm_scaleset_not_empty/vm_scaleset_not_empty_test.py +++ b/tests/providers/azure/services/vm/vm_scaleset_not_empty/vm_scaleset_not_empty_test.py @@ -3,6 +3,7 @@ from uuid import uuid4 from prowler.providers.azure.services.vm.vm_service import VirtualMachineScaleSet from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, set_mocked_azure_provider, ) @@ -83,7 +84,7 @@ class Test_vm_scaleset_not_empty: assert result[0].resource_id == vmss_id assert result[0].resource_name == "empty-vmss" assert result[0].location == "eastus" - expected_status_extended = f"Scale set 'empty-vmss' in subscription '{AZURE_SUBSCRIPTION_ID}' is empty: no VM instances present." + expected_status_extended = f"Scale set 'empty-vmss' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}' is empty: no VM instances present." assert result[0].status_extended == expected_status_extended def test_scale_set_with_instances(self): @@ -121,7 +122,7 @@ class Test_vm_scaleset_not_empty: assert result[0].resource_id == vmss_id assert result[0].resource_name == "nonempty-vmss" assert result[0].location == "westeurope" - expected_status_extended = f"Scale set 'nonempty-vmss' in subscription '{AZURE_SUBSCRIPTION_ID}' has {len(instance_ids)} VM instances." + expected_status_extended = f"Scale set 'nonempty-vmss' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}' has {len(instance_ids)} VM instances." assert result[0].status_extended == expected_status_extended def test_multiple_scale_sets(self): @@ -165,10 +166,10 @@ class Test_vm_scaleset_not_empty: assert len(result) == 2 for r in result: if r.resource_name == "empty-vmss": - expected_status_extended = f"Scale set 'empty-vmss' in subscription '{AZURE_SUBSCRIPTION_ID}' is empty: no VM instances present." + expected_status_extended = f"Scale set 'empty-vmss' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}' is empty: no VM instances present." assert r.status == "FAIL" assert r.status_extended == expected_status_extended elif r.resource_name == "nonempty-vmss": - expected_status_extended = f"Scale set 'nonempty-vmss' in subscription '{AZURE_SUBSCRIPTION_ID}' has {len(instance_ids)} VM instances." + expected_status_extended = f"Scale set 'nonempty-vmss' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}' has {len(instance_ids)} VM instances." assert r.status == "PASS" assert r.status_extended == expected_status_extended diff --git a/tests/providers/azure/services/vm/vm_sufficient_daily_backup_retention_period/vm_sufficient_daily_backup_retention_period_test.py b/tests/providers/azure/services/vm/vm_sufficient_daily_backup_retention_period/vm_sufficient_daily_backup_retention_period_test.py index 28aab1b38b..70b1cf638f 100644 --- a/tests/providers/azure/services/vm/vm_sufficient_daily_backup_retention_period/vm_sufficient_daily_backup_retention_period_test.py +++ b/tests/providers/azure/services/vm/vm_sufficient_daily_backup_retention_period/vm_sufficient_daily_backup_retention_period_test.py @@ -3,6 +3,7 @@ from uuid import uuid4 from tests.providers.azure.azure_fixtures import ( AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -10,7 +11,9 @@ from tests.providers.azure.azure_fixtures import ( class Test_vm_sufficient_daily_backup_retention_period: def test_no_subscriptions(self): vm_client = mock.MagicMock() + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} recovery_client = mock.MagicMock() + recovery_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = {} recovery_client.vaults = {} with ( @@ -37,7 +40,9 @@ class Test_vm_sufficient_daily_backup_retention_period: def test_no_vms(self): vm_client = mock.MagicMock() + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} recovery_client = mock.MagicMock() + recovery_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {}} recovery_client.vaults = {AZURE_SUBSCRIPTION_ID: {}} with ( @@ -118,7 +123,9 @@ class Test_vm_sufficient_daily_backup_retention_period: backup_policies={policy_id: backup_policy}, ) vm_client = mock.MagicMock() + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} recovery_client = mock.MagicMock() + recovery_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {vm_id: vm}} recovery_client.vaults = {AZURE_SUBSCRIPTION_ID: {vault_id: vault}} vm_client.audit_config = { @@ -212,7 +219,9 @@ class Test_vm_sufficient_daily_backup_retention_period: backup_policies={policy_id: backup_policy}, ) vm_client = mock.MagicMock() + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} recovery_client = mock.MagicMock() + recovery_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {vm_id: vm}} recovery_client.vaults = {AZURE_SUBSCRIPTION_ID: {vault_id: vault}} vm_client.audit_config = { @@ -306,7 +315,9 @@ class Test_vm_sufficient_daily_backup_retention_period: backup_policies={policy_id: backup_policy}, ) vm_client = mock.MagicMock() + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} recovery_client = mock.MagicMock() + recovery_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {vm_id: vm}} recovery_client.vaults = {AZURE_SUBSCRIPTION_ID: {vault_id: vault}} vm_client.audit_config = { @@ -391,7 +402,9 @@ class Test_vm_sufficient_daily_backup_retention_period: backup_policies={}, ) vm_client = mock.MagicMock() + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} recovery_client = mock.MagicMock() + recovery_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {vm_id: vm}} recovery_client.vaults = {AZURE_SUBSCRIPTION_ID: {vault_id: vault}} with ( diff --git a/tests/providers/azure/services/vm/vm_trusted_launch_enabled/vm_trusted_launch_enabled_test.py b/tests/providers/azure/services/vm/vm_trusted_launch_enabled/vm_trusted_launch_enabled_test.py index 83ab63acce..364267fbed 100644 --- a/tests/providers/azure/services/vm/vm_trusted_launch_enabled/vm_trusted_launch_enabled_test.py +++ b/tests/providers/azure/services/vm/vm_trusted_launch_enabled/vm_trusted_launch_enabled_test.py @@ -10,7 +10,9 @@ from prowler.providers.azure.services.vm.vm_service import ( VirtualMachine, ) from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, set_mocked_azure_provider, ) @@ -18,6 +20,7 @@ from tests.providers.azure.azure_fixtures import ( class Test_vm_trusted_launch_enabled: def test_vm_no_subscriptions(self): vm_client = mock.MagicMock + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = {} with ( mock.patch( @@ -39,6 +42,7 @@ class Test_vm_trusted_launch_enabled: def test_vm_no_vm(self): vm_client = mock.MagicMock + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {}} with ( mock.patch( @@ -61,6 +65,7 @@ class Test_vm_trusted_launch_enabled: def test_vm_trusted_launch_enabled(self): vm_id = str(uuid4()) vm_client = mock.MagicMock + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -111,12 +116,13 @@ class Test_vm_trusted_launch_enabled: assert result[0].resource_id == vm_id assert ( result[0].status_extended - == f"VM VMTest has trusted launch enabled in subscription {AZURE_SUBSCRIPTION_ID}" + == f"VM VMTest has trusted launch enabled in subscription {AZURE_SUBSCRIPTION_DISPLAY}" ) def test_vm_trusted_launch_disabled(self): vm_id = str(uuid4()) vm_client = mock.MagicMock + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -168,12 +174,13 @@ class Test_vm_trusted_launch_enabled: assert result[0].resource_id == vm_id assert ( result[0].status_extended - == f"VM VMTest has trusted launch disabled in subscription {AZURE_SUBSCRIPTION_ID}" + == f"VM VMTest has trusted launch disabled in subscription {AZURE_SUBSCRIPTION_DISPLAY}" ) def test_vm_no_security_profile(self): vm_id = str(uuid4()) vm_client = mock.MagicMock + vm_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} with ( mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -219,5 +226,5 @@ class Test_vm_trusted_launch_enabled: assert result[0].resource_id == vm_id assert ( result[0].status_extended - == f"VM VMTest has trusted launch disabled in subscription {AZURE_SUBSCRIPTION_ID}" + == f"VM VMTest has trusted launch disabled in subscription {AZURE_SUBSCRIPTION_DISPLAY}" ) diff --git a/tests/providers/cloudflare/services/zone/zone_waf_enabled/zone_waf_enabled_test.py b/tests/providers/cloudflare/services/zone/zone_waf_enabled/zone_waf_enabled_test.py index 0d7d91bc5f..6bd3aace97 100644 --- a/tests/providers/cloudflare/services/zone/zone_waf_enabled/zone_waf_enabled_test.py +++ b/tests/providers/cloudflare/services/zone/zone_waf_enabled/zone_waf_enabled_test.py @@ -136,3 +136,79 @@ class Test_zone_waf_enabled: result = check.execute() assert len(result) == 1 assert result[0].status == "FAIL" + + def test_zone_waf_disabled_paid_plan_includes_hint(self): + zone_client = mock.MagicMock + zone_client.zones = { + ZONE_ID: CloudflareZone( + id=ZONE_ID, + name=ZONE_NAME, + status="active", + paused=False, + plan="Pro Website", + settings=CloudflareZoneSettings( + waf="off", + ), + ) + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_cloudflare_provider(), + ), + mock.patch( + "prowler.providers.cloudflare.services.zone.zone_waf_enabled.zone_waf_enabled.zone_client", + new=zone_client, + ), + ): + from prowler.providers.cloudflare.services.zone.zone_waf_enabled.zone_waf_enabled import ( + zone_waf_enabled, + ) + + check = zone_waf_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "WAF is not enabled" in result[0].status_extended + assert "false positive" in result[0].status_extended + assert "Cloudflare dashboard" in result[0].status_extended + + def test_zone_waf_disabled_free_plan_includes_unavailable_hint(self): + zone_client = mock.MagicMock + zone_client.zones = { + ZONE_ID: CloudflareZone( + id=ZONE_ID, + name=ZONE_NAME, + status="active", + paused=False, + plan="Free Website", + settings=CloudflareZoneSettings( + waf="off", + ), + ) + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_cloudflare_provider(), + ), + mock.patch( + "prowler.providers.cloudflare.services.zone.zone_waf_enabled.zone_waf_enabled.zone_client", + new=zone_client, + ), + ): + from prowler.providers.cloudflare.services.zone.zone_waf_enabled.zone_waf_enabled import ( + zone_waf_enabled, + ) + + check = zone_waf_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "WAF is not enabled" in result[0].status_extended + assert "not available on the Cloudflare Free plan" in ( + result[0].status_extended + ) + assert "false positive" not in result[0].status_extended diff --git a/tests/providers/gcp/gcp_fixtures.py b/tests/providers/gcp/gcp_fixtures.py index 99eb88d25b..ba6480ee22 100644 --- a/tests/providers/gcp/gcp_fixtures.py +++ b/tests/providers/gcp/gcp_fixtures.py @@ -41,7 +41,7 @@ def set_mocked_gcp_provider( return provider -def mock_api_client(GCPService, service, api_version, _): +def mock_api_client(_GCPService, service, _api_version, _): client = MagicMock() mock_api_projects_calls(client) @@ -126,7 +126,11 @@ def mock_api_projects_calls(client: MagicMock): "etag": "BwWWja0YfJA=", "version": 3, } - # Used by compute client and cloudresourcemanager + # Used by compute client and cloudresourcemanager. + # `enable-oslogin` covers the documented uppercase form (TRUE); + # `enable-oslogin-2fa` covers the lowercase form (true) that GCP's + # `constraints/compute.requireOsLogin` org-policy controller writes + # in production. The service-layer parser must handle both casings. client.projects().get().execute.return_value = { "projectNumber": "123456789012", "commonInstanceMetadata": { @@ -139,6 +143,10 @@ def mock_api_projects_calls(client: MagicMock): "key": "enable-oslogin", "value": "FALSE", }, + { + "key": "enable-oslogin-2fa", + "value": "true", + }, { "key": "testing-key", "value": "TRUE", @@ -703,6 +711,9 @@ def mock_api_instances_calls(client: MagicMock, service: str): "databaseVersion": "MYSQL_5_7", "region": "us-central1", "ipAddresses": [{"type": "PRIMARY", "ipAddress": "66.66.66.66"}], + "diskEncryptionConfiguration": { + "kmsKeyName": "projects/123/locations/us-central1/keyRings/keyring1/cryptoKeys/key1" + }, "settings": { "ipConfiguration": { "requireSsl": True, @@ -1323,6 +1334,7 @@ def mock_api_images_calls(client: MagicMock): client.images().list_next.return_value = None def mock_get_image_iam_policy(project, resource): + del project return_value = MagicMock() if resource == "test-image-1": return_value.execute.return_value = { diff --git a/tests/providers/gcp/services/cloudsql/cloudsql_instance_cmek_encryption_enabled/cloudsql_instance_cmek_encryption_enabled_test.py b/tests/providers/gcp/services/cloudsql/cloudsql_instance_cmek_encryption_enabled/cloudsql_instance_cmek_encryption_enabled_test.py new file mode 100644 index 0000000000..e2e6e3f9e4 --- /dev/null +++ b/tests/providers/gcp/services/cloudsql/cloudsql_instance_cmek_encryption_enabled/cloudsql_instance_cmek_encryption_enabled_test.py @@ -0,0 +1,277 @@ +from unittest import mock +from unittest.mock import MagicMock, patch + +from tests.providers.gcp.gcp_fixtures import ( + GCP_EU1_LOCATION, + GCP_PROJECT_ID, + mock_is_api_active, + set_mocked_gcp_provider, +) + + +class Test_cloudsql_instance_cmek_encryption_enabled: + def test_no_instances(self): + cloudsql_client = mock.MagicMock() + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.cloudsql.cloudsql_instance_cmek_encryption_enabled.cloudsql_instance_cmek_encryption_enabled.cloudsql_client", + new=cloudsql_client, + ), + ): + from prowler.providers.gcp.services.cloudsql.cloudsql_instance_cmek_encryption_enabled.cloudsql_instance_cmek_encryption_enabled import ( + cloudsql_instance_cmek_encryption_enabled, + ) + + cloudsql_client.instances = [] + check = cloudsql_instance_cmek_encryption_enabled() + result = check.execute() + assert len(result) == 0 + + def test_instance_cmek_enabled(self): + cloudsql_client = mock.MagicMock() + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.cloudsql.cloudsql_instance_cmek_encryption_enabled.cloudsql_instance_cmek_encryption_enabled.cloudsql_client", + new=cloudsql_client, + ), + ): + from prowler.providers.gcp.services.cloudsql.cloudsql_instance_cmek_encryption_enabled.cloudsql_instance_cmek_encryption_enabled import ( + cloudsql_instance_cmek_encryption_enabled, + ) + from prowler.providers.gcp.services.cloudsql.cloudsql_service import ( + Instance, + ) + + cloudsql_client.instances = [ + Instance( + name="db-cmek", + version="POSTGRES_15", + ip_addresses=[], + region=GCP_EU1_LOCATION, + public_ip=False, + require_ssl=False, + ssl_mode="ENCRYPTED_ONLY", + automated_backups=True, + authorized_networks=[], + flags=[], + project_id=GCP_PROJECT_ID, + cmek_key_name="projects/123456789012/locations/europe-west1/keyRings/my-ring/cryptoKeys/my-key", + ) + ] + check = cloudsql_instance_cmek_encryption_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].resource_id == "db-cmek" + assert result[0].location == GCP_EU1_LOCATION + assert result[0].project_id == GCP_PROJECT_ID + + def test_instance_cmek_not_configured(self): + cloudsql_client = mock.MagicMock() + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.cloudsql.cloudsql_instance_cmek_encryption_enabled.cloudsql_instance_cmek_encryption_enabled.cloudsql_client", + new=cloudsql_client, + ), + ): + from prowler.providers.gcp.services.cloudsql.cloudsql_instance_cmek_encryption_enabled.cloudsql_instance_cmek_encryption_enabled import ( + cloudsql_instance_cmek_encryption_enabled, + ) + from prowler.providers.gcp.services.cloudsql.cloudsql_service import ( + Instance, + ) + + cloudsql_client.instances = [ + Instance( + name="db-google-managed", + version="POSTGRES_15", + ip_addresses=[], + region=GCP_EU1_LOCATION, + public_ip=False, + require_ssl=False, + ssl_mode="ENCRYPTED_ONLY", + automated_backups=True, + authorized_networks=[], + flags=[], + project_id=GCP_PROJECT_ID, + cmek_key_name=None, + ) + ] + check = cloudsql_instance_cmek_encryption_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].resource_id == "db-google-managed" + assert result[0].location == GCP_EU1_LOCATION + assert result[0].project_id == GCP_PROJECT_ID + + def test_instance_cmek_empty_string(self): + cloudsql_client = mock.MagicMock() + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.cloudsql.cloudsql_instance_cmek_encryption_enabled.cloudsql_instance_cmek_encryption_enabled.cloudsql_client", + new=cloudsql_client, + ), + ): + from prowler.providers.gcp.services.cloudsql.cloudsql_instance_cmek_encryption_enabled.cloudsql_instance_cmek_encryption_enabled import ( + cloudsql_instance_cmek_encryption_enabled, + ) + from prowler.providers.gcp.services.cloudsql.cloudsql_service import ( + Instance, + ) + + cloudsql_client.instances = [ + Instance( + name="db-empty-key", + version="POSTGRES_15", + ip_addresses=[], + region=GCP_EU1_LOCATION, + public_ip=False, + require_ssl=False, + ssl_mode="ENCRYPTED_ONLY", + automated_backups=True, + authorized_networks=[], + flags=[], + project_id=GCP_PROJECT_ID, + cmek_key_name="", + ) + ] + check = cloudsql_instance_cmek_encryption_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].resource_id == "db-empty-key" + + def test_unsupported_instance_type_skipped(self): + cloudsql_client = mock.MagicMock() + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.cloudsql.cloudsql_instance_cmek_encryption_enabled.cloudsql_instance_cmek_encryption_enabled.cloudsql_client", + new=cloudsql_client, + ), + ): + from prowler.providers.gcp.services.cloudsql.cloudsql_instance_cmek_encryption_enabled.cloudsql_instance_cmek_encryption_enabled import ( + cloudsql_instance_cmek_encryption_enabled, + ) + from prowler.providers.gcp.services.cloudsql.cloudsql_service import ( + Instance, + ) + + cloudsql_client.instances = [ + Instance( + name="external-primary", + version="MYSQL_8_0", + ip_addresses=[], + region=GCP_EU1_LOCATION, + public_ip=False, + require_ssl=False, + ssl_mode="ENCRYPTED_ONLY", + automated_backups=False, + authorized_networks=[], + flags=[], + project_id=GCP_PROJECT_ID, + instance_type="ON_PREMISES_INSTANCE", + cmek_key_name=None, + ), + Instance( + name="db-cmek", + version="POSTGRES_15", + ip_addresses=[], + region=GCP_EU1_LOCATION, + public_ip=False, + require_ssl=False, + ssl_mode="ENCRYPTED_ONLY", + automated_backups=True, + authorized_networks=[], + flags=[], + project_id=GCP_PROJECT_ID, + instance_type="CLOUD_SQL_INSTANCE", + cmek_key_name="projects/p/locations/europe-west1/keyRings/r/cryptoKeys/k", + ), + ] + check = cloudsql_instance_cmek_encryption_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].resource_id == "db-cmek" + assert result[0].status == "PASS" + + def test_service_parser_missing_disk_encryption(self): + """Exercise the real service parser path when diskEncryptionConfiguration is absent.""" + + def mock_api_client_without_disk_encryption(*_args, **_kwargs): + client = MagicMock() + client.instances().list().execute.return_value = { + "items": [ + { + "name": "db-no-encryption-config", + "databaseVersion": "POSTGRES_14", + "region": "us-central1", + "ipAddresses": [], + "settings": { + "ipConfiguration": {"requireSsl": True}, + "backupConfiguration": {"enabled": True}, + "databaseFlags": [], + }, + } + ] + } + client.instances().list_next.return_value = None + return client + + with ( + patch( + "prowler.providers.gcp.lib.service.service.GCPService.__is_api_active__", + new=mock_is_api_active, + ), + patch( + "prowler.providers.gcp.lib.service.service.GCPService.__generate_client__", + new=mock_api_client_without_disk_encryption, + ), + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(project_ids=[GCP_PROJECT_ID]), + ), + ): + from prowler.providers.gcp.services.cloudsql.cloudsql_service import ( + CloudSQL, + ) + + cloudsql_client = CloudSQL( + set_mocked_gcp_provider(project_ids=[GCP_PROJECT_ID]) + ) + assert len(cloudsql_client.instances) == 1 + assert cloudsql_client.instances[0].cmek_key_name is None + + with patch( + "prowler.providers.gcp.services.cloudsql.cloudsql_instance_cmek_encryption_enabled.cloudsql_instance_cmek_encryption_enabled.cloudsql_client", + new=cloudsql_client, + ): + from prowler.providers.gcp.services.cloudsql.cloudsql_instance_cmek_encryption_enabled.cloudsql_instance_cmek_encryption_enabled import ( + cloudsql_instance_cmek_encryption_enabled, + ) + + check = cloudsql_instance_cmek_encryption_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].resource_id == "db-no-encryption-config" diff --git a/tests/providers/gcp/services/cloudsql/cloudsql_service_test.py b/tests/providers/gcp/services/cloudsql/cloudsql_service_test.py index fcc6473559..b5bb846e9e 100644 --- a/tests/providers/gcp/services/cloudsql/cloudsql_service_test.py +++ b/tests/providers/gcp/services/cloudsql/cloudsql_service_test.py @@ -43,6 +43,11 @@ class TestCloudSQLService: {"value": "test"} ] assert cloudsql_client.instances[0].flags == [] + assert cloudsql_client.instances[0].instance_type == "CLOUD_SQL_INSTANCE" + assert ( + cloudsql_client.instances[0].cmek_key_name + == "projects/123/locations/us-central1/keyRings/keyring1/cryptoKeys/key1" + ) assert cloudsql_client.instances[0].project_id == GCP_PROJECT_ID assert cloudsql_client.instances[1].name == "instance2" @@ -62,12 +67,14 @@ class TestCloudSQLService: {"value": "test"} ] assert cloudsql_client.instances[1].flags == [] + assert cloudsql_client.instances[1].instance_type == "CLOUD_SQL_INSTANCE" + assert cloudsql_client.instances[1].cmek_key_name is None assert cloudsql_client.instances[1].project_id == GCP_PROJECT_ID def test_instances_without_backup_configuration(self): """Test that CloudSQL service handles instances without backupConfiguration field""" - def mock_api_client_without_backup_config(*args, **kwargs): + def mock_api_client_without_backup_config(*_args, **_kwargs): from unittest.mock import MagicMock client = MagicMock() @@ -119,7 +126,7 @@ class TestCloudSQLService: def test_instances_with_empty_backup_configuration(self): """Test that CloudSQL service handles instances with empty backupConfiguration""" - def mock_api_client_with_empty_backup_config(*args, **kwargs): + def mock_api_client_with_empty_backup_config(*_args, **_kwargs): from unittest.mock import MagicMock client = MagicMock() @@ -170,7 +177,7 @@ class TestCloudSQLService: def test_instances_without_settings_fields(self): """Test that CloudSQL service handles instances with minimal settings""" - def mock_api_client_with_minimal_settings(*args, **kwargs): + def mock_api_client_with_minimal_settings(*_args, **_kwargs): from unittest.mock import MagicMock client = MagicMock() diff --git a/tests/providers/gcp/services/compute/compute_service_test.py b/tests/providers/gcp/services/compute/compute_service_test.py index 28a3466a5d..2c408e8a39 100644 --- a/tests/providers/gcp/services/compute/compute_service_test.py +++ b/tests/providers/gcp/services/compute/compute_service_test.py @@ -34,6 +34,7 @@ class TestComputeService: assert len(compute_client.compute_projects) == 1 assert compute_client.compute_projects[0].id == GCP_PROJECT_ID assert compute_client.compute_projects[0].enable_oslogin + assert compute_client.compute_projects[0].enable_oslogin_2fa assert len(compute_client.instances) == 2 assert compute_client.instances[0].name == "instance1" diff --git a/tests/providers/googleworkspace/services/additionalservices/additionalservices_external_groups_disabled/additionalservices_external_groups_disabled_test.py b/tests/providers/googleworkspace/services/additionalservices/additionalservices_external_groups_disabled/additionalservices_external_groups_disabled_test.py new file mode 100644 index 0000000000..662f5324cd --- /dev/null +++ b/tests/providers/googleworkspace/services/additionalservices/additionalservices_external_groups_disabled/additionalservices_external_groups_disabled_test.py @@ -0,0 +1,128 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.additionalservices.additionalservices_service import ( + AdditionalServicesPolicies, +) +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + set_mocked_googleworkspace_provider, +) + + +class TestAdditionalServicesExternalGroupsDisabled: + def test_pass_groups_disabled(self): + """Test PASS when external Google Groups access is disabled""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.additionalservices.additionalservices_external_groups_disabled.additionalservices_external_groups_disabled.additionalservices_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.additionalservices.additionalservices_external_groups_disabled.additionalservices_external_groups_disabled import ( + additionalservices_external_groups_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = AdditionalServicesPolicies( + groups_service_state="DISABLED" + ) + + check = additionalservices_external_groups_disabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "disabled" in findings[0].status_extended + assert findings[0].resource_name == "Additional Services Policies" + assert findings[0].resource_id == "additionalServicesPolicies" + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_groups_enabled(self): + """Test FAIL when external Google Groups access is enabled""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.additionalservices.additionalservices_external_groups_disabled.additionalservices_external_groups_disabled.additionalservices_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.additionalservices.additionalservices_external_groups_disabled.additionalservices_external_groups_disabled import ( + additionalservices_external_groups_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = AdditionalServicesPolicies( + groups_service_state="ENABLED" + ) + + check = additionalservices_external_groups_disabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "enabled" in findings[0].status_extended + + def test_fail_no_policy_set(self): + """Test FAIL when no explicit policy is set (None) - Google default is ON (insecure)""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.additionalservices.additionalservices_external_groups_disabled.additionalservices_external_groups_disabled.additionalservices_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.additionalservices.additionalservices_external_groups_disabled.additionalservices_external_groups_disabled import ( + additionalservices_external_groups_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = AdditionalServicesPolicies(groups_service_state=None) + + check = additionalservices_external_groups_disabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "not explicitly configured" in findings[0].status_extended + + def test_no_findings_when_fetch_failed(self): + """Test no findings returned when the API fetch failed""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.additionalservices.additionalservices_external_groups_disabled.additionalservices_external_groups_disabled.additionalservices_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.additionalservices.additionalservices_external_groups_disabled.additionalservices_external_groups_disabled import ( + additionalservices_external_groups_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = False + mock_client.policies = AdditionalServicesPolicies() + + check = additionalservices_external_groups_disabled() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/additionalservices/additionalservices_service_test.py b/tests/providers/googleworkspace/services/additionalservices/additionalservices_service_test.py new file mode 100644 index 0000000000..85d9afea45 --- /dev/null +++ b/tests/providers/googleworkspace/services/additionalservices/additionalservices_service_test.py @@ -0,0 +1,234 @@ +from unittest.mock import MagicMock, patch + +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + set_mocked_googleworkspace_provider, +) + + +class TestAdditionalServicesService: + def test_fetch_policies_groups_off(self): + """Test fetching Additional Services policy with Groups OFF""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_credentials = MagicMock() + mock_session = MagicMock() + mock_session.credentials = mock_credentials + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = { + "policies": [ + { + "setting": { + "type": "settings/groups.service_status", + "value": { + "serviceState": "DISABLED", + }, + } + }, + ] + } + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.additionalservices.additionalservices_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.additionalservices.additionalservices_service import ( + AdditionalServices, + ) + + additional_services = AdditionalServices(mock_provider) + + assert additional_services.policies_fetched is True + assert additional_services.policies.groups_service_state == "DISABLED" + + def test_fetch_policies_groups_on(self): + """Test fetching Additional Services policy with Groups ON""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = { + "policies": [ + { + "setting": { + "type": "settings/groups.service_status", + "value": { + "serviceState": "ENABLED", + }, + } + }, + ] + } + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.additionalservices.additionalservices_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.additionalservices.additionalservices_service import ( + AdditionalServices, + ) + + additional_services = AdditionalServices(mock_provider) + + assert additional_services.policies_fetched is True + assert additional_services.policies.groups_service_state == "ENABLED" + + def test_fetch_policies_empty_response(self): + """Test handling empty policies response""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = {"policies": []} + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.additionalservices.additionalservices_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.additionalservices.additionalservices_service import ( + AdditionalServices, + ) + + additional_services = AdditionalServices(mock_provider) + + assert additional_services.policies_fetched is True + assert additional_services.policies.groups_service_state is None + + def test_fetch_policies_api_error(self): + """Test handling of API errors during policy fetch""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_service.policies().list.side_effect = Exception("API Error") + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.additionalservices.additionalservices_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.additionalservices.additionalservices_service import ( + AdditionalServices, + ) + + additional_services = AdditionalServices(mock_provider) + + assert additional_services.policies_fetched is False + assert additional_services.policies.groups_service_state is None + + def test_fetch_policies_build_service_returns_none(self): + """Test early return when _build_service fails to construct the client""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.additionalservices.additionalservices_service.GoogleWorkspaceService._build_service", + return_value=None, + ), + ): + from prowler.providers.googleworkspace.services.additionalservices.additionalservices_service import ( + AdditionalServices, + ) + + additional_services = AdditionalServices(mock_provider) + + assert additional_services.policies_fetched is False + assert additional_services.policies.groups_service_state is None + + def test_fetch_policies_execute_raises(self): + """Test inner except handler when request.execute() raises during pagination""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_request = MagicMock() + mock_request.execute.side_effect = Exception("Execute failed") + mock_service.policies().list.return_value = mock_request + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.additionalservices.additionalservices_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.additionalservices.additionalservices_service import ( + AdditionalServices, + ) + + additional_services = AdditionalServices(mock_provider) + + assert additional_services.policies_fetched is False + assert additional_services.policies.groups_service_state is None + + def test_additional_services_policies_model(self): + """Test AdditionalServicesPolicies Pydantic model""" + from prowler.providers.googleworkspace.services.additionalservices.additionalservices_service import ( + AdditionalServicesPolicies, + ) + + policies = AdditionalServicesPolicies(groups_service_state="DISABLED") + + assert policies.groups_service_state == "DISABLED" diff --git a/tests/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/calendar_external_invitations_warning_test.py b/tests/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/calendar_external_invitations_warning_test.py index 0320ea4a82..5ff2cee8f6 100644 --- a/tests/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/calendar_external_invitations_warning_test.py +++ b/tests/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/calendar_external_invitations_warning_test.py @@ -5,7 +5,6 @@ from prowler.providers.googleworkspace.services.calendar.calendar_service import ) from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, - DOMAIN, set_mocked_googleworkspace_provider, ) @@ -40,7 +39,7 @@ class TestCalendarExternalInvitationsWarning: assert len(findings) == 1 assert findings[0].status == "PASS" assert "enabled" in findings[0].status_extended - assert findings[0].resource_name == DOMAIN + assert findings[0].resource_name == "Calendar Policies" assert findings[0].customer_id == CUSTOMER_ID def test_fail_warnings_disabled(self): diff --git a/tests/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/calendar_external_sharing_primary_calendar_test.py b/tests/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/calendar_external_sharing_primary_calendar_test.py index 0d34c6a9be..175955f4b6 100644 --- a/tests/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/calendar_external_sharing_primary_calendar_test.py +++ b/tests/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/calendar_external_sharing_primary_calendar_test.py @@ -5,7 +5,6 @@ from prowler.providers.googleworkspace.services.calendar.calendar_service import ) from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, - DOMAIN, set_mocked_googleworkspace_provider, ) @@ -40,10 +39,15 @@ class TestCalendarExternalSharingPrimaryCalendar: assert len(findings) == 1 assert findings[0].status == "PASS" assert "free/busy information only" in findings[0].status_extended - assert findings[0].resource_name == DOMAIN - assert findings[0].resource_id == CUSTOMER_ID + assert findings[0].resource_name == "Calendar Policies" + assert findings[0].resource_id == "calendarPolicies" assert findings[0].customer_id == CUSTOMER_ID - assert findings[0].resource == mock_provider.domain_resource.dict() + assert ( + findings[0].resource + == CalendarPolicies( + primary_calendar_external_sharing="EXTERNAL_FREE_BUSY_ONLY" + ).dict() + ) def test_fail_read_only(self): """Test FAIL when external sharing allows read-only access""" diff --git a/tests/providers/googleworkspace/services/calendar/calendar_external_sharing_secondary_calendar/calendar_external_sharing_secondary_calendar_test.py b/tests/providers/googleworkspace/services/calendar/calendar_external_sharing_secondary_calendar/calendar_external_sharing_secondary_calendar_test.py index 800f9ab5f0..b513860125 100644 --- a/tests/providers/googleworkspace/services/calendar/calendar_external_sharing_secondary_calendar/calendar_external_sharing_secondary_calendar_test.py +++ b/tests/providers/googleworkspace/services/calendar/calendar_external_sharing_secondary_calendar/calendar_external_sharing_secondary_calendar_test.py @@ -5,7 +5,6 @@ from prowler.providers.googleworkspace.services.calendar.calendar_service import ) from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, - DOMAIN, set_mocked_googleworkspace_provider, ) @@ -40,7 +39,7 @@ class TestCalendarExternalSharingSecondaryCalendar: assert len(findings) == 1 assert findings[0].status == "PASS" assert "free/busy information only" in findings[0].status_extended - assert findings[0].resource_name == DOMAIN + assert findings[0].resource_name == "Calendar Policies" assert findings[0].customer_id == CUSTOMER_ID def test_fail_read_only(self): diff --git a/tests/providers/googleworkspace/services/chat/chat_apps_installation_disabled/chat_apps_installation_disabled_test.py b/tests/providers/googleworkspace/services/chat/chat_apps_installation_disabled/chat_apps_installation_disabled_test.py new file mode 100644 index 0000000000..a476321754 --- /dev/null +++ b/tests/providers/googleworkspace/services/chat/chat_apps_installation_disabled/chat_apps_installation_disabled_test.py @@ -0,0 +1,119 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.chat.chat_service import ChatPolicies +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + set_mocked_googleworkspace_provider, +) + + +class TestChatAppsInstallationDisabled: + def test_pass(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.chat.chat_apps_installation_disabled.chat_apps_installation_disabled.chat_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.chat.chat_apps_installation_disabled.chat_apps_installation_disabled import ( + chat_apps_installation_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = ChatPolicies(enable_apps=False) + + check = chat_apps_installation_disabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "disabled" in findings[0].status_extended + assert findings[0].resource_name == "Chat Policies" + assert findings[0].resource_id == "chatPolicies" + assert findings[0].customer_id == CUSTOMER_ID + assert findings[0].resource == ChatPolicies(enable_apps=False).dict() + + def test_fail_enabled(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.chat.chat_apps_installation_disabled.chat_apps_installation_disabled.chat_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.chat.chat_apps_installation_disabled.chat_apps_installation_disabled import ( + chat_apps_installation_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = ChatPolicies(enable_apps=True) + + check = chat_apps_installation_disabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "enabled" in findings[0].status_extended + + def test_pass_no_policy_set(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.chat.chat_apps_installation_disabled.chat_apps_installation_disabled.chat_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.chat.chat_apps_installation_disabled.chat_apps_installation_disabled import ( + chat_apps_installation_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = ChatPolicies(enable_apps=None) + + check = chat_apps_installation_disabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "secure default" in findings[0].status_extended + + def test_no_findings_when_fetch_failed(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.chat.chat_apps_installation_disabled.chat_apps_installation_disabled.chat_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.chat.chat_apps_installation_disabled.chat_apps_installation_disabled import ( + chat_apps_installation_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = False + mock_client.policies = ChatPolicies() + + check = chat_apps_installation_disabled() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/chat/chat_external_file_sharing_disabled/chat_external_file_sharing_disabled_test.py b/tests/providers/googleworkspace/services/chat/chat_external_file_sharing_disabled/chat_external_file_sharing_disabled_test.py new file mode 100644 index 0000000000..429375ebe5 --- /dev/null +++ b/tests/providers/googleworkspace/services/chat/chat_external_file_sharing_disabled/chat_external_file_sharing_disabled_test.py @@ -0,0 +1,149 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.chat.chat_service import ChatPolicies +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + set_mocked_googleworkspace_provider, +) + + +class TestChatExternalFileSharingDisabled: + def test_pass(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.chat.chat_external_file_sharing_disabled.chat_external_file_sharing_disabled.chat_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.chat.chat_external_file_sharing_disabled.chat_external_file_sharing_disabled import ( + chat_external_file_sharing_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = ChatPolicies(external_file_sharing="NO_FILES") + + check = chat_external_file_sharing_disabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "disabled" in findings[0].status_extended + assert findings[0].resource_name == "Chat Policies" + assert findings[0].resource_id == "chatPolicies" + assert findings[0].customer_id == CUSTOMER_ID + assert ( + findings[0].resource + == ChatPolicies(external_file_sharing="NO_FILES").dict() + ) + + def test_fail_all_files(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.chat.chat_external_file_sharing_disabled.chat_external_file_sharing_disabled.chat_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.chat.chat_external_file_sharing_disabled.chat_external_file_sharing_disabled import ( + chat_external_file_sharing_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = ChatPolicies(external_file_sharing="ALL_FILES") + + check = chat_external_file_sharing_disabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "ALL_FILES" in findings[0].status_extended + + def test_fail_images_only(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.chat.chat_external_file_sharing_disabled.chat_external_file_sharing_disabled.chat_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.chat.chat_external_file_sharing_disabled.chat_external_file_sharing_disabled import ( + chat_external_file_sharing_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = ChatPolicies(external_file_sharing="IMAGES_ONLY") + + check = chat_external_file_sharing_disabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "IMAGES_ONLY" in findings[0].status_extended + + def test_fail_no_policy_set(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.chat.chat_external_file_sharing_disabled.chat_external_file_sharing_disabled.chat_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.chat.chat_external_file_sharing_disabled.chat_external_file_sharing_disabled import ( + chat_external_file_sharing_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = ChatPolicies(external_file_sharing=None) + + check = chat_external_file_sharing_disabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "not explicitly configured" in findings[0].status_extended + + def test_no_findings_when_fetch_failed(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.chat.chat_external_file_sharing_disabled.chat_external_file_sharing_disabled.chat_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.chat.chat_external_file_sharing_disabled.chat_external_file_sharing_disabled import ( + chat_external_file_sharing_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = False + mock_client.policies = ChatPolicies() + + check = chat_external_file_sharing_disabled() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/chat/chat_external_messaging_restricted/chat_external_messaging_restricted_test.py b/tests/providers/googleworkspace/services/chat/chat_external_messaging_restricted/chat_external_messaging_restricted_test.py new file mode 100644 index 0000000000..09fa3d2dbc --- /dev/null +++ b/tests/providers/googleworkspace/services/chat/chat_external_messaging_restricted/chat_external_messaging_restricted_test.py @@ -0,0 +1,154 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.chat.chat_service import ChatPolicies +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + set_mocked_googleworkspace_provider, +) + + +class TestChatExternalMessagingRestricted: + def test_pass_external_chat_disabled(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.chat.chat_external_messaging_restricted.chat_external_messaging_restricted.chat_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.chat.chat_external_messaging_restricted.chat_external_messaging_restricted import ( + chat_external_messaging_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = ChatPolicies(allow_external_chat=False) + + check = chat_external_messaging_restricted() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "disabled" in findings[0].status_extended + assert findings[0].resource_name == "Chat Policies" + assert findings[0].resource_id == "chatPolicies" + assert findings[0].customer_id == CUSTOMER_ID + assert ( + findings[0].resource == ChatPolicies(allow_external_chat=False).dict() + ) + + def test_pass_trusted_domains(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.chat.chat_external_messaging_restricted.chat_external_messaging_restricted.chat_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.chat.chat_external_messaging_restricted.chat_external_messaging_restricted import ( + chat_external_messaging_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = ChatPolicies( + allow_external_chat=True, + external_chat_restriction="TRUSTED_DOMAINS", + ) + + check = chat_external_messaging_restricted() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "restricted to allowed domains" in findings[0].status_extended + + def test_fail_no_restriction(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.chat.chat_external_messaging_restricted.chat_external_messaging_restricted.chat_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.chat.chat_external_messaging_restricted.chat_external_messaging_restricted import ( + chat_external_messaging_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = ChatPolicies( + allow_external_chat=True, + external_chat_restriction="NO_RESTRICTION", + ) + + check = chat_external_messaging_restricted() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "not restricted" in findings[0].status_extended + + def test_pass_no_policy_set(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.chat.chat_external_messaging_restricted.chat_external_messaging_restricted.chat_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.chat.chat_external_messaging_restricted.chat_external_messaging_restricted import ( + chat_external_messaging_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = ChatPolicies() + + check = chat_external_messaging_restricted() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "secure default" in findings[0].status_extended + + def test_no_findings_when_fetch_failed(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.chat.chat_external_messaging_restricted.chat_external_messaging_restricted.chat_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.chat.chat_external_messaging_restricted.chat_external_messaging_restricted import ( + chat_external_messaging_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = False + mock_client.policies = ChatPolicies() + + check = chat_external_messaging_restricted() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/chat/chat_external_spaces_restricted/chat_external_spaces_restricted_test.py b/tests/providers/googleworkspace/services/chat/chat_external_spaces_restricted/chat_external_spaces_restricted_test.py new file mode 100644 index 0000000000..608ae11d01 --- /dev/null +++ b/tests/providers/googleworkspace/services/chat/chat_external_spaces_restricted/chat_external_spaces_restricted_test.py @@ -0,0 +1,155 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.chat.chat_service import ChatPolicies +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + set_mocked_googleworkspace_provider, +) + + +class TestChatExternalSpacesRestricted: + def test_pass_spaces_disabled(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.chat.chat_external_spaces_restricted.chat_external_spaces_restricted.chat_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.chat.chat_external_spaces_restricted.chat_external_spaces_restricted import ( + chat_external_spaces_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = ChatPolicies(external_spaces_enabled=False) + + check = chat_external_spaces_restricted() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "disabled" in findings[0].status_extended + assert findings[0].resource_name == "Chat Policies" + assert findings[0].resource_id == "chatPolicies" + assert findings[0].customer_id == CUSTOMER_ID + assert ( + findings[0].resource + == ChatPolicies(external_spaces_enabled=False).dict() + ) + + def test_pass_trusted_domains(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.chat.chat_external_spaces_restricted.chat_external_spaces_restricted.chat_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.chat.chat_external_spaces_restricted.chat_external_spaces_restricted import ( + chat_external_spaces_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = ChatPolicies( + external_spaces_enabled=True, + external_spaces_domain_allowlist_mode="TRUSTED_DOMAINS", + ) + + check = chat_external_spaces_restricted() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "restricted to allowed domains" in findings[0].status_extended + + def test_fail_all_domains(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.chat.chat_external_spaces_restricted.chat_external_spaces_restricted.chat_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.chat.chat_external_spaces_restricted.chat_external_spaces_restricted import ( + chat_external_spaces_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = ChatPolicies( + external_spaces_enabled=True, + external_spaces_domain_allowlist_mode="ALL_DOMAINS", + ) + + check = chat_external_spaces_restricted() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "not restricted" in findings[0].status_extended + + def test_fail_no_policy_set(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.chat.chat_external_spaces_restricted.chat_external_spaces_restricted.chat_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.chat.chat_external_spaces_restricted.chat_external_spaces_restricted import ( + chat_external_spaces_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = ChatPolicies() + + check = chat_external_spaces_restricted() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "not explicitly configured" in findings[0].status_extended + + def test_no_findings_when_fetch_failed(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.chat.chat_external_spaces_restricted.chat_external_spaces_restricted.chat_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.chat.chat_external_spaces_restricted.chat_external_spaces_restricted import ( + chat_external_spaces_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = False + mock_client.policies = ChatPolicies() + + check = chat_external_spaces_restricted() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/chat/chat_incoming_webhooks_disabled/chat_incoming_webhooks_disabled_test.py b/tests/providers/googleworkspace/services/chat/chat_incoming_webhooks_disabled/chat_incoming_webhooks_disabled_test.py new file mode 100644 index 0000000000..712bb21fa5 --- /dev/null +++ b/tests/providers/googleworkspace/services/chat/chat_incoming_webhooks_disabled/chat_incoming_webhooks_disabled_test.py @@ -0,0 +1,119 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.chat.chat_service import ChatPolicies +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + set_mocked_googleworkspace_provider, +) + + +class TestChatIncomingWebhooksDisabled: + def test_pass(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.chat.chat_incoming_webhooks_disabled.chat_incoming_webhooks_disabled.chat_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.chat.chat_incoming_webhooks_disabled.chat_incoming_webhooks_disabled import ( + chat_incoming_webhooks_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = ChatPolicies(enable_webhooks=False) + + check = chat_incoming_webhooks_disabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "disabled" in findings[0].status_extended + assert findings[0].resource_name == "Chat Policies" + assert findings[0].resource_id == "chatPolicies" + assert findings[0].customer_id == CUSTOMER_ID + assert findings[0].resource == ChatPolicies(enable_webhooks=False).dict() + + def test_fail_enabled(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.chat.chat_incoming_webhooks_disabled.chat_incoming_webhooks_disabled.chat_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.chat.chat_incoming_webhooks_disabled.chat_incoming_webhooks_disabled import ( + chat_incoming_webhooks_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = ChatPolicies(enable_webhooks=True) + + check = chat_incoming_webhooks_disabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "enabled" in findings[0].status_extended + + def test_pass_no_policy_set(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.chat.chat_incoming_webhooks_disabled.chat_incoming_webhooks_disabled.chat_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.chat.chat_incoming_webhooks_disabled.chat_incoming_webhooks_disabled import ( + chat_incoming_webhooks_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = ChatPolicies(enable_webhooks=None) + + check = chat_incoming_webhooks_disabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "secure default" in findings[0].status_extended + + def test_no_findings_when_fetch_failed(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.chat.chat_incoming_webhooks_disabled.chat_incoming_webhooks_disabled.chat_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.chat.chat_incoming_webhooks_disabled.chat_incoming_webhooks_disabled import ( + chat_incoming_webhooks_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = False + mock_client.policies = ChatPolicies() + + check = chat_incoming_webhooks_disabled() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/chat/chat_internal_file_sharing_disabled/chat_internal_file_sharing_disabled_test.py b/tests/providers/googleworkspace/services/chat/chat_internal_file_sharing_disabled/chat_internal_file_sharing_disabled_test.py new file mode 100644 index 0000000000..297d00548d --- /dev/null +++ b/tests/providers/googleworkspace/services/chat/chat_internal_file_sharing_disabled/chat_internal_file_sharing_disabled_test.py @@ -0,0 +1,122 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.chat.chat_service import ChatPolicies +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + set_mocked_googleworkspace_provider, +) + + +class TestChatInternalFileSharingDisabled: + def test_pass(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.chat.chat_internal_file_sharing_disabled.chat_internal_file_sharing_disabled.chat_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.chat.chat_internal_file_sharing_disabled.chat_internal_file_sharing_disabled import ( + chat_internal_file_sharing_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = ChatPolicies(internal_file_sharing="NO_FILES") + + check = chat_internal_file_sharing_disabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "disabled" in findings[0].status_extended + assert findings[0].resource_name == "Chat Policies" + assert findings[0].resource_id == "chatPolicies" + assert findings[0].customer_id == CUSTOMER_ID + assert ( + findings[0].resource + == ChatPolicies(internal_file_sharing="NO_FILES").dict() + ) + + def test_fail_all_files(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.chat.chat_internal_file_sharing_disabled.chat_internal_file_sharing_disabled.chat_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.chat.chat_internal_file_sharing_disabled.chat_internal_file_sharing_disabled import ( + chat_internal_file_sharing_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = ChatPolicies(internal_file_sharing="ALL_FILES") + + check = chat_internal_file_sharing_disabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "ALL_FILES" in findings[0].status_extended + + def test_fail_no_policy_set(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.chat.chat_internal_file_sharing_disabled.chat_internal_file_sharing_disabled.chat_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.chat.chat_internal_file_sharing_disabled.chat_internal_file_sharing_disabled import ( + chat_internal_file_sharing_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = ChatPolicies(internal_file_sharing=None) + + check = chat_internal_file_sharing_disabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "not explicitly configured" in findings[0].status_extended + + def test_no_findings_when_fetch_failed(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.chat.chat_internal_file_sharing_disabled.chat_internal_file_sharing_disabled.chat_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.chat.chat_internal_file_sharing_disabled.chat_internal_file_sharing_disabled import ( + chat_internal_file_sharing_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = False + mock_client.policies = ChatPolicies() + + check = chat_internal_file_sharing_disabled() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/chat/chat_service_test.py b/tests/providers/googleworkspace/services/chat/chat_service_test.py new file mode 100644 index 0000000000..673f1eb3cd --- /dev/null +++ b/tests/providers/googleworkspace/services/chat/chat_service_test.py @@ -0,0 +1,440 @@ +from unittest.mock import MagicMock, patch + +from googleapiclient.errors import HttpError +from httplib2 import Response as HttpResponse + +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + ROOT_ORG_UNIT_ID, + set_mocked_googleworkspace_provider, +) + + +class TestChatService: + def test_chat_fetch_policies_all_settings(self): + """Test fetching all 4 Chat policy settings from Cloud Identity API""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_credentials = MagicMock() + mock_session = MagicMock() + mock_session.credentials = mock_credentials + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = { + "policies": [ + { + "setting": { + "type": "settings/chat.chat_file_sharing", + "value": { + "externalFileSharing": "NO_FILES", + "internalFileSharing": "IMAGES_ONLY", + }, + } + }, + { + "setting": { + "type": "settings/chat.external_chat_restriction", + "value": { + "allowExternalChat": True, + "externalChatRestriction": "TRUSTED_DOMAINS", + }, + } + }, + { + "setting": { + "type": "settings/chat.chat_external_spaces", + "value": { + "enabled": True, + "domainAllowlistMode": "TRUSTED_DOMAINS", + }, + } + }, + { + "setting": { + "type": "settings/chat.chat_apps_access", + "value": { + "enableApps": False, + "enableWebhooks": False, + }, + } + }, + ] + } + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.chat.chat_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.chat.chat_service import ( + Chat, + ) + + chat = Chat(mock_provider) + + assert chat.policies_fetched is True + assert chat.policies.external_file_sharing == "NO_FILES" + assert chat.policies.internal_file_sharing == "IMAGES_ONLY" + assert chat.policies.allow_external_chat is True + assert chat.policies.external_chat_restriction == "TRUSTED_DOMAINS" + assert chat.policies.external_spaces_enabled is True + assert ( + chat.policies.external_spaces_domain_allowlist_mode == "TRUSTED_DOMAINS" + ) + assert chat.policies.enable_apps is False + assert chat.policies.enable_webhooks is False + + def test_chat_fetch_policies_empty_response(self): + """Test handling empty policies response""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = {"policies": []} + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.chat.chat_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.chat.chat_service import ( + Chat, + ) + + chat = Chat(mock_provider) + + assert chat.policies_fetched is True + assert chat.policies.external_file_sharing is None + assert chat.policies.allow_external_chat is None + assert chat.policies.enable_apps is None + assert chat.policies.enable_webhooks is None + + def test_chat_fetch_policies_api_error(self): + """Test handling of API errors during policy fetch""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_service.policies().list.side_effect = Exception("API Error") + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.chat.chat_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.chat.chat_service import ( + Chat, + ) + + chat = Chat(mock_provider) + + assert chat.policies_fetched is False + assert chat.policies.external_file_sharing is None + + def test_chat_fetch_policies_build_service_returns_none(self): + """Test early return when _build_service fails to construct the client""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.chat.chat_service.GoogleWorkspaceService._build_service", + return_value=None, + ), + ): + from prowler.providers.googleworkspace.services.chat.chat_service import ( + Chat, + ) + + chat = Chat(mock_provider) + + assert chat.policies_fetched is False + assert chat.policies.external_file_sharing is None + + def test_chat_fetch_policies_execute_raises(self): + """Test inner except handler when request.execute() raises during pagination""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_request = MagicMock() + mock_request.execute.side_effect = Exception("Execute failed") + mock_service.policies().list.return_value = mock_request + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.chat.chat_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.chat.chat_service import ( + Chat, + ) + + chat = Chat(mock_provider) + + assert chat.policies_fetched is False + assert chat.policies.external_file_sharing is None + + def test_chat_fetch_policies_ignores_ou_and_group_level(self): + """Test that OU-level and group-level policies are skipped, only customer-level used""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = { + "policies": [ + { + # Customer-level: no policyQuery → should be used + "setting": { + "type": "settings/chat.chat_apps_access", + "value": {"enableApps": False, "enableWebhooks": False}, + } + }, + { + # OU-level: has policyQuery.orgUnit → should be skipped + "policyQuery": {"orgUnit": "orgUnits/sales_team"}, + "setting": { + "type": "settings/chat.chat_apps_access", + "value": {"enableApps": True, "enableWebhooks": True}, + }, + }, + { + # Group-level: has policyQuery.group → should be skipped + "policyQuery": {"group": "groups/contractors"}, + "setting": { + "type": "settings/chat.chat_file_sharing", + "value": { + "externalFileSharing": "ALL_FILES", + "internalFileSharing": "ALL_FILES", + }, + }, + }, + { + # Customer-level: no policyQuery → should be used + "setting": { + "type": "settings/chat.chat_file_sharing", + "value": { + "externalFileSharing": "NO_FILES", + "internalFileSharing": "NO_FILES", + }, + } + }, + ] + } + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.chat.chat_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.chat.chat_service import ( + Chat, + ) + + chat = Chat(mock_provider) + + assert chat.policies_fetched is True + assert chat.policies.enable_apps is False + assert chat.policies.external_file_sharing == "NO_FILES" + + def test_chat_fetch_policies_accepts_root_ou(self): + """Test that root-OU-scoped policies are accepted as customer-level""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = { + "policies": [ + { + # Root OU: matches provider's root_org_unit_id → should be accepted + "policyQuery": {"orgUnit": f"orgUnits/{ROOT_ORG_UNIT_ID}"}, + "setting": { + "type": "settings/chat.chat_apps_access", + "value": {"enableApps": False, "enableWebhooks": True}, + }, + }, + { + # Sub-OU: different orgUnit → should be skipped + "policyQuery": {"orgUnit": "orgUnits/sub_ou_sales"}, + "setting": { + "type": "settings/chat.chat_file_sharing", + "value": { + "externalFileSharing": "ALL_FILES", + "internalFileSharing": "ALL_FILES", + }, + }, + }, + ] + } + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.chat.chat_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.chat.chat_service import ( + Chat, + ) + + chat = Chat(mock_provider) + + assert chat.policies_fetched is True + # Root OU policy accepted + assert chat.policies.enable_apps is False + assert chat.policies.enable_webhooks is True + # Sub-OU policy skipped + assert chat.policies.external_file_sharing is None + + def test_chat_partial_fetch_marks_policies_fetched_false(self): + """Regression: if page 1 returns valid data but page 2 raises an error, + policies_fetched must be False even though some policy values were stored.""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + + # Page 1: returns valid Chat data + page1_response = { + "policies": [ + { + "setting": { + "type": "settings/chat.chat_apps_access", + "value": {"enableApps": False, "enableWebhooks": False}, + } + }, + ] + } + + # Page 2 request raises HttpError 429 + page1_request = MagicMock() + page1_request.execute.return_value = page1_response + + page2_request = MagicMock() + page2_request.execute.side_effect = HttpError( + HttpResponse({"status": "429"}), b"Rate limit exceeded" + ) + + mock_service.policies().list.return_value = page1_request + mock_service.policies().list_next.return_value = page2_request + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.chat.chat_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.chat.chat_service import ( + Chat, + ) + + chat = Chat(mock_provider) + + # Page 1 data was stored + assert chat.policies.enable_apps is False + # But policies_fetched must be False because page 2 failed + assert chat.policies_fetched is False + + def test_chat_policies_model(self): + """Test ChatPolicies Pydantic model""" + from prowler.providers.googleworkspace.services.chat.chat_service import ( + ChatPolicies, + ) + + policies = ChatPolicies( + external_file_sharing="NO_FILES", + internal_file_sharing="IMAGES_ONLY", + allow_external_chat=True, + external_chat_restriction="TRUSTED_DOMAINS", + external_spaces_enabled=True, + external_spaces_domain_allowlist_mode="TRUSTED_DOMAINS", + enable_apps=False, + enable_webhooks=False, + ) + + assert policies.external_file_sharing == "NO_FILES" + assert policies.internal_file_sharing == "IMAGES_ONLY" + assert policies.allow_external_chat is True + assert policies.external_chat_restriction == "TRUSTED_DOMAINS" + assert policies.external_spaces_enabled is True + assert policies.external_spaces_domain_allowlist_mode == "TRUSTED_DOMAINS" + assert policies.enable_apps is False + assert policies.enable_webhooks is False diff --git a/tests/providers/googleworkspace/services/directory/directory_super_admin_count/directory_super_admin_count_test.py b/tests/providers/googleworkspace/services/directory/directory_super_admin_count/directory_super_admin_count_test.py index 99ae59ef29..2b5df2db09 100644 --- a/tests/providers/googleworkspace/services/directory/directory_super_admin_count/directory_super_admin_count_test.py +++ b/tests/providers/googleworkspace/services/directory/directory_super_admin_count/directory_super_admin_count_test.py @@ -3,7 +3,6 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.directory.directory_service import User from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, - DOMAIN, set_mocked_googleworkspace_provider, ) @@ -54,8 +53,8 @@ class TestDirectorySuperAdminCount: assert findings[0].status == "PASS" assert "2 super administrator(s)" in findings[0].status_extended assert "within the recommended range" in findings[0].status_extended - assert findings[0].resource_name == DOMAIN - assert findings[0].resource_id == CUSTOMER_ID + assert findings[0].resource_name == "Directory Users" + assert findings[0].resource_id == "directoryUsers" assert findings[0].customer_id == CUSTOMER_ID assert findings[0].resource == mock_provider.domain_resource.dict() diff --git a/tests/providers/googleworkspace/services/directory/directory_super_admin_only_admin_roles/directory_super_admin_only_admin_roles_test.py b/tests/providers/googleworkspace/services/directory/directory_super_admin_only_admin_roles/directory_super_admin_only_admin_roles_test.py index 0d15eef2a5..c3341159bd 100644 --- a/tests/providers/googleworkspace/services/directory/directory_super_admin_only_admin_roles/directory_super_admin_only_admin_roles_test.py +++ b/tests/providers/googleworkspace/services/directory/directory_super_admin_only_admin_roles/directory_super_admin_only_admin_roles_test.py @@ -6,7 +6,6 @@ from prowler.providers.googleworkspace.services.directory.directory_service impo ) from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, - DOMAIN, set_mocked_googleworkspace_provider, ) @@ -90,8 +89,8 @@ class TestDirectorySuperAdminOnlyAdminRoles: assert len(findings) == 1 assert findings[0].status == "PASS" assert "used only for super admin activities" in findings[0].status_extended - assert findings[0].resource_name == DOMAIN - assert findings[0].resource_id == CUSTOMER_ID + assert findings[0].resource_name == "Directory Users" + assert findings[0].resource_id == "directoryUsers" assert findings[0].customer_id == CUSTOMER_ID assert findings[0].resource == mock_provider.domain_resource.dict() diff --git a/tests/providers/googleworkspace/services/drive/drive_access_checker_recipients_only/drive_access_checker_recipients_only_test.py b/tests/providers/googleworkspace/services/drive/drive_access_checker_recipients_only/drive_access_checker_recipients_only_test.py index b439429e20..e200836bcd 100644 --- a/tests/providers/googleworkspace/services/drive/drive_access_checker_recipients_only/drive_access_checker_recipients_only_test.py +++ b/tests/providers/googleworkspace/services/drive/drive_access_checker_recipients_only/drive_access_checker_recipients_only_test.py @@ -3,7 +3,6 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.drive.drive_service import DrivePolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, - DOMAIN, set_mocked_googleworkspace_provider, ) @@ -38,7 +37,7 @@ class TestDriveAccessCheckerRecipientsOnly: assert len(findings) == 1 assert findings[0].status == "PASS" assert "recipients only" in findings[0].status_extended - assert findings[0].resource_name == DOMAIN + assert findings[0].resource_name == "Drive Policies" assert findings[0].customer_id == CUSTOMER_ID def test_fail_recipients_or_audience(self): diff --git a/tests/providers/googleworkspace/services/drive/drive_desktop_access_disabled/drive_desktop_access_disabled_test.py b/tests/providers/googleworkspace/services/drive/drive_desktop_access_disabled/drive_desktop_access_disabled_test.py index 00ed8dfa26..7225974c68 100644 --- a/tests/providers/googleworkspace/services/drive/drive_desktop_access_disabled/drive_desktop_access_disabled_test.py +++ b/tests/providers/googleworkspace/services/drive/drive_desktop_access_disabled/drive_desktop_access_disabled_test.py @@ -3,7 +3,6 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.drive.drive_service import DrivePolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, - DOMAIN, set_mocked_googleworkspace_provider, ) @@ -36,7 +35,7 @@ class TestDriveDesktopAccessDisabled: assert len(findings) == 1 assert findings[0].status == "PASS" assert "disabled" in findings[0].status_extended - assert findings[0].resource_name == DOMAIN + assert findings[0].resource_name == "Drive Policies" assert findings[0].customer_id == CUSTOMER_ID def test_fail_desktop_enabled(self): diff --git a/tests/providers/googleworkspace/services/drive/drive_external_sharing_warn_users/drive_external_sharing_warn_users_test.py b/tests/providers/googleworkspace/services/drive/drive_external_sharing_warn_users/drive_external_sharing_warn_users_test.py index 8520971268..3b66837460 100644 --- a/tests/providers/googleworkspace/services/drive/drive_external_sharing_warn_users/drive_external_sharing_warn_users_test.py +++ b/tests/providers/googleworkspace/services/drive/drive_external_sharing_warn_users/drive_external_sharing_warn_users_test.py @@ -3,7 +3,6 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.drive.drive_service import DrivePolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, - DOMAIN, set_mocked_googleworkspace_provider, ) @@ -36,10 +35,13 @@ class TestDriveExternalSharingWarnUsers: assert len(findings) == 1 assert findings[0].status == "PASS" assert "enabled" in findings[0].status_extended - assert findings[0].resource_name == DOMAIN - assert findings[0].resource_id == CUSTOMER_ID + assert findings[0].resource_name == "Drive Policies" + assert findings[0].resource_id == "drivePolicies" assert findings[0].customer_id == CUSTOMER_ID - assert findings[0].resource == mock_provider.domain_resource.dict() + assert ( + findings[0].resource + == DrivePolicies(warn_for_external_sharing=True).dict() + ) def test_fail_warning_disabled(self): """Test FAIL when external sharing warning is explicitly disabled""" diff --git a/tests/providers/googleworkspace/services/drive/drive_internal_users_distribute_content/drive_internal_users_distribute_content_test.py b/tests/providers/googleworkspace/services/drive/drive_internal_users_distribute_content/drive_internal_users_distribute_content_test.py index 5ac3067102..b089d52d58 100644 --- a/tests/providers/googleworkspace/services/drive/drive_internal_users_distribute_content/drive_internal_users_distribute_content_test.py +++ b/tests/providers/googleworkspace/services/drive/drive_internal_users_distribute_content/drive_internal_users_distribute_content_test.py @@ -3,7 +3,6 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.drive.drive_service import DrivePolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, - DOMAIN, set_mocked_googleworkspace_provider, ) @@ -38,7 +37,7 @@ class TestDriveInternalUsersDistributeContent: assert len(findings) == 1 assert findings[0].status == "PASS" assert "ELIGIBLE_INTERNAL_USERS" in findings[0].status_extended - assert findings[0].resource_name == DOMAIN + assert findings[0].resource_name == "Drive Policies" assert findings[0].customer_id == CUSTOMER_ID def test_pass_none_allowed(self): diff --git a/tests/providers/googleworkspace/services/drive/drive_publishing_files_disabled/drive_publishing_files_disabled_test.py b/tests/providers/googleworkspace/services/drive/drive_publishing_files_disabled/drive_publishing_files_disabled_test.py index cbcef4f900..8d6a0efcff 100644 --- a/tests/providers/googleworkspace/services/drive/drive_publishing_files_disabled/drive_publishing_files_disabled_test.py +++ b/tests/providers/googleworkspace/services/drive/drive_publishing_files_disabled/drive_publishing_files_disabled_test.py @@ -3,7 +3,6 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.drive.drive_service import DrivePolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, - DOMAIN, set_mocked_googleworkspace_provider, ) @@ -36,7 +35,7 @@ class TestDrivePublishingFilesDisabled: assert len(findings) == 1 assert findings[0].status == "PASS" assert "disabled" in findings[0].status_extended - assert findings[0].resource_name == DOMAIN + assert findings[0].resource_name == "Drive Policies" assert findings[0].customer_id == CUSTOMER_ID def test_fail_publishing_enabled(self): diff --git a/tests/providers/googleworkspace/services/drive/drive_shared_drive_creation_allowed/drive_shared_drive_creation_allowed_test.py b/tests/providers/googleworkspace/services/drive/drive_shared_drive_creation_allowed/drive_shared_drive_creation_allowed_test.py index 016e643f2d..6ad0222f06 100644 --- a/tests/providers/googleworkspace/services/drive/drive_shared_drive_creation_allowed/drive_shared_drive_creation_allowed_test.py +++ b/tests/providers/googleworkspace/services/drive/drive_shared_drive_creation_allowed/drive_shared_drive_creation_allowed_test.py @@ -3,7 +3,6 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.drive.drive_service import DrivePolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, - DOMAIN, set_mocked_googleworkspace_provider, ) @@ -36,7 +35,7 @@ class TestDriveSharedDriveCreationAllowed: assert len(findings) == 1 assert findings[0].status == "PASS" assert "allowed" in findings[0].status_extended - assert findings[0].resource_name == DOMAIN + assert findings[0].resource_name == "Drive Policies" assert findings[0].customer_id == CUSTOMER_ID def test_fail_creation_disabled(self): diff --git a/tests/providers/googleworkspace/services/drive/drive_shared_drive_disable_download_print_copy/drive_shared_drive_disable_download_print_copy_test.py b/tests/providers/googleworkspace/services/drive/drive_shared_drive_disable_download_print_copy/drive_shared_drive_disable_download_print_copy_test.py index 05edcb57a2..4a7a926745 100644 --- a/tests/providers/googleworkspace/services/drive/drive_shared_drive_disable_download_print_copy/drive_shared_drive_disable_download_print_copy_test.py +++ b/tests/providers/googleworkspace/services/drive/drive_shared_drive_disable_download_print_copy/drive_shared_drive_disable_download_print_copy_test.py @@ -3,7 +3,6 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.drive.drive_service import DrivePolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, - DOMAIN, set_mocked_googleworkspace_provider, ) @@ -38,7 +37,7 @@ class TestDriveSharedDriveDisableDownloadPrintCopy: assert len(findings) == 1 assert findings[0].status == "PASS" assert "EDITORS_ONLY" in findings[0].status_extended - assert findings[0].resource_name == DOMAIN + assert findings[0].resource_name == "Drive Policies" assert findings[0].customer_id == CUSTOMER_ID def test_pass_managers_only(self): diff --git a/tests/providers/googleworkspace/services/drive/drive_shared_drive_managers_cannot_override/drive_shared_drive_managers_cannot_override_test.py b/tests/providers/googleworkspace/services/drive/drive_shared_drive_managers_cannot_override/drive_shared_drive_managers_cannot_override_test.py index 4b29aa5a0a..99e4af7d16 100644 --- a/tests/providers/googleworkspace/services/drive/drive_shared_drive_managers_cannot_override/drive_shared_drive_managers_cannot_override_test.py +++ b/tests/providers/googleworkspace/services/drive/drive_shared_drive_managers_cannot_override/drive_shared_drive_managers_cannot_override_test.py @@ -3,7 +3,6 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.drive.drive_service import DrivePolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, - DOMAIN, set_mocked_googleworkspace_provider, ) @@ -38,7 +37,7 @@ class TestDriveSharedDriveManagersCannotOverride: assert len(findings) == 1 assert findings[0].status == "PASS" assert "cannot override" in findings[0].status_extended - assert findings[0].resource_name == DOMAIN + assert findings[0].resource_name == "Drive Policies" assert findings[0].customer_id == CUSTOMER_ID def test_fail_override_allowed(self): diff --git a/tests/providers/googleworkspace/services/drive/drive_shared_drive_members_only_access/drive_shared_drive_members_only_access_test.py b/tests/providers/googleworkspace/services/drive/drive_shared_drive_members_only_access/drive_shared_drive_members_only_access_test.py index 06b1877d52..b25fa33cf0 100644 --- a/tests/providers/googleworkspace/services/drive/drive_shared_drive_members_only_access/drive_shared_drive_members_only_access_test.py +++ b/tests/providers/googleworkspace/services/drive/drive_shared_drive_members_only_access/drive_shared_drive_members_only_access_test.py @@ -3,7 +3,6 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.drive.drive_service import DrivePolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, - DOMAIN, set_mocked_googleworkspace_provider, ) @@ -36,7 +35,7 @@ class TestDriveSharedDriveMembersOnlyAccess: assert len(findings) == 1 assert findings[0].status == "PASS" assert "members only" in findings[0].status_extended - assert findings[0].resource_name == DOMAIN + assert findings[0].resource_name == "Drive Policies" assert findings[0].customer_id == CUSTOMER_ID def test_fail_non_member_access_enabled(self): diff --git a/tests/providers/googleworkspace/services/drive/drive_sharing_allowlisted_domains/drive_sharing_allowlisted_domains_test.py b/tests/providers/googleworkspace/services/drive/drive_sharing_allowlisted_domains/drive_sharing_allowlisted_domains_test.py index 0323e5e399..337afdc9aa 100644 --- a/tests/providers/googleworkspace/services/drive/drive_sharing_allowlisted_domains/drive_sharing_allowlisted_domains_test.py +++ b/tests/providers/googleworkspace/services/drive/drive_sharing_allowlisted_domains/drive_sharing_allowlisted_domains_test.py @@ -3,7 +3,6 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.drive.drive_service import DrivePolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, - DOMAIN, set_mocked_googleworkspace_provider, ) @@ -38,7 +37,7 @@ class TestDriveSharingAllowlistedDomains: assert len(findings) == 1 assert findings[0].status == "PASS" assert "allowlisted domains" in findings[0].status_extended - assert findings[0].resource_name == DOMAIN + assert findings[0].resource_name == "Drive Policies" assert findings[0].customer_id == CUSTOMER_ID def test_fail_allowed(self): diff --git a/tests/providers/googleworkspace/services/drive/drive_warn_sharing_with_allowlisted_domains/drive_warn_sharing_with_allowlisted_domains_test.py b/tests/providers/googleworkspace/services/drive/drive_warn_sharing_with_allowlisted_domains/drive_warn_sharing_with_allowlisted_domains_test.py index 1731b81fcc..1aadeb924b 100644 --- a/tests/providers/googleworkspace/services/drive/drive_warn_sharing_with_allowlisted_domains/drive_warn_sharing_with_allowlisted_domains_test.py +++ b/tests/providers/googleworkspace/services/drive/drive_warn_sharing_with_allowlisted_domains/drive_warn_sharing_with_allowlisted_domains_test.py @@ -3,7 +3,6 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.drive.drive_service import DrivePolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, - DOMAIN, set_mocked_googleworkspace_provider, ) @@ -38,7 +37,7 @@ class TestDriveWarnSharingWithAllowlistedDomains: assert len(findings) == 1 assert findings[0].status == "PASS" assert "warned" in findings[0].status_extended - assert findings[0].resource_name == DOMAIN + assert findings[0].resource_name == "Drive Policies" assert findings[0].customer_id == CUSTOMER_ID def test_fail_warning_disabled(self): diff --git a/tests/providers/googleworkspace/services/gmail/gmail_anomalous_attachment_protection_enabled/gmail_anomalous_attachment_protection_enabled_test.py b/tests/providers/googleworkspace/services/gmail/gmail_anomalous_attachment_protection_enabled/gmail_anomalous_attachment_protection_enabled_test.py new file mode 100644 index 0000000000..a57bee1ff1 --- /dev/null +++ b/tests/providers/googleworkspace/services/gmail/gmail_anomalous_attachment_protection_enabled/gmail_anomalous_attachment_protection_enabled_test.py @@ -0,0 +1,153 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.gmail.gmail_service import GmailPolicies +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + set_mocked_googleworkspace_provider, +) + + +class TestGmailAnomalousAttachmentProtectionEnabled: + def test_pass(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_anomalous_attachment_protection_enabled.gmail_anomalous_attachment_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_anomalous_attachment_protection_enabled.gmail_anomalous_attachment_protection_enabled import ( + gmail_anomalous_attachment_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + enable_anomalous_attachment_protection=True, + anomalous_attachment_protection_consequence="WARNING", + ) + + check = gmail_anomalous_attachment_protection_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "WARNING" in findings[0].status_extended + assert findings[0].resource_name == "Gmail Policies" + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_no_action(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_anomalous_attachment_protection_enabled.gmail_anomalous_attachment_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_anomalous_attachment_protection_enabled.gmail_anomalous_attachment_protection_enabled import ( + gmail_anomalous_attachment_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + enable_anomalous_attachment_protection=True, + anomalous_attachment_protection_consequence="NO_ACTION", + ) + + check = gmail_anomalous_attachment_protection_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "no action" in findings[0].status_extended + + def test_fail_protection_disabled(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_anomalous_attachment_protection_enabled.gmail_anomalous_attachment_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_anomalous_attachment_protection_enabled.gmail_anomalous_attachment_protection_enabled import ( + gmail_anomalous_attachment_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + enable_anomalous_attachment_protection=False, + anomalous_attachment_protection_consequence="WARNING", + ) + + check = gmail_anomalous_attachment_protection_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "disabled" in findings[0].status_extended + + def test_fail_no_policy_set(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_anomalous_attachment_protection_enabled.gmail_anomalous_attachment_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_anomalous_attachment_protection_enabled.gmail_anomalous_attachment_protection_enabled import ( + gmail_anomalous_attachment_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies() + + check = gmail_anomalous_attachment_protection_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "insecure default" in findings[0].status_extended + + def test_no_findings_when_fetch_failed(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_anomalous_attachment_protection_enabled.gmail_anomalous_attachment_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_anomalous_attachment_protection_enabled.gmail_anomalous_attachment_protection_enabled import ( + gmail_anomalous_attachment_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = False + mock_client.policies = GmailPolicies() + + check = gmail_anomalous_attachment_protection_enabled() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/gmail/gmail_auto_forwarding_disabled/gmail_auto_forwarding_disabled_test.py b/tests/providers/googleworkspace/services/gmail/gmail_auto_forwarding_disabled/gmail_auto_forwarding_disabled_test.py index 462f8b4f05..2de8a79b9b 100644 --- a/tests/providers/googleworkspace/services/gmail/gmail_auto_forwarding_disabled/gmail_auto_forwarding_disabled_test.py +++ b/tests/providers/googleworkspace/services/gmail/gmail_auto_forwarding_disabled/gmail_auto_forwarding_disabled_test.py @@ -3,7 +3,6 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.gmail.gmail_service import GmailPolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, - DOMAIN, set_mocked_googleworkspace_provider, ) @@ -35,10 +34,13 @@ class TestGmailAutoForwardingDisabled: assert len(findings) == 1 assert findings[0].status == "PASS" assert "disabled" in findings[0].status_extended - assert findings[0].resource_name == DOMAIN - assert findings[0].resource_id == CUSTOMER_ID + assert findings[0].resource_name == "Gmail Policies" + assert findings[0].resource_id == "gmailPolicies" assert findings[0].customer_id == CUSTOMER_ID - assert findings[0].resource == mock_provider.domain_resource.dict() + assert ( + findings[0].resource + == GmailPolicies(enable_auto_forwarding=False).dict() + ) def test_fail_disabled(self): mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/gmail/gmail_comprehensive_mail_storage_enabled/gmail_comprehensive_mail_storage_enabled_test.py b/tests/providers/googleworkspace/services/gmail/gmail_comprehensive_mail_storage_enabled/gmail_comprehensive_mail_storage_enabled_test.py index 8c2525e819..1dde307b71 100644 --- a/tests/providers/googleworkspace/services/gmail/gmail_comprehensive_mail_storage_enabled/gmail_comprehensive_mail_storage_enabled_test.py +++ b/tests/providers/googleworkspace/services/gmail/gmail_comprehensive_mail_storage_enabled/gmail_comprehensive_mail_storage_enabled_test.py @@ -3,7 +3,6 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.gmail.gmail_service import GmailPolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, - DOMAIN, set_mocked_googleworkspace_provider, ) @@ -37,7 +36,7 @@ class TestGmailComprehensiveMailStorageEnabled: assert len(findings) == 1 assert findings[0].status == "PASS" assert "enabled" in findings[0].status_extended - assert findings[0].resource_name == DOMAIN + assert findings[0].resource_name == "Gmail Policies" assert findings[0].customer_id == CUSTOMER_ID def test_fail_disabled(self): diff --git a/tests/providers/googleworkspace/services/gmail/gmail_domain_spoofing_protection_enabled/gmail_domain_spoofing_protection_enabled_test.py b/tests/providers/googleworkspace/services/gmail/gmail_domain_spoofing_protection_enabled/gmail_domain_spoofing_protection_enabled_test.py new file mode 100644 index 0000000000..de18c6d9c6 --- /dev/null +++ b/tests/providers/googleworkspace/services/gmail/gmail_domain_spoofing_protection_enabled/gmail_domain_spoofing_protection_enabled_test.py @@ -0,0 +1,153 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.gmail.gmail_service import GmailPolicies +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + set_mocked_googleworkspace_provider, +) + + +class TestGmailDomainSpoofingProtectionEnabled: + def test_pass(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_domain_spoofing_protection_enabled.gmail_domain_spoofing_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_domain_spoofing_protection_enabled.gmail_domain_spoofing_protection_enabled import ( + gmail_domain_spoofing_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + detect_domain_name_spoofing=True, + domain_spoofing_consequence="SPAM_FOLDER", + ) + + check = gmail_domain_spoofing_protection_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "SPAM_FOLDER" in findings[0].status_extended + assert findings[0].resource_name == "Gmail Policies" + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_no_action(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_domain_spoofing_protection_enabled.gmail_domain_spoofing_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_domain_spoofing_protection_enabled.gmail_domain_spoofing_protection_enabled import ( + gmail_domain_spoofing_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + detect_domain_name_spoofing=True, + domain_spoofing_consequence="NO_ACTION", + ) + + check = gmail_domain_spoofing_protection_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "no action" in findings[0].status_extended + + def test_fail_protection_disabled(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_domain_spoofing_protection_enabled.gmail_domain_spoofing_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_domain_spoofing_protection_enabled.gmail_domain_spoofing_protection_enabled import ( + gmail_domain_spoofing_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + detect_domain_name_spoofing=False, + domain_spoofing_consequence="WARNING", + ) + + check = gmail_domain_spoofing_protection_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "disabled" in findings[0].status_extended + + def test_pass_using_default(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_domain_spoofing_protection_enabled.gmail_domain_spoofing_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_domain_spoofing_protection_enabled.gmail_domain_spoofing_protection_enabled import ( + gmail_domain_spoofing_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies() + + check = gmail_domain_spoofing_protection_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "secure default" in findings[0].status_extended + + def test_no_findings_when_fetch_failed(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_domain_spoofing_protection_enabled.gmail_domain_spoofing_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_domain_spoofing_protection_enabled.gmail_domain_spoofing_protection_enabled import ( + gmail_domain_spoofing_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = False + mock_client.policies = GmailPolicies() + + check = gmail_domain_spoofing_protection_enabled() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/gmail/gmail_employee_name_spoofing_protection_enabled/gmail_employee_name_spoofing_protection_enabled_test.py b/tests/providers/googleworkspace/services/gmail/gmail_employee_name_spoofing_protection_enabled/gmail_employee_name_spoofing_protection_enabled_test.py new file mode 100644 index 0000000000..d30284b852 --- /dev/null +++ b/tests/providers/googleworkspace/services/gmail/gmail_employee_name_spoofing_protection_enabled/gmail_employee_name_spoofing_protection_enabled_test.py @@ -0,0 +1,153 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.gmail.gmail_service import GmailPolicies +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + set_mocked_googleworkspace_provider, +) + + +class TestGmailEmployeeNameSpoofingProtectionEnabled: + def test_pass(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_employee_name_spoofing_protection_enabled.gmail_employee_name_spoofing_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_employee_name_spoofing_protection_enabled.gmail_employee_name_spoofing_protection_enabled import ( + gmail_employee_name_spoofing_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + detect_employee_name_spoofing=True, + employee_name_spoofing_consequence="SPAM_FOLDER", + ) + + check = gmail_employee_name_spoofing_protection_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "SPAM_FOLDER" in findings[0].status_extended + assert findings[0].resource_name == "Gmail Policies" + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_no_action(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_employee_name_spoofing_protection_enabled.gmail_employee_name_spoofing_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_employee_name_spoofing_protection_enabled.gmail_employee_name_spoofing_protection_enabled import ( + gmail_employee_name_spoofing_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + detect_employee_name_spoofing=True, + employee_name_spoofing_consequence="NO_ACTION", + ) + + check = gmail_employee_name_spoofing_protection_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "no action" in findings[0].status_extended + + def test_fail_protection_disabled(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_employee_name_spoofing_protection_enabled.gmail_employee_name_spoofing_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_employee_name_spoofing_protection_enabled.gmail_employee_name_spoofing_protection_enabled import ( + gmail_employee_name_spoofing_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + detect_employee_name_spoofing=False, + employee_name_spoofing_consequence="WARNING", + ) + + check = gmail_employee_name_spoofing_protection_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "disabled" in findings[0].status_extended + + def test_pass_using_default(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_employee_name_spoofing_protection_enabled.gmail_employee_name_spoofing_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_employee_name_spoofing_protection_enabled.gmail_employee_name_spoofing_protection_enabled import ( + gmail_employee_name_spoofing_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies() + + check = gmail_employee_name_spoofing_protection_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "secure default" in findings[0].status_extended + + def test_no_findings_when_fetch_failed(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_employee_name_spoofing_protection_enabled.gmail_employee_name_spoofing_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_employee_name_spoofing_protection_enabled.gmail_employee_name_spoofing_protection_enabled import ( + gmail_employee_name_spoofing_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = False + mock_client.policies = GmailPolicies() + + check = gmail_employee_name_spoofing_protection_enabled() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/gmail/gmail_encrypted_attachment_protection_enabled/gmail_encrypted_attachment_protection_enabled_test.py b/tests/providers/googleworkspace/services/gmail/gmail_encrypted_attachment_protection_enabled/gmail_encrypted_attachment_protection_enabled_test.py new file mode 100644 index 0000000000..cc41ff3f66 --- /dev/null +++ b/tests/providers/googleworkspace/services/gmail/gmail_encrypted_attachment_protection_enabled/gmail_encrypted_attachment_protection_enabled_test.py @@ -0,0 +1,153 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.gmail.gmail_service import GmailPolicies +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + set_mocked_googleworkspace_provider, +) + + +class TestGmailEncryptedAttachmentProtectionEnabled: + def test_pass(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_encrypted_attachment_protection_enabled.gmail_encrypted_attachment_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_encrypted_attachment_protection_enabled.gmail_encrypted_attachment_protection_enabled import ( + gmail_encrypted_attachment_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + enable_encrypted_attachment_protection=True, + encrypted_attachment_protection_consequence="QUARANTINE", + ) + + check = gmail_encrypted_attachment_protection_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "QUARANTINE" in findings[0].status_extended + assert findings[0].resource_name == "Gmail Policies" + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_no_action(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_encrypted_attachment_protection_enabled.gmail_encrypted_attachment_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_encrypted_attachment_protection_enabled.gmail_encrypted_attachment_protection_enabled import ( + gmail_encrypted_attachment_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + enable_encrypted_attachment_protection=True, + encrypted_attachment_protection_consequence="NO_ACTION", + ) + + check = gmail_encrypted_attachment_protection_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "no action" in findings[0].status_extended + + def test_fail_protection_disabled(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_encrypted_attachment_protection_enabled.gmail_encrypted_attachment_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_encrypted_attachment_protection_enabled.gmail_encrypted_attachment_protection_enabled import ( + gmail_encrypted_attachment_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + enable_encrypted_attachment_protection=False, + encrypted_attachment_protection_consequence="WARNING", + ) + + check = gmail_encrypted_attachment_protection_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "disabled" in findings[0].status_extended + + def test_pass_using_default(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_encrypted_attachment_protection_enabled.gmail_encrypted_attachment_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_encrypted_attachment_protection_enabled.gmail_encrypted_attachment_protection_enabled import ( + gmail_encrypted_attachment_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies() + + check = gmail_encrypted_attachment_protection_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "secure default" in findings[0].status_extended + + def test_no_findings_when_fetch_failed(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_encrypted_attachment_protection_enabled.gmail_encrypted_attachment_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_encrypted_attachment_protection_enabled.gmail_encrypted_attachment_protection_enabled import ( + gmail_encrypted_attachment_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = False + mock_client.policies = GmailPolicies() + + check = gmail_encrypted_attachment_protection_enabled() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/gmail/gmail_enhanced_pre_delivery_scanning_enabled/gmail_enhanced_pre_delivery_scanning_enabled_test.py b/tests/providers/googleworkspace/services/gmail/gmail_enhanced_pre_delivery_scanning_enabled/gmail_enhanced_pre_delivery_scanning_enabled_test.py index 140a41ec99..823f7ee899 100644 --- a/tests/providers/googleworkspace/services/gmail/gmail_enhanced_pre_delivery_scanning_enabled/gmail_enhanced_pre_delivery_scanning_enabled_test.py +++ b/tests/providers/googleworkspace/services/gmail/gmail_enhanced_pre_delivery_scanning_enabled/gmail_enhanced_pre_delivery_scanning_enabled_test.py @@ -3,7 +3,6 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.gmail.gmail_service import GmailPolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, - DOMAIN, set_mocked_googleworkspace_provider, ) @@ -37,7 +36,7 @@ class TestGmailEnhancedPreDeliveryScanningEnabled: assert len(findings) == 1 assert findings[0].status == "PASS" assert "enabled" in findings[0].status_extended - assert findings[0].resource_name == DOMAIN + assert findings[0].resource_name == "Gmail Policies" assert findings[0].customer_id == CUSTOMER_ID def test_fail_disabled(self): diff --git a/tests/providers/googleworkspace/services/gmail/gmail_external_image_scanning_enabled/gmail_external_image_scanning_enabled_test.py b/tests/providers/googleworkspace/services/gmail/gmail_external_image_scanning_enabled/gmail_external_image_scanning_enabled_test.py index a720df1c43..17b0bf7624 100644 --- a/tests/providers/googleworkspace/services/gmail/gmail_external_image_scanning_enabled/gmail_external_image_scanning_enabled_test.py +++ b/tests/providers/googleworkspace/services/gmail/gmail_external_image_scanning_enabled/gmail_external_image_scanning_enabled_test.py @@ -3,7 +3,6 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.gmail.gmail_service import GmailPolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, - DOMAIN, set_mocked_googleworkspace_provider, ) @@ -35,7 +34,7 @@ class TestGmailExternalImageScanningEnabled: assert len(findings) == 1 assert findings[0].status == "PASS" assert "enabled" in findings[0].status_extended - assert findings[0].resource_name == DOMAIN + assert findings[0].resource_name == "Gmail Policies" assert findings[0].customer_id == CUSTOMER_ID def test_fail_disabled(self): diff --git a/tests/providers/googleworkspace/services/gmail/gmail_groups_spoofing_protection_enabled/gmail_groups_spoofing_protection_enabled_test.py b/tests/providers/googleworkspace/services/gmail/gmail_groups_spoofing_protection_enabled/gmail_groups_spoofing_protection_enabled_test.py new file mode 100644 index 0000000000..b06092ebe5 --- /dev/null +++ b/tests/providers/googleworkspace/services/gmail/gmail_groups_spoofing_protection_enabled/gmail_groups_spoofing_protection_enabled_test.py @@ -0,0 +1,186 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.gmail.gmail_service import GmailPolicies +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + set_mocked_googleworkspace_provider, +) + + +class TestGmailGroupsSpoofingProtectionEnabled: + def test_pass(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_groups_spoofing_protection_enabled.gmail_groups_spoofing_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_groups_spoofing_protection_enabled.gmail_groups_spoofing_protection_enabled import ( + gmail_groups_spoofing_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + detect_groups_spoofing=True, + groups_spoofing_consequence="SPAM_FOLDER", + ) + + check = gmail_groups_spoofing_protection_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "all groups" in findings[0].status_extended + assert "SPAM_FOLDER" in findings[0].status_extended + assert findings[0].resource_name == "Gmail Policies" + assert findings[0].customer_id == CUSTOMER_ID + + def test_pass_private_groups_only(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_groups_spoofing_protection_enabled.gmail_groups_spoofing_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_groups_spoofing_protection_enabled.gmail_groups_spoofing_protection_enabled import ( + gmail_groups_spoofing_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + detect_groups_spoofing=True, + groups_spoofing_visibility_type="PRIVATE_GROUPS_ONLY", + groups_spoofing_consequence="SPAM_FOLDER", + ) + + check = gmail_groups_spoofing_protection_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "private groups only" in findings[0].status_extended + assert "SPAM_FOLDER" in findings[0].status_extended + + def test_fail_no_action(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_groups_spoofing_protection_enabled.gmail_groups_spoofing_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_groups_spoofing_protection_enabled.gmail_groups_spoofing_protection_enabled import ( + gmail_groups_spoofing_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + detect_groups_spoofing=True, + groups_spoofing_consequence="NO_ACTION", + ) + + check = gmail_groups_spoofing_protection_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "no action" in findings[0].status_extended + + def test_fail_protection_disabled(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_groups_spoofing_protection_enabled.gmail_groups_spoofing_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_groups_spoofing_protection_enabled.gmail_groups_spoofing_protection_enabled import ( + gmail_groups_spoofing_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + detect_groups_spoofing=False, + groups_spoofing_consequence="WARNING", + ) + + check = gmail_groups_spoofing_protection_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "disabled" in findings[0].status_extended + + def test_fail_no_policy_set(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_groups_spoofing_protection_enabled.gmail_groups_spoofing_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_groups_spoofing_protection_enabled.gmail_groups_spoofing_protection_enabled import ( + gmail_groups_spoofing_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies() + + check = gmail_groups_spoofing_protection_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "insecure default" in findings[0].status_extended + + def test_no_findings_when_fetch_failed(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_groups_spoofing_protection_enabled.gmail_groups_spoofing_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_groups_spoofing_protection_enabled.gmail_groups_spoofing_protection_enabled import ( + gmail_groups_spoofing_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = False + mock_client.policies = GmailPolicies() + + check = gmail_groups_spoofing_protection_enabled() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/gmail/gmail_inbound_domain_spoofing_protection_enabled/gmail_inbound_domain_spoofing_protection_enabled_test.py b/tests/providers/googleworkspace/services/gmail/gmail_inbound_domain_spoofing_protection_enabled/gmail_inbound_domain_spoofing_protection_enabled_test.py new file mode 100644 index 0000000000..b319b8fdb1 --- /dev/null +++ b/tests/providers/googleworkspace/services/gmail/gmail_inbound_domain_spoofing_protection_enabled/gmail_inbound_domain_spoofing_protection_enabled_test.py @@ -0,0 +1,153 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.gmail.gmail_service import GmailPolicies +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + set_mocked_googleworkspace_provider, +) + + +class TestGmailInboundDomainSpoofingProtectionEnabled: + def test_pass(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_inbound_domain_spoofing_protection_enabled.gmail_inbound_domain_spoofing_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_inbound_domain_spoofing_protection_enabled.gmail_inbound_domain_spoofing_protection_enabled import ( + gmail_inbound_domain_spoofing_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + detect_inbound_domain_spoofing=True, + inbound_domain_spoofing_consequence="QUARANTINE", + ) + + check = gmail_inbound_domain_spoofing_protection_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "QUARANTINE" in findings[0].status_extended + assert findings[0].resource_name == "Gmail Policies" + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_no_action(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_inbound_domain_spoofing_protection_enabled.gmail_inbound_domain_spoofing_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_inbound_domain_spoofing_protection_enabled.gmail_inbound_domain_spoofing_protection_enabled import ( + gmail_inbound_domain_spoofing_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + detect_inbound_domain_spoofing=True, + inbound_domain_spoofing_consequence="NO_ACTION", + ) + + check = gmail_inbound_domain_spoofing_protection_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "no action" in findings[0].status_extended + + def test_fail_protection_disabled(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_inbound_domain_spoofing_protection_enabled.gmail_inbound_domain_spoofing_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_inbound_domain_spoofing_protection_enabled.gmail_inbound_domain_spoofing_protection_enabled import ( + gmail_inbound_domain_spoofing_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + detect_inbound_domain_spoofing=False, + inbound_domain_spoofing_consequence="WARNING", + ) + + check = gmail_inbound_domain_spoofing_protection_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "disabled" in findings[0].status_extended + + def test_pass_using_default(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_inbound_domain_spoofing_protection_enabled.gmail_inbound_domain_spoofing_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_inbound_domain_spoofing_protection_enabled.gmail_inbound_domain_spoofing_protection_enabled import ( + gmail_inbound_domain_spoofing_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies() + + check = gmail_inbound_domain_spoofing_protection_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "secure default" in findings[0].status_extended + + def test_no_findings_when_fetch_failed(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_inbound_domain_spoofing_protection_enabled.gmail_inbound_domain_spoofing_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_inbound_domain_spoofing_protection_enabled.gmail_inbound_domain_spoofing_protection_enabled import ( + gmail_inbound_domain_spoofing_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = False + mock_client.policies = GmailPolicies() + + check = gmail_inbound_domain_spoofing_protection_enabled() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/gmail/gmail_mail_delegation_disabled/gmail_mail_delegation_disabled_test.py b/tests/providers/googleworkspace/services/gmail/gmail_mail_delegation_disabled/gmail_mail_delegation_disabled_test.py index f42062cc7e..e471d030ff 100644 --- a/tests/providers/googleworkspace/services/gmail/gmail_mail_delegation_disabled/gmail_mail_delegation_disabled_test.py +++ b/tests/providers/googleworkspace/services/gmail/gmail_mail_delegation_disabled/gmail_mail_delegation_disabled_test.py @@ -3,7 +3,6 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.gmail.gmail_service import GmailPolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, - DOMAIN, set_mocked_googleworkspace_provider, ) @@ -35,7 +34,7 @@ class TestGmailMailDelegationDisabled: assert len(findings) == 1 assert findings[0].status == "PASS" assert "disabled" in findings[0].status_extended - assert findings[0].resource_name == DOMAIN + assert findings[0].resource_name == "Gmail Policies" assert findings[0].customer_id == CUSTOMER_ID def test_fail_delegation_enabled(self): diff --git a/tests/providers/googleworkspace/services/gmail/gmail_per_user_outbound_gateway_disabled/gmail_per_user_outbound_gateway_disabled_test.py b/tests/providers/googleworkspace/services/gmail/gmail_per_user_outbound_gateway_disabled/gmail_per_user_outbound_gateway_disabled_test.py index 12c1267a07..2fe105df42 100644 --- a/tests/providers/googleworkspace/services/gmail/gmail_per_user_outbound_gateway_disabled/gmail_per_user_outbound_gateway_disabled_test.py +++ b/tests/providers/googleworkspace/services/gmail/gmail_per_user_outbound_gateway_disabled/gmail_per_user_outbound_gateway_disabled_test.py @@ -3,7 +3,6 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.gmail.gmail_service import GmailPolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, - DOMAIN, set_mocked_googleworkspace_provider, ) @@ -35,7 +34,7 @@ class TestGmailPerUserOutboundGatewayDisabled: assert len(findings) == 1 assert findings[0].status == "PASS" assert "disabled" in findings[0].status_extended - assert findings[0].resource_name == DOMAIN + assert findings[0].resource_name == "Gmail Policies" assert findings[0].customer_id == CUSTOMER_ID def test_fail_disabled(self): diff --git a/tests/providers/googleworkspace/services/gmail/gmail_pop_imap_access_disabled/gmail_pop_imap_access_disabled_test.py b/tests/providers/googleworkspace/services/gmail/gmail_pop_imap_access_disabled/gmail_pop_imap_access_disabled_test.py index 58c939b4f6..d5f87ac2c8 100644 --- a/tests/providers/googleworkspace/services/gmail/gmail_pop_imap_access_disabled/gmail_pop_imap_access_disabled_test.py +++ b/tests/providers/googleworkspace/services/gmail/gmail_pop_imap_access_disabled/gmail_pop_imap_access_disabled_test.py @@ -3,7 +3,6 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.gmail.gmail_service import GmailPolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, - DOMAIN, set_mocked_googleworkspace_provider, ) @@ -37,7 +36,7 @@ class TestGmailPopImapAccessDisabled: assert len(findings) == 1 assert findings[0].status == "PASS" assert "disabled" in findings[0].status_extended - assert findings[0].resource_name == DOMAIN + assert findings[0].resource_name == "Gmail Policies" assert findings[0].customer_id == CUSTOMER_ID def test_fail_both_enabled(self): diff --git a/tests/providers/googleworkspace/services/gmail/gmail_script_attachment_protection_enabled/gmail_script_attachment_protection_enabled_test.py b/tests/providers/googleworkspace/services/gmail/gmail_script_attachment_protection_enabled/gmail_script_attachment_protection_enabled_test.py new file mode 100644 index 0000000000..9f04178457 --- /dev/null +++ b/tests/providers/googleworkspace/services/gmail/gmail_script_attachment_protection_enabled/gmail_script_attachment_protection_enabled_test.py @@ -0,0 +1,153 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.gmail.gmail_service import GmailPolicies +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + set_mocked_googleworkspace_provider, +) + + +class TestGmailScriptAttachmentProtectionEnabled: + def test_pass(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_script_attachment_protection_enabled.gmail_script_attachment_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_script_attachment_protection_enabled.gmail_script_attachment_protection_enabled import ( + gmail_script_attachment_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + enable_script_attachment_protection=True, + script_attachment_protection_consequence="QUARANTINE", + ) + + check = gmail_script_attachment_protection_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "QUARANTINE" in findings[0].status_extended + assert findings[0].resource_name == "Gmail Policies" + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_no_action(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_script_attachment_protection_enabled.gmail_script_attachment_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_script_attachment_protection_enabled.gmail_script_attachment_protection_enabled import ( + gmail_script_attachment_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + enable_script_attachment_protection=True, + script_attachment_protection_consequence="NO_ACTION", + ) + + check = gmail_script_attachment_protection_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "no action" in findings[0].status_extended + + def test_fail_protection_disabled(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_script_attachment_protection_enabled.gmail_script_attachment_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_script_attachment_protection_enabled.gmail_script_attachment_protection_enabled import ( + gmail_script_attachment_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + enable_script_attachment_protection=False, + script_attachment_protection_consequence="WARNING", + ) + + check = gmail_script_attachment_protection_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "disabled" in findings[0].status_extended + + def test_pass_using_default(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_script_attachment_protection_enabled.gmail_script_attachment_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_script_attachment_protection_enabled.gmail_script_attachment_protection_enabled import ( + gmail_script_attachment_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies() + + check = gmail_script_attachment_protection_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "secure default" in findings[0].status_extended + + def test_no_findings_when_fetch_failed(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_script_attachment_protection_enabled.gmail_script_attachment_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_script_attachment_protection_enabled.gmail_script_attachment_protection_enabled import ( + gmail_script_attachment_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = False + mock_client.policies = GmailPolicies() + + check = gmail_script_attachment_protection_enabled() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/gmail/gmail_service_test.py b/tests/providers/googleworkspace/services/gmail/gmail_service_test.py index ca1c4c249f..cdfa29ed14 100644 --- a/tests/providers/googleworkspace/services/gmail/gmail_service_test.py +++ b/tests/providers/googleworkspace/services/gmail/gmail_service_test.py @@ -34,8 +34,11 @@ class TestGmailService: "setting": { "type": "settings/gmail.email_attachment_safety", "value": { + "enableEncryptedAttachmentProtection": True, "encryptedAttachmentProtectionConsequence": "SPAM_FOLDER", + "enableAttachmentWithScriptsProtection": True, "scriptAttachmentProtectionConsequence": "QUARANTINE", + "enableAnomalousAttachmentProtection": True, "anomalousAttachmentProtectionConsequence": "WARNING", }, } @@ -54,10 +57,15 @@ class TestGmailService: "setting": { "type": "settings/gmail.spoofing_and_authentication", "value": { + "detectDomainNameSpoofing": True, "domainSpoofingConsequence": "SPAM_FOLDER", + "detectEmployeeNameSpoofing": True, "employeeNameSpoofingConsequence": "SPAM_FOLDER", + "detectDomainSpoofingFromUnauthenticatedSenders": True, "inboundDomainSpoofingConsequence": "QUARANTINE", + "detectUnauthenticatedEmails": True, "unauthenticatedEmailConsequence": "WARNING", + "detectGroupsSpoofing": True, "groupsSpoofingConsequence": "SPAM_FOLDER", }, } @@ -121,23 +129,31 @@ class TestGmailService: assert gmail.policies_fetched is True assert gmail.policies.enable_mail_delegation is False + assert gmail.policies.enable_encrypted_attachment_protection is True assert ( gmail.policies.encrypted_attachment_protection_consequence == "SPAM_FOLDER" ) + assert gmail.policies.enable_script_attachment_protection is True assert ( gmail.policies.script_attachment_protection_consequence == "QUARANTINE" ) + assert gmail.policies.enable_anomalous_attachment_protection is True assert ( gmail.policies.anomalous_attachment_protection_consequence == "WARNING" ) assert gmail.policies.enable_shortener_scanning is True assert gmail.policies.enable_external_image_scanning is True assert gmail.policies.enable_aggressive_warnings_on_untrusted_links is True + assert gmail.policies.detect_domain_name_spoofing is True assert gmail.policies.domain_spoofing_consequence == "SPAM_FOLDER" + assert gmail.policies.detect_employee_name_spoofing is True assert gmail.policies.employee_name_spoofing_consequence == "SPAM_FOLDER" + assert gmail.policies.detect_inbound_domain_spoofing is True assert gmail.policies.inbound_domain_spoofing_consequence == "QUARANTINE" + assert gmail.policies.detect_unauthenticated_emails is True assert gmail.policies.unauthenticated_email_consequence == "WARNING" + assert gmail.policies.detect_groups_spoofing is True assert gmail.policies.groups_spoofing_consequence == "SPAM_FOLDER" assert gmail.policies.enable_pop_access is False assert gmail.policies.enable_imap_access is False @@ -464,16 +480,24 @@ class TestGmailService: policies = GmailPolicies( enable_mail_delegation=False, + enable_encrypted_attachment_protection=True, encrypted_attachment_protection_consequence="SPAM_FOLDER", + enable_script_attachment_protection=True, script_attachment_protection_consequence="QUARANTINE", + enable_anomalous_attachment_protection=True, anomalous_attachment_protection_consequence="WARNING", enable_shortener_scanning=True, enable_external_image_scanning=True, enable_aggressive_warnings_on_untrusted_links=True, + detect_domain_name_spoofing=True, domain_spoofing_consequence="SPAM_FOLDER", + detect_employee_name_spoofing=True, employee_name_spoofing_consequence="SPAM_FOLDER", + detect_inbound_domain_spoofing=True, inbound_domain_spoofing_consequence="QUARANTINE", + detect_unauthenticated_emails=True, unauthenticated_email_consequence="WARNING", + detect_groups_spoofing=True, groups_spoofing_consequence="SPAM_FOLDER", enable_pop_access=False, enable_imap_access=False, @@ -484,8 +508,10 @@ class TestGmailService: ) assert policies.enable_mail_delegation is False + assert policies.enable_encrypted_attachment_protection is True assert policies.encrypted_attachment_protection_consequence == "SPAM_FOLDER" assert policies.enable_shortener_scanning is True + assert policies.detect_domain_name_spoofing is True assert policies.domain_spoofing_consequence == "SPAM_FOLDER" assert policies.enable_pop_access is False assert policies.enable_auto_forwarding is False diff --git a/tests/providers/googleworkspace/services/gmail/gmail_shortener_scanning_enabled/gmail_shortener_scanning_enabled_test.py b/tests/providers/googleworkspace/services/gmail/gmail_shortener_scanning_enabled/gmail_shortener_scanning_enabled_test.py index adfde86db6..11fbe29098 100644 --- a/tests/providers/googleworkspace/services/gmail/gmail_shortener_scanning_enabled/gmail_shortener_scanning_enabled_test.py +++ b/tests/providers/googleworkspace/services/gmail/gmail_shortener_scanning_enabled/gmail_shortener_scanning_enabled_test.py @@ -3,7 +3,6 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.gmail.gmail_service import GmailPolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, - DOMAIN, set_mocked_googleworkspace_provider, ) @@ -35,7 +34,7 @@ class TestGmailShortenerScanningEnabled: assert len(findings) == 1 assert findings[0].status == "PASS" assert "enabled" in findings[0].status_extended - assert findings[0].resource_name == DOMAIN + assert findings[0].resource_name == "Gmail Policies" assert findings[0].customer_id == CUSTOMER_ID def test_fail_disabled(self): diff --git a/tests/providers/googleworkspace/services/gmail/gmail_unauthenticated_email_protection_enabled/gmail_unauthenticated_email_protection_enabled_test.py b/tests/providers/googleworkspace/services/gmail/gmail_unauthenticated_email_protection_enabled/gmail_unauthenticated_email_protection_enabled_test.py new file mode 100644 index 0000000000..3a8fd0d337 --- /dev/null +++ b/tests/providers/googleworkspace/services/gmail/gmail_unauthenticated_email_protection_enabled/gmail_unauthenticated_email_protection_enabled_test.py @@ -0,0 +1,153 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.gmail.gmail_service import GmailPolicies +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + set_mocked_googleworkspace_provider, +) + + +class TestGmailUnauthenticatedEmailProtectionEnabled: + def test_pass(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_unauthenticated_email_protection_enabled.gmail_unauthenticated_email_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_unauthenticated_email_protection_enabled.gmail_unauthenticated_email_protection_enabled import ( + gmail_unauthenticated_email_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + detect_unauthenticated_emails=True, + unauthenticated_email_consequence="WARNING", + ) + + check = gmail_unauthenticated_email_protection_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "WARNING" in findings[0].status_extended + assert findings[0].resource_name == "Gmail Policies" + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_no_action(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_unauthenticated_email_protection_enabled.gmail_unauthenticated_email_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_unauthenticated_email_protection_enabled.gmail_unauthenticated_email_protection_enabled import ( + gmail_unauthenticated_email_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + detect_unauthenticated_emails=True, + unauthenticated_email_consequence="NO_ACTION", + ) + + check = gmail_unauthenticated_email_protection_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "no action" in findings[0].status_extended + + def test_fail_protection_disabled(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_unauthenticated_email_protection_enabled.gmail_unauthenticated_email_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_unauthenticated_email_protection_enabled.gmail_unauthenticated_email_protection_enabled import ( + gmail_unauthenticated_email_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies( + detect_unauthenticated_emails=False, + unauthenticated_email_consequence="WARNING", + ) + + check = gmail_unauthenticated_email_protection_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "disabled" in findings[0].status_extended + + def test_fail_no_policy_set(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_unauthenticated_email_protection_enabled.gmail_unauthenticated_email_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_unauthenticated_email_protection_enabled.gmail_unauthenticated_email_protection_enabled import ( + gmail_unauthenticated_email_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GmailPolicies() + + check = gmail_unauthenticated_email_protection_enabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "insecure default" in findings[0].status_extended + + def test_no_findings_when_fetch_failed(self): + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.gmail.gmail_unauthenticated_email_protection_enabled.gmail_unauthenticated_email_protection_enabled.gmail_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.gmail.gmail_unauthenticated_email_protection_enabled.gmail_unauthenticated_email_protection_enabled import ( + gmail_unauthenticated_email_protection_enabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = False + mock_client.policies = GmailPolicies() + + check = gmail_unauthenticated_email_protection_enabled() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/gmail/gmail_untrusted_link_warnings_enabled/gmail_untrusted_link_warnings_enabled_test.py b/tests/providers/googleworkspace/services/gmail/gmail_untrusted_link_warnings_enabled/gmail_untrusted_link_warnings_enabled_test.py index 611848b12f..70fc539836 100644 --- a/tests/providers/googleworkspace/services/gmail/gmail_untrusted_link_warnings_enabled/gmail_untrusted_link_warnings_enabled_test.py +++ b/tests/providers/googleworkspace/services/gmail/gmail_untrusted_link_warnings_enabled/gmail_untrusted_link_warnings_enabled_test.py @@ -3,7 +3,6 @@ from unittest.mock import patch from prowler.providers.googleworkspace.services.gmail.gmail_service import GmailPolicies from tests.providers.googleworkspace.googleworkspace_fixtures import ( CUSTOMER_ID, - DOMAIN, set_mocked_googleworkspace_provider, ) @@ -37,7 +36,7 @@ class TestGmailUntrustedLinkWarningsEnabled: assert len(findings) == 1 assert findings[0].status == "PASS" assert "enabled" in findings[0].status_extended - assert findings[0].resource_name == DOMAIN + assert findings[0].resource_name == "Gmail Policies" assert findings[0].customer_id == CUSTOMER_ID def test_fail_disabled(self): @@ -69,7 +68,7 @@ class TestGmailUntrustedLinkWarningsEnabled: assert findings[0].status == "FAIL" assert "disabled" in findings[0].status_extended - def test_pass_using_default(self): + def test_fail_insecure_default(self): mock_provider = set_mocked_googleworkspace_provider() with ( @@ -95,8 +94,8 @@ class TestGmailUntrustedLinkWarningsEnabled: findings = check.execute() assert len(findings) == 1 - assert findings[0].status == "PASS" - assert "secure default" in findings[0].status_extended + assert findings[0].status == "FAIL" + assert "insecure default" in findings[0].status_extended def test_no_findings_when_fetch_failed(self): mock_provider = set_mocked_googleworkspace_provider() diff --git a/tests/providers/googleworkspace/services/groups/groups_creation_restricted/groups_creation_restricted_test.py b/tests/providers/googleworkspace/services/groups/groups_creation_restricted/groups_creation_restricted_test.py new file mode 100644 index 0000000000..b91457f7b0 --- /dev/null +++ b/tests/providers/googleworkspace/services/groups/groups_creation_restricted/groups_creation_restricted_test.py @@ -0,0 +1,271 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.groups.groups_service import ( + GroupsPolicies, +) +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + set_mocked_googleworkspace_provider, +) + + +class TestGroupsCreationRestricted: + def test_pass_all_restricted(self): + """Test PASS when all creation settings are secure""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.groups.groups_creation_restricted.groups_creation_restricted.groups_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.groups.groups_creation_restricted.groups_creation_restricted import ( + groups_creation_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GroupsPolicies( + create_groups_access_level="ADMIN_ONLY", + owners_can_allow_external_members=False, + owners_can_allow_incoming_mail_from_public=False, + ) + + check = groups_creation_restricted() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "properly restricted" in findings[0].status_extended + assert findings[0].resource_name == "Groups Policies" + assert findings[0].resource_id == "groupsPolicies" + assert findings[0].customer_id == CUSTOMER_ID + assert ( + findings[0].resource + == GroupsPolicies( + create_groups_access_level="ADMIN_ONLY", + owners_can_allow_external_members=False, + owners_can_allow_incoming_mail_from_public=False, + ).dict() + ) + + def test_fail_users_in_domain(self): + """Test FAIL when anyone in org can create groups""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.groups.groups_creation_restricted.groups_creation_restricted.groups_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.groups.groups_creation_restricted.groups_creation_restricted import ( + groups_creation_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GroupsPolicies( + create_groups_access_level="USERS_IN_DOMAIN", + owners_can_allow_external_members=False, + owners_can_allow_incoming_mail_from_public=False, + ) + + check = groups_creation_restricted() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "USERS_IN_DOMAIN" in findings[0].status_extended + + def test_fail_external_members_allowed(self): + """Test FAIL when external members are allowed""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.groups.groups_creation_restricted.groups_creation_restricted.groups_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.groups.groups_creation_restricted.groups_creation_restricted import ( + groups_creation_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GroupsPolicies( + create_groups_access_level="ADMIN_ONLY", + owners_can_allow_external_members=True, + owners_can_allow_incoming_mail_from_public=False, + ) + + check = groups_creation_restricted() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "external members" in findings[0].status_extended + + def test_fail_incoming_mail_allowed(self): + """Test FAIL when incoming email from outside is allowed""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.groups.groups_creation_restricted.groups_creation_restricted.groups_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.groups.groups_creation_restricted.groups_creation_restricted import ( + groups_creation_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GroupsPolicies( + create_groups_access_level="ADMIN_ONLY", + owners_can_allow_external_members=False, + owners_can_allow_incoming_mail_from_public=True, + ) + + check = groups_creation_restricted() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "incoming email" in findings[0].status_extended + + def test_fail_all_defaults_none(self): + """Test FAIL when all settings are None — only USERS_IN_DOMAIN default is insecure""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.groups.groups_creation_restricted.groups_creation_restricted.groups_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.groups.groups_creation_restricted.groups_creation_restricted import ( + groups_creation_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GroupsPolicies() + + check = groups_creation_restricted() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + # Only creation access level has an insecure default (USERS_IN_DOMAIN) + assert "ADMIN_ONLY" in findings[0].status_extended + assert "incoming email" not in findings[0].status_extended + assert "external members" not in findings[0].status_extended + + def test_pass_admin_only_others_none(self): + """Test PASS when creation is ADMIN_ONLY and boolean settings are None (secure defaults)""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.groups.groups_creation_restricted.groups_creation_restricted.groups_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.groups.groups_creation_restricted.groups_creation_restricted import ( + groups_creation_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GroupsPolicies( + create_groups_access_level="ADMIN_ONLY", + ) + + check = groups_creation_restricted() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "properly restricted" in findings[0].status_extended + + def test_fail_multiple_issues(self): + """Test FAIL with all three sub-settings non-compliant""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.groups.groups_creation_restricted.groups_creation_restricted.groups_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.groups.groups_creation_restricted.groups_creation_restricted import ( + groups_creation_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GroupsPolicies( + create_groups_access_level="ANYONE_CAN_CREATE", + owners_can_allow_external_members=True, + owners_can_allow_incoming_mail_from_public=True, + ) + + check = groups_creation_restricted() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "ANYONE_CAN_CREATE" in findings[0].status_extended + assert "external members" in findings[0].status_extended + assert "incoming email" in findings[0].status_extended + + def test_no_findings_when_fetch_failed(self): + """Test no findings returned when the API fetch failed""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.groups.groups_creation_restricted.groups_creation_restricted.groups_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.groups.groups_creation_restricted.groups_creation_restricted import ( + groups_creation_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = False + mock_client.policies = GroupsPolicies() + + check = groups_creation_restricted() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/groups/groups_external_access_restricted/groups_external_access_restricted_test.py b/tests/providers/googleworkspace/services/groups/groups_external_access_restricted/groups_external_access_restricted_test.py new file mode 100644 index 0000000000..ec25683728 --- /dev/null +++ b/tests/providers/googleworkspace/services/groups/groups_external_access_restricted/groups_external_access_restricted_test.py @@ -0,0 +1,132 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.groups.groups_service import ( + GroupsPolicies, +) +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + set_mocked_googleworkspace_provider, +) + + +class TestGroupsExternalAccessRestricted: + def test_pass_domain_users_only(self): + """Test PASS when external access is set to DOMAIN_USERS_ONLY""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.groups.groups_external_access_restricted.groups_external_access_restricted.groups_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.groups.groups_external_access_restricted.groups_external_access_restricted import ( + groups_external_access_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GroupsPolicies( + collaboration_capability="DOMAIN_USERS_ONLY" + ) + + check = groups_external_access_restricted() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "private" in findings[0].status_extended + assert findings[0].resource_name == "Groups Policies" + assert findings[0].resource_id == "groupsPolicies" + assert findings[0].customer_id == CUSTOMER_ID + assert ( + findings[0].resource + == GroupsPolicies(collaboration_capability="DOMAIN_USERS_ONLY").dict() + ) + + def test_fail_anyone_can_access(self): + """Test FAIL when external access is set to ANYONE_CAN_ACCESS""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.groups.groups_external_access_restricted.groups_external_access_restricted.groups_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.groups.groups_external_access_restricted.groups_external_access_restricted import ( + groups_external_access_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GroupsPolicies( + collaboration_capability="ANYONE_CAN_ACCESS" + ) + + check = groups_external_access_restricted() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "ANYONE_CAN_ACCESS" in findings[0].status_extended + + def test_pass_no_policy_set(self): + """Test PASS when no explicit policy is set (None) - Google default is DOMAIN_USERS_ONLY (secure)""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.groups.groups_external_access_restricted.groups_external_access_restricted.groups_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.groups.groups_external_access_restricted.groups_external_access_restricted import ( + groups_external_access_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GroupsPolicies(collaboration_capability=None) + + check = groups_external_access_restricted() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "secure default" in findings[0].status_extended + + def test_no_findings_when_fetch_failed(self): + """Test no findings returned when the API fetch failed""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.groups.groups_external_access_restricted.groups_external_access_restricted.groups_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.groups.groups_external_access_restricted.groups_external_access_restricted import ( + groups_external_access_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = False + mock_client.policies = GroupsPolicies() + + check = groups_external_access_restricted() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/groups/groups_service_test.py b/tests/providers/googleworkspace/services/groups/groups_service_test.py new file mode 100644 index 0000000000..86b031a8a2 --- /dev/null +++ b/tests/providers/googleworkspace/services/groups/groups_service_test.py @@ -0,0 +1,287 @@ +from unittest.mock import MagicMock, patch + +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + set_mocked_googleworkspace_provider, +) + + +class TestGroupsService: + def test_fetch_policies_all_settings(self): + """Test fetching all Groups for Business policy settings from Cloud Identity API""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_credentials = MagicMock() + mock_session = MagicMock() + mock_session.credentials = mock_credentials + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = { + "policies": [ + { + "setting": { + "type": "settings/groups_for_business.groups_sharing", + "value": { + "collaborationCapability": "DOMAIN_USERS_ONLY", + "createGroupsAccessLevel": "ADMIN_ONLY", + "ownersCanAllowExternalMembers": False, + "ownersCanAllowIncomingMailFromPublic": False, + "viewTopicsDefaultAccessLevel": "GROUP_MEMBERS", + "ownersCanHideGroups": False, + "newGroupsAreHidden": False, + }, + } + }, + ] + } + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.groups.groups_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.groups.groups_service import ( + Groups, + ) + + groups = Groups(mock_provider) + + assert groups.policies_fetched is True + assert groups.policies.collaboration_capability == "DOMAIN_USERS_ONLY" + assert groups.policies.create_groups_access_level == "ADMIN_ONLY" + assert groups.policies.owners_can_allow_external_members is False + assert groups.policies.owners_can_allow_incoming_mail_from_public is False + assert groups.policies.view_topics_default_access_level == "GROUP_MEMBERS" + assert groups.policies.owners_can_hide_groups is False + assert groups.policies.new_groups_are_hidden is False + + def test_fetch_policies_empty_response(self): + """Test handling empty policies response""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = {"policies": []} + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.groups.groups_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.groups.groups_service import ( + Groups, + ) + + groups = Groups(mock_provider) + + assert groups.policies_fetched is True + assert groups.policies.collaboration_capability is None + assert groups.policies.create_groups_access_level is None + assert groups.policies.owners_can_allow_external_members is None + assert groups.policies.owners_can_allow_incoming_mail_from_public is None + assert groups.policies.view_topics_default_access_level is None + + def test_fetch_policies_api_error(self): + """Test handling of API errors during policy fetch""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_service.policies().list.side_effect = Exception("API Error") + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.groups.groups_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.groups.groups_service import ( + Groups, + ) + + groups = Groups(mock_provider) + + assert groups.policies_fetched is False + assert groups.policies.collaboration_capability is None + + def test_fetch_policies_build_service_returns_none(self): + """Test early return when _build_service fails to construct the client""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.groups.groups_service.GoogleWorkspaceService._build_service", + return_value=None, + ), + ): + from prowler.providers.googleworkspace.services.groups.groups_service import ( + Groups, + ) + + groups = Groups(mock_provider) + + assert groups.policies_fetched is False + assert groups.policies.collaboration_capability is None + + def test_fetch_policies_execute_raises(self): + """Test inner except handler when request.execute() raises during pagination""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_request = MagicMock() + mock_request.execute.side_effect = Exception("Execute failed") + mock_service.policies().list.return_value = mock_request + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.groups.groups_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.groups.groups_service import ( + Groups, + ) + + groups = Groups(mock_provider) + + assert groups.policies_fetched is False + assert groups.policies.collaboration_capability is None + + def test_fetch_policies_ignores_ou_and_group_level(self): + """Test that OU-level and group-level policies are skipped, only customer-level used""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = { + "policies": [ + { + # Customer-level: no policyQuery → should be used + "setting": { + "type": "settings/groups_for_business.groups_sharing", + "value": { + "collaborationCapability": "DOMAIN_USERS_ONLY", + "createGroupsAccessLevel": "ADMIN_ONLY", + }, + } + }, + { + # OU-level: has policyQuery.orgUnit → should be skipped + "policyQuery": {"orgUnit": "orgUnits/sales_team"}, + "setting": { + "type": "settings/groups_for_business.groups_sharing", + "value": { + "collaborationCapability": "ANYONE_CAN_ACCESS", + }, + }, + }, + { + # Group-level: has policyQuery.group → should be skipped + "policyQuery": {"group": "groups/contractors"}, + "setting": { + "type": "settings/groups_for_business.groups_sharing", + "value": { + "collaborationCapability": "ANYONE_CAN_ACCESS", + }, + }, + }, + ] + } + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.groups.groups_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.groups.groups_service import ( + Groups, + ) + + groups = Groups(mock_provider) + + assert groups.policies_fetched is True + assert groups.policies.collaboration_capability == "DOMAIN_USERS_ONLY" + assert groups.policies.create_groups_access_level == "ADMIN_ONLY" + + def test_policies_model(self): + """Test GroupsPolicies Pydantic model""" + from prowler.providers.googleworkspace.services.groups.groups_service import ( + GroupsPolicies, + ) + + policies = GroupsPolicies( + collaboration_capability="DOMAIN_USERS_ONLY", + create_groups_access_level="ADMIN_ONLY", + owners_can_allow_external_members=False, + owners_can_allow_incoming_mail_from_public=False, + view_topics_default_access_level="GROUP_MEMBERS", + owners_can_hide_groups=False, + new_groups_are_hidden=False, + ) + + assert policies.collaboration_capability == "DOMAIN_USERS_ONLY" + assert policies.create_groups_access_level == "ADMIN_ONLY" + assert policies.owners_can_allow_external_members is False + assert policies.owners_can_allow_incoming_mail_from_public is False + assert policies.view_topics_default_access_level == "GROUP_MEMBERS" + assert policies.owners_can_hide_groups is False + assert policies.new_groups_are_hidden is False diff --git a/tests/providers/googleworkspace/services/groups/groups_view_conversations_restricted/groups_view_conversations_restricted_test.py b/tests/providers/googleworkspace/services/groups/groups_view_conversations_restricted/groups_view_conversations_restricted_test.py new file mode 100644 index 0000000000..761073cc89 --- /dev/null +++ b/tests/providers/googleworkspace/services/groups/groups_view_conversations_restricted/groups_view_conversations_restricted_test.py @@ -0,0 +1,165 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.groups.groups_service import ( + GroupsPolicies, +) +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + set_mocked_googleworkspace_provider, +) + + +class TestGroupsViewConversationsRestricted: + def test_pass_group_members(self): + """Test PASS when view conversations is set to GROUP_MEMBERS""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.groups.groups_view_conversations_restricted.groups_view_conversations_restricted.groups_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.groups.groups_view_conversations_restricted.groups_view_conversations_restricted import ( + groups_view_conversations_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GroupsPolicies( + view_topics_default_access_level="GROUP_MEMBERS" + ) + + check = groups_view_conversations_restricted() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "group members" in findings[0].status_extended + assert findings[0].resource_name == "Groups Policies" + assert findings[0].resource_id == "groupsPolicies" + assert findings[0].customer_id == CUSTOMER_ID + assert ( + findings[0].resource + == GroupsPolicies( + view_topics_default_access_level="GROUP_MEMBERS" + ).dict() + ) + + def test_fail_domain_users(self): + """Test FAIL when view conversations is set to DOMAIN_USERS""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.groups.groups_view_conversations_restricted.groups_view_conversations_restricted.groups_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.groups.groups_view_conversations_restricted.groups_view_conversations_restricted import ( + groups_view_conversations_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GroupsPolicies( + view_topics_default_access_level="DOMAIN_USERS" + ) + + check = groups_view_conversations_restricted() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "DOMAIN_USERS" in findings[0].status_extended + + def test_fail_anyone_can_view(self): + """Test FAIL when view conversations is set to ANYONE_CAN_VIEW_TOPICS""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.groups.groups_view_conversations_restricted.groups_view_conversations_restricted.groups_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.groups.groups_view_conversations_restricted.groups_view_conversations_restricted import ( + groups_view_conversations_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GroupsPolicies( + view_topics_default_access_level="ANYONE_CAN_VIEW_TOPICS" + ) + + check = groups_view_conversations_restricted() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "ANYONE_CAN_VIEW_TOPICS" in findings[0].status_extended + + def test_fail_no_policy_set(self): + """Test FAIL when no explicit policy is set (None) - Google default is DOMAIN_USERS (insecure)""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.groups.groups_view_conversations_restricted.groups_view_conversations_restricted.groups_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.groups.groups_view_conversations_restricted.groups_view_conversations_restricted import ( + groups_view_conversations_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = GroupsPolicies(view_topics_default_access_level=None) + + check = groups_view_conversations_restricted() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "default" in findings[0].status_extended + assert "all organization users" in findings[0].status_extended + + def test_no_findings_when_fetch_failed(self): + """Test no findings returned when the API fetch failed""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.groups.groups_view_conversations_restricted.groups_view_conversations_restricted.groups_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.groups.groups_view_conversations_restricted.groups_view_conversations_restricted import ( + groups_view_conversations_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = False + mock_client.policies = GroupsPolicies() + + check = groups_view_conversations_restricted() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/marketplace/marketplace_apps_access_restricted/marketplace_apps_access_restricted_test.py b/tests/providers/googleworkspace/services/marketplace/marketplace_apps_access_restricted/marketplace_apps_access_restricted_test.py new file mode 100644 index 0000000000..8da2a4326d --- /dev/null +++ b/tests/providers/googleworkspace/services/marketplace/marketplace_apps_access_restricted/marketplace_apps_access_restricted_test.py @@ -0,0 +1,152 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.marketplace.marketplace_service import ( + MarketplacePolicies, +) +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + set_mocked_googleworkspace_provider, +) + + +class TestMarketplaceAppsAccessRestricted: + def test_pass_allow_listed_apps(self): + """Test PASS when Marketplace access is restricted to admin-approved apps""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.marketplace.marketplace_apps_access_restricted.marketplace_apps_access_restricted.marketplace_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.marketplace.marketplace_apps_access_restricted.marketplace_apps_access_restricted import ( + marketplace_apps_access_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = MarketplacePolicies(access_level="ALLOW_LISTED_APPS") + + check = marketplace_apps_access_restricted() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "restricted" in findings[0].status_extended + assert findings[0].resource_name == "Marketplace Policies" + assert findings[0].resource_id == "marketplacePolicies" + assert findings[0].customer_id == CUSTOMER_ID + + def test_pass_allow_none(self): + """Test PASS when Marketplace app installation is fully blocked""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.marketplace.marketplace_apps_access_restricted.marketplace_apps_access_restricted.marketplace_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.marketplace.marketplace_apps_access_restricted.marketplace_apps_access_restricted import ( + marketplace_apps_access_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = MarketplacePolicies(access_level="ALLOW_NONE") + + check = marketplace_apps_access_restricted() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "blocked" in findings[0].status_extended + + def test_fail_allow_all(self): + """Test FAIL when Marketplace allows all apps""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.marketplace.marketplace_apps_access_restricted.marketplace_apps_access_restricted.marketplace_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.marketplace.marketplace_apps_access_restricted.marketplace_apps_access_restricted import ( + marketplace_apps_access_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = MarketplacePolicies(access_level="ALLOW_ALL") + + check = marketplace_apps_access_restricted() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "any app" in findings[0].status_extended + + def test_fail_no_policy_set(self): + """Test FAIL when no explicit policy is set (None) - Google default is ALLOW_ALL (insecure)""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.marketplace.marketplace_apps_access_restricted.marketplace_apps_access_restricted.marketplace_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.marketplace.marketplace_apps_access_restricted.marketplace_apps_access_restricted import ( + marketplace_apps_access_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = MarketplacePolicies(access_level=None) + + check = marketplace_apps_access_restricted() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "not explicitly configured" in findings[0].status_extended + + def test_no_findings_when_fetch_failed(self): + """Test no findings returned when the API fetch failed""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.marketplace.marketplace_apps_access_restricted.marketplace_apps_access_restricted.marketplace_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.marketplace.marketplace_apps_access_restricted.marketplace_apps_access_restricted import ( + marketplace_apps_access_restricted, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = False + mock_client.policies = MarketplacePolicies() + + check = marketplace_apps_access_restricted() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/marketplace/marketplace_service_test.py b/tests/providers/googleworkspace/services/marketplace/marketplace_service_test.py new file mode 100644 index 0000000000..8c549cd0d7 --- /dev/null +++ b/tests/providers/googleworkspace/services/marketplace/marketplace_service_test.py @@ -0,0 +1,279 @@ +from unittest.mock import MagicMock, patch + +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + set_mocked_googleworkspace_provider, +) + + +class TestMarketplaceService: + def test_fetch_policies_allow_all(self): + """Test fetching Marketplace policy with ALLOW_ALL access level""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_credentials = MagicMock() + mock_session = MagicMock() + mock_session.credentials = mock_credentials + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = { + "policies": [ + { + "setting": { + "type": "settings/workspace_marketplace.apps_access_options", + "value": { + "accessLevel": "ALLOW_ALL", + }, + } + }, + ] + } + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.marketplace.marketplace_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.marketplace.marketplace_service import ( + Marketplace, + ) + + marketplace = Marketplace(mock_provider) + + assert marketplace.policies_fetched is True + assert marketplace.policies.access_level == "ALLOW_ALL" + + def test_fetch_policies_allow_listed_apps(self): + """Test fetching Marketplace policy with ALLOW_LISTED_APPS access level""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = { + "policies": [ + { + "setting": { + "type": "settings/workspace_marketplace.apps_access_options", + "value": { + "accessLevel": "ALLOW_LISTED_APPS", + }, + } + }, + ] + } + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.marketplace.marketplace_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.marketplace.marketplace_service import ( + Marketplace, + ) + + marketplace = Marketplace(mock_provider) + + assert marketplace.policies_fetched is True + assert marketplace.policies.access_level == "ALLOW_LISTED_APPS" + + def test_fetch_policies_allow_none(self): + """Test fetching Marketplace policy with ALLOW_NONE access level""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = { + "policies": [ + { + "setting": { + "type": "settings/workspace_marketplace.apps_access_options", + "value": { + "accessLevel": "ALLOW_NONE", + }, + } + }, + ] + } + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.marketplace.marketplace_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.marketplace.marketplace_service import ( + Marketplace, + ) + + marketplace = Marketplace(mock_provider) + + assert marketplace.policies_fetched is True + assert marketplace.policies.access_level == "ALLOW_NONE" + + def test_fetch_policies_empty_response(self): + """Test handling empty policies response""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = {"policies": []} + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.marketplace.marketplace_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.marketplace.marketplace_service import ( + Marketplace, + ) + + marketplace = Marketplace(mock_provider) + + assert marketplace.policies_fetched is True + assert marketplace.policies.access_level is None + + def test_fetch_policies_api_error(self): + """Test handling of API errors during policy fetch""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_service.policies().list.side_effect = Exception("API Error") + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.marketplace.marketplace_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.marketplace.marketplace_service import ( + Marketplace, + ) + + marketplace = Marketplace(mock_provider) + + assert marketplace.policies_fetched is False + assert marketplace.policies.access_level is None + + def test_fetch_policies_build_service_returns_none(self): + """Test early return when _build_service fails to construct the client""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.marketplace.marketplace_service.GoogleWorkspaceService._build_service", + return_value=None, + ), + ): + from prowler.providers.googleworkspace.services.marketplace.marketplace_service import ( + Marketplace, + ) + + marketplace = Marketplace(mock_provider) + + assert marketplace.policies_fetched is False + assert marketplace.policies.access_level is None + + def test_fetch_policies_execute_raises(self): + """Test inner except handler when request.execute() raises during pagination""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_request = MagicMock() + mock_request.execute.side_effect = Exception("Execute failed") + mock_service.policies().list.return_value = mock_request + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.marketplace.marketplace_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.marketplace.marketplace_service import ( + Marketplace, + ) + + marketplace = Marketplace(mock_provider) + + assert marketplace.policies_fetched is False + assert marketplace.policies.access_level is None + + def test_marketplace_policies_model(self): + """Test MarketplacePolicies Pydantic model""" + from prowler.providers.googleworkspace.services.marketplace.marketplace_service import ( + MarketplacePolicies, + ) + + policies = MarketplacePolicies(access_level="ALLOW_LISTED_APPS") + + assert policies.access_level == "ALLOW_LISTED_APPS" diff --git a/tests/providers/googleworkspace/services/sites/sites_service_disabled/sites_service_disabled_test.py b/tests/providers/googleworkspace/services/sites/sites_service_disabled/sites_service_disabled_test.py new file mode 100644 index 0000000000..ff668f5cf5 --- /dev/null +++ b/tests/providers/googleworkspace/services/sites/sites_service_disabled/sites_service_disabled_test.py @@ -0,0 +1,124 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.sites.sites_service import ( + SitesPolicies, +) +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + set_mocked_googleworkspace_provider, +) + + +class TestSitesServiceDisabled: + def test_pass_service_disabled(self): + """Test PASS when Sites service is disabled (OFF for everyone)""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.sites.sites_service_disabled.sites_service_disabled.sites_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.sites.sites_service_disabled.sites_service_disabled import ( + sites_service_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SitesPolicies(service_state="DISABLED") + + check = sites_service_disabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "disabled" in findings[0].status_extended + assert findings[0].resource_name == "Sites Policies" + assert findings[0].resource_id == "sitesPolicies" + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_service_enabled(self): + """Test FAIL when Sites service is enabled (ON for everyone)""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.sites.sites_service_disabled.sites_service_disabled.sites_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.sites.sites_service_disabled.sites_service_disabled import ( + sites_service_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SitesPolicies(service_state="ENABLED") + + check = sites_service_disabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "enabled" in findings[0].status_extended + + def test_fail_no_policy_set(self): + """Test FAIL when no explicit policy is set (None) - Google default is ON (insecure)""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.sites.sites_service_disabled.sites_service_disabled.sites_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.sites.sites_service_disabled.sites_service_disabled import ( + sites_service_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = True + mock_client.policies = SitesPolicies(service_state=None) + + check = sites_service_disabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "not explicitly configured" in findings[0].status_extended + + def test_no_findings_when_fetch_failed(self): + """Test no findings returned when the API fetch failed""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.sites.sites_service_disabled.sites_service_disabled.sites_client" + ) as mock_client, + ): + from prowler.providers.googleworkspace.services.sites.sites_service_disabled.sites_service_disabled import ( + sites_service_disabled, + ) + + mock_client.provider = mock_provider + mock_client.policies_fetched = False + mock_client.policies = SitesPolicies() + + check = sites_service_disabled() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/sites/sites_service_test.py b/tests/providers/googleworkspace/services/sites/sites_service_test.py new file mode 100644 index 0000000000..63a473238d --- /dev/null +++ b/tests/providers/googleworkspace/services/sites/sites_service_test.py @@ -0,0 +1,234 @@ +from unittest.mock import MagicMock, patch + +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + set_mocked_googleworkspace_provider, +) + + +class TestSitesService: + def test_fetch_policies_service_status_off(self): + """Test fetching Sites policy with service status OFF""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_credentials = MagicMock() + mock_session = MagicMock() + mock_session.credentials = mock_credentials + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = { + "policies": [ + { + "setting": { + "type": "settings/sites.service_status", + "value": { + "serviceState": "DISABLED", + }, + } + }, + ] + } + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.sites.sites_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.sites.sites_service import ( + Sites, + ) + + sites = Sites(mock_provider) + + assert sites.policies_fetched is True + assert sites.policies.service_state == "DISABLED" + + def test_fetch_policies_service_status_on(self): + """Test fetching Sites policy with service status ON""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = { + "policies": [ + { + "setting": { + "type": "settings/sites.service_status", + "value": { + "serviceState": "ENABLED", + }, + } + }, + ] + } + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.sites.sites_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.sites.sites_service import ( + Sites, + ) + + sites = Sites(mock_provider) + + assert sites.policies_fetched is True + assert sites.policies.service_state == "ENABLED" + + def test_fetch_policies_empty_response(self): + """Test handling empty policies response""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = {"policies": []} + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.sites.sites_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.sites.sites_service import ( + Sites, + ) + + sites = Sites(mock_provider) + + assert sites.policies_fetched is True + assert sites.policies.service_state is None + + def test_fetch_policies_api_error(self): + """Test handling of API errors during policy fetch""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_service.policies().list.side_effect = Exception("API Error") + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.sites.sites_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.sites.sites_service import ( + Sites, + ) + + sites = Sites(mock_provider) + + assert sites.policies_fetched is False + assert sites.policies.service_state is None + + def test_fetch_policies_build_service_returns_none(self): + """Test early return when _build_service fails to construct the client""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.sites.sites_service.GoogleWorkspaceService._build_service", + return_value=None, + ), + ): + from prowler.providers.googleworkspace.services.sites.sites_service import ( + Sites, + ) + + sites = Sites(mock_provider) + + assert sites.policies_fetched is False + assert sites.policies.service_state is None + + def test_fetch_policies_execute_raises(self): + """Test inner except handler when request.execute() raises during pagination""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_request = MagicMock() + mock_request.execute.side_effect = Exception("Execute failed") + mock_service.policies().list.return_value = mock_request + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.sites.sites_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.sites.sites_service import ( + Sites, + ) + + sites = Sites(mock_provider) + + assert sites.policies_fetched is False + assert sites.policies.service_state is None + + def test_sites_policies_model(self): + """Test SitesPolicies Pydantic model""" + from prowler.providers.googleworkspace.services.sites.sites_service import ( + SitesPolicies, + ) + + policies = SitesPolicies(service_state="DISABLED") + + assert policies.service_state == "DISABLED" diff --git a/tests/providers/iac/iac_fixtures.py b/tests/providers/iac/iac_fixtures.py index 2e769a732a..b2d205940e 100644 --- a/tests/providers/iac/iac_fixtures.py +++ b/tests/providers/iac/iac_fixtures.py @@ -259,7 +259,13 @@ SAMPLE_TRIVY_VULNERABILITY_OUTPUT = { "Title": "Example vulnerability", "Description": "This is an example vulnerability", "Severity": "high", - "PrimaryURL": "https://example.com/cve-2023-1234", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2023-1234", + "References": [ + "https://avd.aquasec.com/nvd/cve-2023-1234", + "https://nvd.nist.gov/vuln/detail/CVE-2023-1234", + "https://www.cve.org/CVERecord?id=CVE-2023-1234", + "https://security.example.com/advisories/CVE-2023-1234", + ], } ], "Secrets": [], @@ -268,6 +274,39 @@ SAMPLE_TRIVY_VULNERABILITY_OUTPUT = { ] } +SAMPLE_TRIVY_VULNERABILITY_WITHOUT_CVE_ORG_REFERENCE = { + "VulnerabilityID": "CVE-2023-5678", + "Title": "Vulnerability without cve.org reference", + "Description": "This vulnerability includes references but no cve.org reference", + "Severity": "high", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2023-5678", + "References": [ + "https://avd.aquasec.com/nvd/cve-2023-5678", + "https://nvd.nist.gov/vuln/detail/CVE-2023-5678", + "https://security.example.com/advisories/CVE-2023-5678", + ], +} + +SAMPLE_TRIVY_VULNERABILITY_WITHOUT_REFERENCES = { + "VulnerabilityID": "CVE-2023-9012", + "Title": "Fallback CVE vulnerability", + "Description": "This vulnerability requires building the URL from VulnerabilityID", + "Severity": "medium", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2023-9012", +} + +SAMPLE_TRIVY_NON_CVE_VULNERABILITY = { + "VulnerabilityID": "GHSA-abcd-1234-efgh", + "Title": "Non-CVE vulnerability", + "Description": "This advisory has no CVE identifier", + "Severity": "high", + "PrimaryURL": "https://avd.aquasec.com/nvd/ghsa-abcd-1234-efgh", + "References": [ + "https://avd.aquasec.com/nvd/ghsa-abcd-1234-efgh", + "https://github.com/advisories/GHSA-abcd-1234-efgh", + ], +} + # Sample Trivy output with secrets SAMPLE_TRIVY_SECRET_OUTPUT = { "Results": [ diff --git a/tests/providers/iac/iac_provider_test.py b/tests/providers/iac/iac_provider_test.py index fc98c097d8..8b4b51000a 100644 --- a/tests/providers/iac/iac_provider_test.py +++ b/tests/providers/iac/iac_provider_test.py @@ -20,6 +20,9 @@ from tests.providers.iac.iac_fixtures import ( SAMPLE_KUBERNETES_CHECK, SAMPLE_PASSED_CHECK, SAMPLE_SKIPPED_CHECK, + SAMPLE_TRIVY_NON_CVE_VULNERABILITY, + SAMPLE_TRIVY_VULNERABILITY_WITHOUT_CVE_ORG_REFERENCE, + SAMPLE_TRIVY_VULNERABILITY_WITHOUT_REFERENCES, SAMPLE_YAML_CHECK, get_empty_trivy_output, get_invalid_trivy_output, @@ -57,13 +60,15 @@ class TestIacProvider: assert isinstance(report, CheckReportIAC) assert report.status == "FAIL" + # Trivy emits "AVD-AWS-0001"; Hub indexes it without the AVD- prefix. + expected_url = "https://hub.prowler.com/check/AWS-0001" assert report.check_metadata.Provider == "iac" assert report.check_metadata.CheckID == SAMPLE_FAILED_CHECK["ID"] assert report.check_metadata.CheckTitle == SAMPLE_FAILED_CHECK["Title"] assert report.check_metadata.Severity == "low" - assert report.check_metadata.RelatedUrl == SAMPLE_FAILED_CHECK.get( - "PrimaryURL", "" - ) + assert report.check_metadata.Remediation.Recommendation.Url == expected_url + assert report.check_metadata.RelatedUrl == "" + assert report.check_metadata.AdditionalURLs == [expected_url] def test_iac_provider_process_finding_passed(self): """Test processing a passed finding""" @@ -79,6 +84,101 @@ class TestIacProvider: assert report.check_metadata.CheckTitle == SAMPLE_PASSED_CHECK["Title"] assert report.check_metadata.Severity == "low" + def test_iac_provider_process_vulnerability_prefers_cve_reference_and_filters_aqua( + self, + ): + """Test CVE findings use cve.org and exclude Aqua references.""" + provider = IacProvider() + + report = provider._process_finding( + { + "VulnerabilityID": "CVE-2023-1234", + "Title": "Example vulnerability", + "Description": "This is an example vulnerability", + "Severity": "high", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2023-1234", + "References": [ + "https://avd.aquasec.com/nvd/cve-2023-1234", + "https://nvd.nist.gov/vuln/detail/CVE-2023-1234", + "https://www.cve.org/CVERecord?id=CVE-2023-1234", + "https://security.example.com/advisories/CVE-2023-1234", + ], + }, + "package.json", + "nodejs", + ) + + assert ( + report.check_metadata.Remediation.Recommendation.Url + == "https://www.cve.org/CVERecord?id=CVE-2023-1234" + ) + assert report.check_metadata.RelatedUrl == "" + assert report.check_metadata.AdditionalURLs == [ + "https://www.cve.org/CVERecord?id=CVE-2023-1234" + ] + + def test_iac_provider_process_vulnerability_builds_cve_org_for_nvd_reference( + self, + ): + """Test official CVE URL is built when only NVD is provided.""" + provider = IacProvider() + + report = provider._process_finding( + SAMPLE_TRIVY_VULNERABILITY_WITHOUT_CVE_ORG_REFERENCE, + "package.json", + "nodejs", + ) + + assert ( + report.check_metadata.Remediation.Recommendation.Url + == "https://www.cve.org/CVERecord?id=CVE-2023-5678" + ) + assert report.check_metadata.RelatedUrl == "" + assert report.check_metadata.AdditionalURLs == [ + "https://www.cve.org/CVERecord?id=CVE-2023-5678" + ] + + def test_iac_provider_process_vulnerability_builds_cve_org_when_references_missing( + self, + ): + """Test CVE URL is built from VulnerabilityID when references are absent.""" + provider = IacProvider() + + report = provider._process_finding( + SAMPLE_TRIVY_VULNERABILITY_WITHOUT_REFERENCES, + "package.json", + "nodejs", + ) + + assert ( + report.check_metadata.Remediation.Recommendation.Url + == "https://www.cve.org/CVERecord?id=CVE-2023-9012" + ) + assert report.check_metadata.RelatedUrl == "" + assert report.check_metadata.AdditionalURLs == [ + "https://www.cve.org/CVERecord?id=CVE-2023-9012" + ] + + def test_iac_provider_process_non_cve_vulnerability_falls_back_to_github_advisory( + self, + ): + """Non-CVE vulnerabilities (GHSA-…) point to GitHub Security Advisories.""" + provider = IacProvider() + + report = provider._process_finding( + SAMPLE_TRIVY_NON_CVE_VULNERABILITY, + "package.json", + "nodejs", + ) + + expected_url = ( + "https://github.com/advisories/" + f"{SAMPLE_TRIVY_NON_CVE_VULNERABILITY['VulnerabilityID'].upper()}" + ) + assert report.check_metadata.Remediation.Recommendation.Url == expected_url + assert report.check_metadata.RelatedUrl == "" + assert report.check_metadata.AdditionalURLs == [expected_url] + @patch("subprocess.run") def test_iac_provider_run_scan_success(self, mock_subprocess): """Test successful IAC scan with Trivy""" diff --git a/tests/providers/image/image_fixtures.py b/tests/providers/image/image_fixtures.py index 920a7f225a..bf5e12df32 100644 --- a/tests/providers/image/image_fixtures.py +++ b/tests/providers/image/image_fixtures.py @@ -11,6 +11,12 @@ SAMPLE_VULNERABILITY_FINDING = { "Title": "OpenSSL Buffer Overflow", "Description": "A buffer overflow vulnerability in OpenSSL allows remote attackers to execute arbitrary code.", "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2024-1234", + "References": [ + "https://avd.aquasec.com/nvd/cve-2024-1234", + "https://nvd.nist.gov/vuln/detail/CVE-2024-1234", + "https://www.cve.org/CVERecord?id=CVE-2024-1234", + "https://security.alpinelinux.org/vuln/CVE-2024-1234", + ], } # Sample secret finding from Trivy @@ -45,6 +51,50 @@ SAMPLE_UNKNOWN_SEVERITY_FINDING = { "Description": "An issue with unknown severity.", } +SAMPLE_VULNERABILITY_WITHOUT_CVE_ORG_REFERENCE = { + "VulnerabilityID": "CVE-2024-5678", + "PkgID": "libcrypto3@3.3.2-r0", + "PkgName": "libcrypto3", + "InstalledVersion": "3.3.2-r0", + "FixedVersion": "3.3.2-r1", + "Severity": "HIGH", + "Title": "OpenSSL advisory without cve.org reference", + "Description": "A vulnerability with references but no cve.org reference.", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2024-5678", + "References": [ + "https://avd.aquasec.com/nvd/cve-2024-5678", + "https://nvd.nist.gov/vuln/detail/CVE-2024-5678", + "https://security.alpinelinux.org/vuln/CVE-2024-5678", + ], +} + +SAMPLE_CVE_WITHOUT_REFERENCES_FINDING = { + "VulnerabilityID": "CVE-2024-9012", + "PkgID": "busybox@1.36.1-r8", + "PkgName": "busybox", + "InstalledVersion": "1.36.1-r8", + "FixedVersion": "1.36.1-r9", + "Severity": "MEDIUM", + "Title": "Busybox fallback CVE", + "Description": "A vulnerability without explicit references.", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2024-9012", +} + +SAMPLE_NON_CVE_VULNERABILITY_FINDING = { + "VulnerabilityID": "GHSA-abcd-1234-efgh", + "PkgID": "custompkg@0.0.1", + "PkgName": "custompkg", + "InstalledVersion": "0.0.1", + "Severity": "HIGH", + "Title": "Non-CVE advisory", + "Description": "An advisory without a CVE identifier.", + "PrimaryURL": "https://avd.aquasec.com/nvd/ghsa-abcd-1234-efgh", + "References": [ + "https://avd.aquasec.com/nvd/ghsa-abcd-1234-efgh", + "https://github.com/advisories/GHSA-abcd-1234-efgh", + ], +} + # Sample image SHA for testing (first 12 chars of a sha256 digest) SAMPLE_IMAGE_SHA = "c1aabb73d233" SAMPLE_IMAGE_ID = f"sha256:{SAMPLE_IMAGE_SHA}abcdef1234567890" diff --git a/tests/providers/image/image_provider_test.py b/tests/providers/image/image_provider_test.py index 4462df6beb..92c4236dd8 100644 --- a/tests/providers/image/image_provider_test.py +++ b/tests/providers/image/image_provider_test.py @@ -23,11 +23,14 @@ from prowler.providers.image.exceptions.exceptions import ( ) from prowler.providers.image.image_provider import ImageProvider from tests.providers.image.image_fixtures import ( + SAMPLE_CVE_WITHOUT_REFERENCES_FINDING, SAMPLE_IMAGE_SHA, SAMPLE_MISCONFIGURATION_FINDING, + SAMPLE_NON_CVE_VULNERABILITY_FINDING, SAMPLE_SECRET_FINDING, SAMPLE_UNKNOWN_SEVERITY_FINDING, SAMPLE_VULNERABILITY_FINDING, + SAMPLE_VULNERABILITY_WITHOUT_CVE_ORG_REFERENCE, get_empty_trivy_output, get_invalid_trivy_output, get_multi_type_trivy_output, @@ -148,6 +151,77 @@ class TestImageProvider: assert report.check_metadata.Categories == ["vulnerabilities"] assert report.check_metadata.RelatedUrl == "" + def test_process_finding_vulnerability_prefers_cve_reference_and_filters_aqua(self): + """Test CVE findings use cve.org and exclude Aqua references.""" + provider = _make_provider() + + report = provider._process_finding( + SAMPLE_VULNERABILITY_FINDING, + "alpine:3.18", + "alpine:3.18 (alpine 3.18.0)", + ) + + assert ( + report.check_metadata.Remediation.Recommendation.Url + == "https://www.cve.org/CVERecord?id=CVE-2024-1234" + ) + assert report.check_metadata.AdditionalURLs == [ + "https://www.cve.org/CVERecord?id=CVE-2024-1234" + ] + + def test_process_finding_vulnerability_builds_cve_org_when_only_nvd_reference( + self, + ): + """Test official CVE URL is built when only NVD is provided.""" + provider = _make_provider() + + report = provider._process_finding( + SAMPLE_VULNERABILITY_WITHOUT_CVE_ORG_REFERENCE, + "alpine:3.18", + "alpine:3.18 (alpine 3.18.0)", + ) + + assert ( + report.check_metadata.Remediation.Recommendation.Url + == "https://www.cve.org/CVERecord?id=CVE-2024-5678" + ) + assert report.check_metadata.AdditionalURLs == [ + "https://www.cve.org/CVERecord?id=CVE-2024-5678" + ] + + def test_process_finding_vulnerability_builds_cve_org_when_references_missing(self): + """Test CVE URL is built from VulnerabilityID when references are absent.""" + provider = _make_provider() + + report = provider._process_finding( + SAMPLE_CVE_WITHOUT_REFERENCES_FINDING, + "alpine:3.18", + "alpine:3.18 (alpine 3.18.0)", + ) + + assert ( + report.check_metadata.Remediation.Recommendation.Url + == "https://www.cve.org/CVERecord?id=CVE-2024-9012" + ) + assert report.check_metadata.AdditionalURLs == [ + "https://www.cve.org/CVERecord?id=CVE-2024-9012" + ] + + def test_process_finding_non_cve_vulnerability_does_not_fallback_to_aqua(self): + """Test non-CVE vulnerabilities do not keep Aqua links.""" + provider = _make_provider() + + report = provider._process_finding( + SAMPLE_NON_CVE_VULNERABILITY_FINDING, + "alpine:3.18", + "alpine:3.18 (alpine 3.18.0)", + ) + + assert report.check_metadata.Remediation.Recommendation.Url == "" + assert report.check_metadata.AdditionalURLs == [ + "https://github.com/advisories/GHSA-abcd-1234-efgh" + ] + def test_process_finding_secret(self): """Test processing a secret finding (identified by RuleID).""" provider = _make_provider() diff --git a/tests/providers/m365/lib/powershell/m365_powershell_test.py b/tests/providers/m365/lib/powershell/m365_powershell_test.py index 84ae2ab129..30e2eedcad 100644 --- a/tests/providers/m365/lib/powershell/m365_powershell_test.py +++ b/tests/providers/m365/lib/powershell/m365_powershell_test.py @@ -101,9 +101,9 @@ class Testm365PowerShell: # Call original init_credential to verify application authentication setup M365PowerShell.init_credential(session, credentials) - session.execute.assert_any_call('$clientID = "test_client_id"') - session.execute.assert_any_call('$clientSecret = "test_client_secret"') - session.execute.assert_any_call('$tenantID = "test_tenant_id"') + session.execute.assert_any_call("$clientID = 'test_client_id'") + session.execute.assert_any_call("$clientSecret = 'test_client_secret'") + session.execute.assert_any_call("$tenantID = 'test_tenant_id'") session.execute.assert_any_call( '$graphtokenBody = @{ Grant_Type = "client_credentials"; Scope = "https://graph.microsoft.com/.default"; Client_Id = $clientID; Client_Secret = $clientSecret }' ) @@ -112,6 +112,128 @@ class Testm365PowerShell: ) session.close() + @patch("subprocess.Popen") + def test_init_credential_special_chars_in_secret(self, mock_popen): + """Test that secrets with $, !, #, and ' are preserved via single-quote escaping.""" + mock_process = MagicMock() + mock_popen.return_value = mock_process + credentials = M365Credentials( + client_id="test_client_id", + client_secret="Pa$$w0rd!#'", + tenant_id="test_tenant_id", + ) + identity = M365IdentityInfo( + identity_id="test_id", + identity_type="Service Principal", + tenant_id="test_tenant", + tenant_domain="example.com", + tenant_domains=["example.com"], + location="test_location", + ) + with patch.object(M365PowerShell, "init_credential"): + session = M365PowerShell(credentials, identity) + + session.execute = MagicMock() + M365PowerShell.init_credential(session, credentials) + + # Single quotes prevent PowerShell $$ expansion; + # embedded ' is escaped as '' per PowerShell convention + session.execute.assert_any_call("$clientSecret = 'Pa$$w0rd!#'''") + + @pytest.mark.parametrize( + "secret, expected_command", + [ + # Plain secret: single quotes behave like the old double quotes. + ("simplesecret", "$clientSecret = 'simplesecret'"), + # $ must NOT expand as a PowerShell variable. + ("Pa$$w0rd", "$clientSecret = 'Pa$$w0rd'"), + # $(...) subexpression must stay literal and never execute. + ("a$(whoami)b", "$clientSecret = 'a$(whoami)b'"), + # ${var} expansion must stay literal too. + ("a${env:PATH}b", "$clientSecret = 'a${env:PATH}b'"), + # Backtick is a literal char inside single quotes (not an escape). + ("pass`word", "$clientSecret = 'pass`word'"), + # Double quotes are literal inside single quotes. + ('pa"ss"word', "$clientSecret = 'pa\"ss\"word'"), + # A single quote is doubled per PowerShell escaping rules. + ("O'Brien", "$clientSecret = 'O''Brien'"), + # Consecutive single quotes each get doubled. + ("a''b", "$clientSecret = 'a''''b'"), + # A payload with quotes and metacharacters stays a single literal + # string and cannot break out of the quoting. + ( + "'; Remove-Item -Recurse -Force; '", + "$clientSecret = '''; Remove-Item -Recurse -Force; '''", + ), + # Other shell metacharacters are preserved verbatim (no sanitize()). + ("p@ss!#%&;w0rd", "$clientSecret = 'p@ss!#%&;w0rd'"), + # Newline embedded in the secret is preserved verbatim. + ("line1\nline2", "$clientSecret = 'line1\nline2'"), + # Empty secret renders an empty single-quoted string. + ("", "$clientSecret = ''"), + # None secret is coerced to an empty single-quoted string. + (None, "$clientSecret = ''"), + ], + ) + @patch("subprocess.Popen") + def test_init_credential_secret_escaping_edge_cases( + self, mock_popen, secret, expected_command + ): + """The client_secret is single-quote escaped, preserving every special + character verbatim with no PowerShell expansion or subexpression + evaluation.""" + mock_process = MagicMock() + mock_popen.return_value = mock_process + credentials = M365Credentials( + client_id="test_client_id", + client_secret=secret, + tenant_id="test_tenant_id", + ) + identity = M365IdentityInfo( + identity_id="test_id", + identity_type="Service Principal", + tenant_id="test_tenant", + tenant_domain="example.com", + tenant_domains=["example.com"], + location="test_location", + ) + with patch.object(M365PowerShell, "init_credential"): + session = M365PowerShell(credentials, identity) + + session.execute = MagicMock() + M365PowerShell.init_credential(session, credentials) + + session.execute.assert_any_call(expected_command) + + @patch("subprocess.Popen") + def test_init_credential_sanitizes_client_and_tenant_id(self, mock_popen): + """client_id and tenant_id are sanitized in the Application Auth path, + stripping shell metacharacters while keeping UUID-safe characters.""" + mock_process = MagicMock() + mock_popen.return_value = mock_process + credentials = M365Credentials( + client_id="abc-123; Remove-Item", + client_secret="secret", + tenant_id="def-456 && whoami", + ) + identity = M365IdentityInfo( + identity_id="test_id", + identity_type="Service Principal", + tenant_id="test_tenant", + tenant_domain="example.com", + tenant_domains=["example.com"], + location="test_location", + ) + with patch.object(M365PowerShell, "init_credential"): + session = M365PowerShell(credentials, identity) + + session.execute = MagicMock() + M365PowerShell.init_credential(session, credentials) + + # sanitize() removes ';', spaces and '&' but keeps letters, digits and '-'. + session.execute.assert_any_call("$clientID = 'abc-123Remove-Item'") + session.execute.assert_any_call("$tenantID = 'def-456whoami'") + @patch("subprocess.Popen") def test_remove_ansi(self, mock_popen): credentials = M365Credentials( @@ -562,6 +684,12 @@ class Testm365PowerShell: assert result is True assert session.execute.call_count == 3 + # The secret must be referenced as a bare PowerShell variable. Wrapping it + # in double quotes ("$clientSecret") would re-expand special chars that were + # correctly escaped during assignment, so assert the exact command. + session.execute.assert_any_call( + "$SecureSecret = ConvertTo-SecureString $clientSecret -AsPlainText -Force" + ) session.execute_connect.assert_called_once_with( 'Connect-ExchangeOnline -AccessToken $exchangeToken.AccessToken -Organization "$tenantID"' ) diff --git a/tests/providers/m365/m365_provider_test.py b/tests/providers/m365/m365_provider_test.py index f2354ea7e7..9f4a151249 100644 --- a/tests/providers/m365/m365_provider_test.py +++ b/tests/providers/m365/m365_provider_test.py @@ -1,6 +1,7 @@ +import asyncio import base64 import os -from unittest.mock import MagicMock, mock_open, patch +from unittest.mock import AsyncMock, MagicMock, mock_open, patch from uuid import uuid4 import pytest @@ -1535,19 +1536,17 @@ class TestM365Provider: TENANT_ID, CLIENT_ID, None, b"fake_certificate_data", certificate_path ) - @patch("prowler.providers.m365.m365_provider.asyncio.get_event_loop") + @patch("prowler.providers.m365.m365_provider.asyncio.run") @patch("prowler.providers.m365.m365_provider.GraphServiceClient") @patch("prowler.providers.m365.m365_provider.CertificateCredential") def test_verify_client_certificate_content_success( - self, mock_cert_cred, mock_graph, mock_loop + self, mock_cert_cred, mock_graph, mock_asyncio_run ): """Test verify_client method with valid certificate content""" certificate_content = base64.b64encode(b"fake_certificate").decode("utf-8") - # Mock the async call - mock_loop_instance = MagicMock() - mock_loop.return_value = mock_loop_instance - mock_loop_instance.run_until_complete.return_value = [{"id": "domain.com"}] + # Mock the async call result + mock_asyncio_run.return_value = [{"id": "domain.com"}] # Mock credential and graph client mock_credential = MagicMock() @@ -1563,19 +1562,17 @@ class TestM365Provider: mock_cert_cred.assert_called_once() mock_graph.assert_called_once_with(credentials=mock_credential) - @patch("prowler.providers.m365.m365_provider.asyncio.get_event_loop") + @patch("prowler.providers.m365.m365_provider.asyncio.run") @patch("prowler.providers.m365.m365_provider.GraphServiceClient") @patch("prowler.providers.m365.m365_provider.CertificateCredential") def test_verify_client_certificate_content_failure( - self, mock_cert_cred, mock_graph, mock_loop + self, mock_cert_cred, mock_graph, mock_asyncio_run ): """Test verify_client method with certificate content that fails validation""" certificate_content = base64.b64encode(b"fake_certificate").decode("utf-8") # Mock the async call to return empty result (invalid certificate) - mock_loop_instance = MagicMock() - mock_loop.return_value = mock_loop_instance - mock_loop_instance.run_until_complete.return_value = None + mock_asyncio_run.return_value = None # Mock credential and graph client mock_credential = MagicMock() @@ -1591,19 +1588,17 @@ class TestM365Provider: assert "certificate content is not valid" in str(exception.value) @patch("builtins.open", mock_open(read_data=b"fake_certificate_data")) - @patch("prowler.providers.m365.m365_provider.asyncio.get_event_loop") + @patch("prowler.providers.m365.m365_provider.asyncio.run") @patch("prowler.providers.m365.m365_provider.GraphServiceClient") @patch("prowler.providers.m365.m365_provider.CertificateCredential") def test_verify_client_certificate_path_success( - self, mock_cert_cred, mock_graph, mock_loop + self, mock_cert_cred, mock_graph, mock_asyncio_run ): """Test verify_client method with valid certificate path""" certificate_path = "/path/to/cert.pem" - # Mock the async call - mock_loop_instance = MagicMock() - mock_loop.return_value = mock_loop_instance - mock_loop_instance.run_until_complete.return_value = [{"id": "domain.com"}] + # Mock the async call result + mock_asyncio_run.return_value = [{"id": "domain.com"}] # Mock credential and graph client mock_credential = MagicMock() @@ -1618,19 +1613,17 @@ class TestM365Provider: mock_graph.assert_called_once_with(credentials=mock_credential) @patch("builtins.open", mock_open(read_data=b"fake_certificate_data")) - @patch("prowler.providers.m365.m365_provider.asyncio.get_event_loop") + @patch("prowler.providers.m365.m365_provider.asyncio.run") @patch("prowler.providers.m365.m365_provider.GraphServiceClient") @patch("prowler.providers.m365.m365_provider.CertificateCredential") def test_verify_client_certificate_path_failure( - self, mock_cert_cred, mock_graph, mock_loop + self, mock_cert_cred, mock_graph, mock_asyncio_run ): """Test verify_client method with certificate path that fails validation""" certificate_path = "/path/to/cert.pem" # Mock the async call to return empty result (invalid certificate) - mock_loop_instance = MagicMock() - mock_loop.return_value = mock_loop_instance - mock_loop_instance.run_until_complete.return_value = None + mock_asyncio_run.return_value = None # Mock credential and graph client mock_credential = MagicMock() @@ -1804,3 +1797,94 @@ class TestM365Provider: assert "Missing environment variable M365_CERTIFICATE_CONTENT" in str( exception.value ) + + +class TestM365ProviderEventLoop: + """Regression for Celery workers on Python 3.12 where + asyncio.get_event_loop() raised + `RuntimeError: There is no current event loop in thread 'MainThread'.` + M365Provider.setup_identity and M365Provider.validate_static_credentials + must work without a pre-existing loop in the current thread.""" + + def _without_event_loop(self, callable_): + # Simulate the Celery worker state: no event loop registered for the + # current thread. + asyncio.set_event_loop(None) + try: + return callable_() + finally: + # Re-arm a loop so sibling tests that rely on the default don't + # bleed into each other. + asyncio.set_event_loop(asyncio.new_event_loop()) + + def test_setup_identity_succeeds_without_active_event_loop(self): + domain = MagicMock() + domain.id = "tenant.onmicrosoft.com" + domain.is_default = True + + org = MagicMock() + org.id = TENANT_ID + + graph_client = MagicMock() + graph_client.domains.get = AsyncMock(return_value=MagicMock(value=[domain])) + graph_client.organization.get = AsyncMock(return_value=MagicMock(value=[org])) + + session = MagicMock() + # `setup_identity` reads `session.credentials[0]._credential.client_id` + # when sp_env_auth is True to populate identity.identity_id. + session.credentials = [MagicMock()] + session.credentials[0]._credential.client_id = CLIENT_ID + + def call(): + with patch( + "prowler.providers.m365.m365_provider.GraphServiceClient", + return_value=graph_client, + ): + return M365Provider.setup_identity( + sp_env_auth=True, + browser_auth=False, + az_cli_auth=False, + certificate_auth=False, + session=session, + ) + + identity = self._without_event_loop(call) + + assert isinstance(identity, M365IdentityInfo) + assert identity.tenant_id == TENANT_ID + graph_client.domains.get.assert_awaited_once() + graph_client.organization.get.assert_awaited_once() + + def test_verify_client_certificate_content_without_active_event_loop(self): + # `verify_client` is the function the Sentry trace exercises through + # certificate-based credential validation; it must run an asyncio + # coroutine to call `client.domains.get()` and previously relied on + # `asyncio.get_event_loop()`. + graph_client = MagicMock() + graph_client.domains.get = AsyncMock( + return_value=MagicMock(value=[MagicMock()]) + ) + + def call(): + with ( + patch("prowler.providers.m365.m365_provider.CertificateCredential"), + patch( + "prowler.providers.m365.m365_provider.GraphServiceClient", + return_value=graph_client, + ), + patch( + "prowler.providers.m365.m365_provider.base64.b64decode", + return_value=b"cert-bytes", + ), + ): + M365Provider.verify_client( + tenant_id=TENANT_ID, + client_id=CLIENT_ID, + client_secret=None, + certificate_content="dGVzdA==", + certificate_path=None, + ) + + # Must not raise "There is no current event loop in thread 'MainThread'.". + self._without_event_loop(call) + graph_client.domains.get.assert_awaited_once() diff --git a/tests/providers/m365/services/entra/entra_app_registration_client_secret_unused/entra_app_registration_client_secret_unused_test.py b/tests/providers/m365/services/entra/entra_app_registration_client_secret_unused/entra_app_registration_client_secret_unused_test.py new file mode 100644 index 0000000000..437ac76277 --- /dev/null +++ b/tests/providers/m365/services/entra/entra_app_registration_client_secret_unused/entra_app_registration_client_secret_unused_test.py @@ -0,0 +1,282 @@ +from datetime import datetime, timedelta, timezone +from unittest import mock +from uuid import uuid4 + +from prowler.providers.m365.services.entra.entra_service import ( + AppRegistration, + PasswordCredential, +) +from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider + + +class Test_entra_app_registration_client_secret_unused: + def test_no_app_registrations(self): + """No app registrations in tenant: no findings.""" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_app_registration_client_secret_unused.entra_app_registration_client_secret_unused.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_app_registration_client_secret_unused.entra_app_registration_client_secret_unused import ( + entra_app_registration_client_secret_unused, + ) + + entra_client.app_registrations = {} + + check = entra_app_registration_client_secret_unused() + result = check.execute() + + assert len(result) == 0 + + def test_app_no_password_credentials(self): + """App with no password credentials: expected PASS.""" + app_id = str(uuid4()) + app_name = "Test App Clean" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_app_registration_client_secret_unused.entra_app_registration_client_secret_unused.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_app_registration_client_secret_unused.entra_app_registration_client_secret_unused import ( + entra_app_registration_client_secret_unused, + ) + + entra_client.app_registrations = { + app_id: AppRegistration( + id=app_id, + app_id=str(uuid4()), + name=app_name, + password_credentials=[], + ) + } + + check = entra_app_registration_client_secret_unused() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"App registration {app_name} does not use password credentials." + ) + assert result[0].resource_name == app_name + assert result[0].resource_id == app_id + + def test_app_with_one_password_credential(self): + """App with one password credential: expected FAIL.""" + app_id = str(uuid4()) + app_name = "Test App With Secret" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_app_registration_client_secret_unused.entra_app_registration_client_secret_unused.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_app_registration_client_secret_unused.entra_app_registration_client_secret_unused import ( + entra_app_registration_client_secret_unused, + ) + + entra_client.app_registrations = { + app_id: AppRegistration( + id=app_id, + app_id=str(uuid4()), + name=app_name, + password_credentials=[ + PasswordCredential( + key_id=str(uuid4()), + display_name="My Secret", + start_date_time="2024-01-01T00:00:00Z", + end_date_time="2025-01-01T00:00:00Z", + ), + ], + ) + } + + check = entra_app_registration_client_secret_unused() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "1 password credential(s)" in result[0].status_extended + assert "My Secret" in result[0].status_extended + assert result[0].resource_name == app_name + assert result[0].resource_id == app_id + + def test_app_with_expired_password_credential_still_fails(self): + """App with an expired password credential: still expected FAIL.""" + app_id = str(uuid4()) + app_name = "Legacy App" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + expired = datetime.now(timezone.utc) - timedelta(days=30) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_app_registration_client_secret_unused.entra_app_registration_client_secret_unused.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_app_registration_client_secret_unused.entra_app_registration_client_secret_unused import ( + entra_app_registration_client_secret_unused, + ) + + entra_client.app_registrations = { + app_id: AppRegistration( + id=app_id, + app_id=str(uuid4()), + name=app_name, + password_credentials=[ + PasswordCredential( + key_id=str(uuid4()), + display_name="old-secret", + end_date_time=expired, + ), + ], + ) + } + + check = entra_app_registration_client_secret_unused() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "1 password credential(s)" in result[0].status_extended + assert result[0].resource_name == app_name + + def test_app_with_multiple_password_credentials(self): + """App with multiple password credentials: expected FAIL.""" + app_id = str(uuid4()) + app_name = "Test App Multiple Secrets" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_app_registration_client_secret_unused.entra_app_registration_client_secret_unused.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_app_registration_client_secret_unused.entra_app_registration_client_secret_unused import ( + entra_app_registration_client_secret_unused, + ) + + entra_client.app_registrations = { + app_id: AppRegistration( + id=app_id, + app_id=str(uuid4()), + name=app_name, + password_credentials=[ + PasswordCredential( + key_id=str(uuid4()), + display_name="Secret 1", + end_date_time="2025-06-01T00:00:00Z", + ), + PasswordCredential( + key_id=str(uuid4()), + display_name="Secret 2", + end_date_time="2025-12-01T00:00:00Z", + ), + ], + ) + } + + check = entra_app_registration_client_secret_unused() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "2 password credential(s)" in result[0].status_extended + assert result[0].resource_name == app_name + + def test_multiple_apps_mixed(self): + """Multiple apps: one clean, one with secrets.""" + app_id_pass = str(uuid4()) + app_name_pass = "Clean App" + app_id_fail = str(uuid4()) + app_name_fail = "App With Secret" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_app_registration_client_secret_unused.entra_app_registration_client_secret_unused.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_app_registration_client_secret_unused.entra_app_registration_client_secret_unused import ( + entra_app_registration_client_secret_unused, + ) + + entra_client.app_registrations = { + app_id_pass: AppRegistration( + id=app_id_pass, + app_id=str(uuid4()), + name=app_name_pass, + password_credentials=[], + ), + app_id_fail: AppRegistration( + id=app_id_fail, + app_id=str(uuid4()), + name=app_name_fail, + password_credentials=[ + PasswordCredential( + key_id=str(uuid4()), + display_name="Legacy Secret", + ), + ], + ), + } + + check = entra_app_registration_client_secret_unused() + result = check.execute() + + assert len(result) == 2 + + result_pass = next(r for r in result if r.resource_id == app_id_pass) + result_fail = next(r for r in result if r.resource_id == app_id_fail) + + assert result_pass.status == "PASS" + assert result_fail.status == "FAIL" + assert "Legacy Secret" in result_fail.status_extended diff --git a/tests/providers/m365/services/entra/entra_break_glass_account_fido2_security_key_registered/entra_break_glass_account_fido2_security_key_registered_test.py b/tests/providers/m365/services/entra/entra_break_glass_account_fido2_security_key_registered/entra_break_glass_account_fido2_security_key_registered_test.py index 31e7c06a1d..1147b64e8e 100644 --- a/tests/providers/m365/services/entra/entra_break_glass_account_fido2_security_key_registered/entra_break_glass_account_fido2_security_key_registered_test.py +++ b/tests/providers/m365/services/entra/entra_break_glass_account_fido2_security_key_registered/entra_break_glass_account_fido2_security_key_registered_test.py @@ -67,6 +67,7 @@ class Test_entra_break_glass_account_fido2_security_key_registered: entra_client = mock.MagicMock entra_client.audited_tenant = "audited_tenant" entra_client.audited_domain = DOMAIN + entra_client.user_registration_details_error = None with ( mock.patch( @@ -104,6 +105,7 @@ class Test_entra_break_glass_account_fido2_security_key_registered: entra_client = mock.MagicMock entra_client.audited_tenant = "audited_tenant" entra_client.audited_domain = DOMAIN + entra_client.user_registration_details_error = None with ( mock.patch( @@ -142,6 +144,7 @@ class Test_entra_break_glass_account_fido2_security_key_registered: entra_client = mock.MagicMock entra_client.audited_tenant = "audited_tenant" entra_client.audited_domain = DOMAIN + entra_client.user_registration_details_error = None with ( mock.patch( @@ -178,6 +181,7 @@ class Test_entra_break_glass_account_fido2_security_key_registered: entra_client = mock.MagicMock entra_client.audited_tenant = "audited_tenant" entra_client.audited_domain = DOMAIN + entra_client.user_registration_details_error = None with ( mock.patch( @@ -228,6 +232,7 @@ class Test_entra_break_glass_account_fido2_security_key_registered: entra_client = mock.MagicMock entra_client.audited_tenant = "audited_tenant" entra_client.audited_domain = DOMAIN + entra_client.user_registration_details_error = None with ( mock.patch( @@ -275,6 +280,7 @@ class Test_entra_break_glass_account_fido2_security_key_registered: entra_client = mock.MagicMock entra_client.audited_tenant = "audited_tenant" entra_client.audited_domain = DOMAIN + entra_client.user_registration_details_error = None with ( mock.patch( @@ -321,6 +327,7 @@ class Test_entra_break_glass_account_fido2_security_key_registered: entra_client = mock.MagicMock entra_client.audited_tenant = "audited_tenant" entra_client.audited_domain = DOMAIN + entra_client.user_registration_details_error = None with ( mock.patch( @@ -368,6 +375,7 @@ class Test_entra_break_glass_account_fido2_security_key_registered: entra_client = mock.MagicMock entra_client.audited_tenant = "audited_tenant" entra_client.audited_domain = DOMAIN + entra_client.user_registration_details_error = None with ( mock.patch( @@ -422,6 +430,7 @@ class Test_entra_break_glass_account_fido2_security_key_registered: entra_client = mock.MagicMock entra_client.audited_tenant = "audited_tenant" entra_client.audited_domain = DOMAIN + entra_client.user_registration_details_error = None with ( mock.patch( @@ -457,6 +466,7 @@ class Test_entra_break_glass_account_fido2_security_key_registered: entra_client = mock.MagicMock entra_client.audited_tenant = "audited_tenant" entra_client.audited_domain = DOMAIN + entra_client.user_registration_details_error = None with ( mock.patch( @@ -500,3 +510,117 @@ class Test_entra_break_glass_account_fido2_security_key_registered: assert len(result) == 1 assert result[0].status == "PASS" assert result[0].resource_name == "BreakGlass1" + + def test_user_registration_details_permission_error(self): + """Test FAIL when there's a permission error reading user registration details.""" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + entra_client.user_registration_details_error = "Insufficient privileges to read user registration details. Required permission: AuditLog.Read.All" + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_break_glass_account_fido2_security_key_registered.entra_break_glass_account_fido2_security_key_registered import ( + entra_break_glass_account_fido2_security_key_registered, + ) + + policy_id = str(uuid4()) + bg_user_id = str(uuid4()) + + entra_client.conditional_access_policies = { + policy_id: _make_policy(policy_id, excluded_users=[bg_user_id]), + } + entra_client.users = { + bg_user_id: User( + id=bg_user_id, + name="BreakGlass1", + on_premises_sync_enabled=False, + authentication_methods=[], + ), + } + + check = entra_break_glass_account_fido2_security_key_registered() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + "Cannot verify FIDO2 security key registration for break glass account BreakGlass1" + in result[0].status_extended + ) + assert "AuditLog.Read.All" in result[0].status_extended + assert result[0].resource_name == "BreakGlass1" + assert result[0].resource_id == bg_user_id + + def test_user_registration_details_permission_error_with_missing_user(self): + """Per-user emission and missing-user short-circuit on the error path. + + Two break-glass user IDs are excluded from all CAPs, but only one is + present in ``entra_client.users``. With ``user_registration_details_error`` + set, the present user must produce one preventive FAIL anchored to the + real user; the missing user must be skipped by the existing + ``if not user: continue`` guard rather than crash or yield a synthetic + finding. + """ + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + entra_client.user_registration_details_error = "Insufficient privileges to read user registration details. Required permission: AuditLog.Read.All" + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_break_glass_account_fido2_security_key_registered.entra_break_glass_account_fido2_security_key_registered import ( + entra_break_glass_account_fido2_security_key_registered, + ) + + policy_id = str(uuid4()) + present_user_id = str(uuid4()) + missing_user_id = str(uuid4()) + + entra_client.conditional_access_policies = { + policy_id: _make_policy( + policy_id, + excluded_users=[present_user_id, missing_user_id], + ), + } + entra_client.users = { + present_user_id: User( + id=present_user_id, + name="BreakGlass1", + on_premises_sync_enabled=False, + authentication_methods=[], + ), + # missing_user_id intentionally absent — exercises the + # `if not user: continue` short-circuit inside the loop. + } + + check = entra_break_glass_account_fido2_security_key_registered() + result = check.execute() + + # One finding for the present user; the missing one is skipped. + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + "Cannot verify FIDO2 security key registration for break glass account BreakGlass1" + in result[0].status_extended + ) + assert "AuditLog.Read.All" in result[0].status_extended + assert result[0].resource == entra_client.users[present_user_id] + assert result[0].resource_name == "BreakGlass1" + assert result[0].resource_id == present_user_id diff --git a/tests/providers/m365/services/entra/entra_emergency_access_exclusion/entra_emergency_access_exclusion_test.py b/tests/providers/m365/services/entra/entra_emergency_access_exclusion/entra_emergency_access_exclusion_test.py index db2fd1d193..cbbd9c6886 100644 --- a/tests/providers/m365/services/entra/entra_emergency_access_exclusion/entra_emergency_access_exclusion_test.py +++ b/tests/providers/m365/services/entra/entra_emergency_access_exclusion/entra_emergency_access_exclusion_test.py @@ -47,7 +47,7 @@ class Test_entra_emergency_access_exclusion: assert result[0].status == "PASS" assert ( result[0].status_extended - == "No enabled Conditional Access policies found. Emergency access exclusions are not required." + == "No enabled Conditional Access policies with a Block grant control found. Emergency access exclusions are not required." ) assert result[0].resource == {} assert result[0].resource_name == "Conditional Access Policies" @@ -98,7 +98,7 @@ class Test_entra_emergency_access_exclusion: ), ), grant_controls=GrantControls( - built_in_controls=[ConditionalAccessGrantControl.MFA], + built_in_controls=[ConditionalAccessGrantControl.BLOCK], operator=GrantControlOperator.AND, ), session_controls=SessionControls( @@ -122,7 +122,7 @@ class Test_entra_emergency_access_exclusion: assert result[0].status == "PASS" assert ( result[0].status_extended - == "No enabled Conditional Access policies found. Emergency access exclusions are not required." + == "No enabled Conditional Access policies with a Block grant control found. Emergency access exclusions are not required." ) def test_entra_no_emergency_access_exclusion(self): @@ -172,7 +172,7 @@ class Test_entra_emergency_access_exclusion: ), ), grant_controls=GrantControls( - built_in_controls=[ConditionalAccessGrantControl.MFA], + built_in_controls=[ConditionalAccessGrantControl.BLOCK], operator=GrantControlOperator.AND, ), session_controls=SessionControls( @@ -207,7 +207,7 @@ class Test_entra_emergency_access_exclusion: ), ), grant_controls=GrantControls( - built_in_controls=[ConditionalAccessGrantControl.MFA], + built_in_controls=[ConditionalAccessGrantControl.BLOCK], operator=GrantControlOperator.AND, ), session_controls=SessionControls( @@ -230,7 +230,7 @@ class Test_entra_emergency_access_exclusion: assert len(result) == 1 assert result[0].status == "FAIL" assert ( - "No user or group is excluded as emergency access from all 2 enabled Conditional Access policies" + "No user or group is excluded as emergency access from all 2 enabled Conditional Access policies with a Block grant control" in result[0].status_extended ) assert result[0].resource_name == "Conditional Access Policies" @@ -283,7 +283,7 @@ class Test_entra_emergency_access_exclusion: ), ), grant_controls=GrantControls( - built_in_controls=[ConditionalAccessGrantControl.MFA], + built_in_controls=[ConditionalAccessGrantControl.BLOCK], operator=GrantControlOperator.AND, ), session_controls=SessionControls( @@ -318,7 +318,7 @@ class Test_entra_emergency_access_exclusion: ), ), grant_controls=GrantControls( - built_in_controls=[ConditionalAccessGrantControl.MFA], + built_in_controls=[ConditionalAccessGrantControl.BLOCK], operator=GrantControlOperator.AND, ), session_controls=SessionControls( @@ -352,7 +352,7 @@ class Test_entra_emergency_access_exclusion: assert result[0].status == "PASS" assert "BreakGlass1" in result[0].status_extended assert ( - "excluded from all 2 enabled Conditional Access policies" + "excluded from all 2 enabled Conditional Access policies with a Block grant control" in result[0].status_extended ) assert result[0].resource_name == "Conditional Access Policies" @@ -405,7 +405,7 @@ class Test_entra_emergency_access_exclusion: ), ), grant_controls=GrantControls( - built_in_controls=[ConditionalAccessGrantControl.MFA], + built_in_controls=[ConditionalAccessGrantControl.BLOCK], operator=GrantControlOperator.AND, ), session_controls=SessionControls( @@ -440,7 +440,7 @@ class Test_entra_emergency_access_exclusion: ), ), grant_controls=GrantControls( - built_in_controls=[ConditionalAccessGrantControl.MFA], + built_in_controls=[ConditionalAccessGrantControl.BLOCK], operator=GrantControlOperator.AND, ), session_controls=SessionControls( @@ -474,7 +474,7 @@ class Test_entra_emergency_access_exclusion: assert result[0].status == "PASS" assert "BreakGlassGroup" in result[0].status_extended assert ( - "excluded from all 2 enabled Conditional Access policies" + "excluded from all 2 enabled Conditional Access policies with a Block grant control" in result[0].status_extended ) assert result[0].resource_name == "Conditional Access Policies" @@ -528,7 +528,7 @@ class Test_entra_emergency_access_exclusion: ), ), grant_controls=GrantControls( - built_in_controls=[ConditionalAccessGrantControl.MFA], + built_in_controls=[ConditionalAccessGrantControl.BLOCK], operator=GrantControlOperator.AND, ), session_controls=SessionControls( @@ -563,7 +563,7 @@ class Test_entra_emergency_access_exclusion: ), ), grant_controls=GrantControls( - built_in_controls=[ConditionalAccessGrantControl.MFA], + built_in_controls=[ConditionalAccessGrantControl.BLOCK], operator=GrantControlOperator.AND, ), session_controls=SessionControls( @@ -605,7 +605,7 @@ class Test_entra_emergency_access_exclusion: assert "BreakGlass1" in result[0].status_extended assert "BreakGlassGroup" in result[0].status_extended assert ( - "excluded from all 2 enabled Conditional Access policies" + "excluded from all 2 enabled Conditional Access policies with a Block grant control" in result[0].status_extended ) assert result[0].resource_name == "Conditional Access Policies" @@ -658,7 +658,7 @@ class Test_entra_emergency_access_exclusion: ), ), grant_controls=GrantControls( - built_in_controls=[ConditionalAccessGrantControl.MFA], + built_in_controls=[ConditionalAccessGrantControl.BLOCK], operator=GrantControlOperator.AND, ), session_controls=SessionControls( @@ -693,7 +693,7 @@ class Test_entra_emergency_access_exclusion: ), ), grant_controls=GrantControls( - built_in_controls=[ConditionalAccessGrantControl.MFA], + built_in_controls=[ConditionalAccessGrantControl.BLOCK], operator=GrantControlOperator.AND, ), session_controls=SessionControls( @@ -729,7 +729,7 @@ class Test_entra_emergency_access_exclusion: assert result[0].resource_id == "conditionalAccessPolicies" assert "BreakGlass1" in result[0].status_extended assert ( - "excluded from all 1 enabled Conditional Access policies" + "excluded from all 1 enabled Conditional Access policies with a Block grant control" in result[0].status_extended ) @@ -781,7 +781,7 @@ class Test_entra_emergency_access_exclusion: ), ), grant_controls=GrantControls( - built_in_controls=[ConditionalAccessGrantControl.MFA], + built_in_controls=[ConditionalAccessGrantControl.BLOCK], operator=GrantControlOperator.AND, ), session_controls=SessionControls( @@ -816,7 +816,7 @@ class Test_entra_emergency_access_exclusion: ), ), grant_controls=GrantControls( - built_in_controls=[ConditionalAccessGrantControl.MFA], + built_in_controls=[ConditionalAccessGrantControl.BLOCK], operator=GrantControlOperator.AND, ), session_controls=SessionControls( @@ -850,8 +850,316 @@ class Test_entra_emergency_access_exclusion: assert result[0].status == "PASS" assert "BreakGlass1" in result[0].status_extended assert ( - "excluded from all 2 enabled Conditional Access policies" + "excluded from all 2 enabled Conditional Access policies with a Block grant control" in result[0].status_extended ) assert result[0].resource_name == "Conditional Access Policies" assert result[0].resource_id == "conditionalAccessPolicies" + + def test_entra_only_non_blocking_policies(self): + """PASS when the tenant has only non-blocking policies (no Block grant).""" + policy_id = str(uuid4()) + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_emergency_access_exclusion.entra_emergency_access_exclusion.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_emergency_access_exclusion.entra_emergency_access_exclusion import ( + entra_emergency_access_exclusion, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name="MFA for all", + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + ), + grant_controls=GrantControls( + built_in_controls=[ConditionalAccessGrantControl.MFA], + operator=GrantControlOperator.AND, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=False, + frequency=None, + type=None, + interval=SignInFrequencyInterval.EVERY_TIME, + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ), + } + + check = entra_emergency_access_exclusion() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "No enabled Conditional Access policies with a Block grant control found. Emergency access exclusions are not required." + ) + + def test_entra_user_excluded_from_blocking_but_included_in_non_blocking(self): + """PASS when only Block-grant exclusions are required (non-blocking inclusion is ignored).""" + block_policy_id = str(uuid4()) + mfa_policy_id = str(uuid4()) + emergency_user_id = "emergency-access-user" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_emergency_access_exclusion.entra_emergency_access_exclusion.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_emergency_access_exclusion.entra_emergency_access_exclusion import ( + entra_emergency_access_exclusion, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + block_policy_id: ConditionalAccessPolicy( + id=block_policy_id, + display_name="Block legacy auth", + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[emergency_user_id], + included_roles=[], + excluded_roles=[], + ), + ), + grant_controls=GrantControls( + built_in_controls=[ConditionalAccessGrantControl.BLOCK], + operator=GrantControlOperator.AND, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=False, + frequency=None, + type=None, + interval=SignInFrequencyInterval.EVERY_TIME, + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ), + mfa_policy_id: ConditionalAccessPolicy( + id=mfa_policy_id, + display_name="MFA for all", + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + ), + grant_controls=GrantControls( + built_in_controls=[ConditionalAccessGrantControl.MFA], + operator=GrantControlOperator.AND, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=False, + frequency=None, + type=None, + interval=SignInFrequencyInterval.EVERY_TIME, + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ), + } + + entra_client.users = { + emergency_user_id: User( + id=emergency_user_id, + name="BreakGlass1", + on_premises_sync_enabled=False, + authentication_methods=[], + ), + } + entra_client.groups = [] + + check = entra_emergency_access_exclusion() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert "BreakGlass1" in result[0].status_extended + assert ( + "excluded from all 1 enabled Conditional Access policies with a Block grant control" + in result[0].status_extended + ) + + def test_entra_user_excluded_only_from_subset_of_blocking_policies(self): + """FAIL when the user is excluded from one Block policy but not the other.""" + block_policy_id_1 = str(uuid4()) + block_policy_id_2 = str(uuid4()) + emergency_user_id = "emergency-access-user" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_emergency_access_exclusion.entra_emergency_access_exclusion.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_emergency_access_exclusion.entra_emergency_access_exclusion import ( + entra_emergency_access_exclusion, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + block_policy_id_1: ConditionalAccessPolicy( + id=block_policy_id_1, + display_name="Block 1", + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[emergency_user_id], + included_roles=[], + excluded_roles=[], + ), + ), + grant_controls=GrantControls( + built_in_controls=[ConditionalAccessGrantControl.BLOCK], + operator=GrantControlOperator.AND, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=False, + frequency=None, + type=None, + interval=SignInFrequencyInterval.EVERY_TIME, + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ), + block_policy_id_2: ConditionalAccessPolicy( + id=block_policy_id_2, + display_name="Block 2", + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + ), + grant_controls=GrantControls( + built_in_controls=[ConditionalAccessGrantControl.BLOCK], + operator=GrantControlOperator.AND, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=False, + frequency=None, + type=None, + interval=SignInFrequencyInterval.EVERY_TIME, + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ), + } + + entra_client.users = { + emergency_user_id: User( + id=emergency_user_id, + name="BreakGlass1", + on_premises_sync_enabled=False, + authentication_methods=[], + ), + } + entra_client.groups = [] + + check = entra_emergency_access_exclusion() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No user or group is excluded as emergency access from all 2 enabled Conditional Access policies with a Block grant control." + ) diff --git a/tests/providers/m365/services/entra/entra_service_principal_no_secrets_for_permanent_tier0_roles/entra_service_principal_no_secrets_for_permanent_tier0_roles_test.py b/tests/providers/m365/services/entra/entra_service_principal_no_secrets_for_permanent_tier0_roles/entra_service_principal_no_secrets_for_permanent_tier0_roles_test.py new file mode 100644 index 0000000000..5fc9dc2dfb --- /dev/null +++ b/tests/providers/m365/services/entra/entra_service_principal_no_secrets_for_permanent_tier0_roles/entra_service_principal_no_secrets_for_permanent_tier0_roles_test.py @@ -0,0 +1,424 @@ +from datetime import datetime, timedelta, timezone +from unittest import mock +from uuid import uuid4 + +from prowler.providers.m365.services.entra.entra_service import ( + KeyCredential, + PasswordCredential, + ServicePrincipal, +) +from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider + + +class Test_entra_service_principal_no_secrets_for_permanent_tier0_roles: + """Tests for the entra_service_principal_no_secrets_for_permanent_tier0_roles check.""" + + def test_no_service_principals(self): + """No service principals configured: expected no findings.""" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_service_principal_no_secrets_for_permanent_tier0_roles.entra_service_principal_no_secrets_for_permanent_tier0_roles.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_service_principal_no_secrets_for_permanent_tier0_roles.entra_service_principal_no_secrets_for_permanent_tier0_roles import ( + entra_service_principal_no_secrets_for_permanent_tier0_roles, + ) + + entra_client.service_principals = {} + + check = entra_service_principal_no_secrets_for_permanent_tier0_roles() + result = check.execute() + + assert len(result) == 0 + + def test_service_principal_no_secrets_no_roles(self): + """Service principal without secrets and no Tier 0 roles: expected PASS.""" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + sp_id = str(uuid4()) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_service_principal_no_secrets_for_permanent_tier0_roles.entra_service_principal_no_secrets_for_permanent_tier0_roles.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_service_principal_no_secrets_for_permanent_tier0_roles.entra_service_principal_no_secrets_for_permanent_tier0_roles import ( + entra_service_principal_no_secrets_for_permanent_tier0_roles, + ) + + entra_client.service_principals = { + sp_id: ServicePrincipal( + id=sp_id, + name="TestApp", + app_id=str(uuid4()), + password_credentials=[], + key_credentials=[ + KeyCredential(key_id=str(uuid4()), display_name="cert1") + ], + directory_role_template_ids=[], + ) + } + + check = entra_service_principal_no_secrets_for_permanent_tier0_roles() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert "does not use client secrets" in result[0].status_extended + assert result[0].resource_id == sp_id + assert result[0].resource_name == "TestApp" + + def test_service_principal_with_secrets_no_tier0_roles(self): + """Service principal with secrets but no Tier 0 roles: expected PASS.""" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + sp_id = str(uuid4()) + non_tier0_role = "4a5d8f65-41da-4de4-8968-e035b65339cf" # Reports Reader + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_service_principal_no_secrets_for_permanent_tier0_roles.entra_service_principal_no_secrets_for_permanent_tier0_roles.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_service_principal_no_secrets_for_permanent_tier0_roles.entra_service_principal_no_secrets_for_permanent_tier0_roles import ( + entra_service_principal_no_secrets_for_permanent_tier0_roles, + ) + + entra_client.service_principals = { + sp_id: ServicePrincipal( + id=sp_id, + name="AppWithSecrets", + app_id=str(uuid4()), + password_credentials=[ + PasswordCredential(key_id=str(uuid4()), display_name="secret1") + ], + key_credentials=[], + directory_role_template_ids=[non_tier0_role], + ) + } + + check = entra_service_principal_no_secrets_for_permanent_tier0_roles() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert "no permanent Tier 0" in result[0].status_extended + assert result[0].resource_id == sp_id + + def test_service_principal_no_secrets_with_tier0_roles(self): + """Service principal without secrets but with Tier 0 roles: expected PASS.""" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + sp_id = str(uuid4()) + global_admin_role = "62e90394-69f5-4237-9190-012177145e10" + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_service_principal_no_secrets_for_permanent_tier0_roles.entra_service_principal_no_secrets_for_permanent_tier0_roles.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_service_principal_no_secrets_for_permanent_tier0_roles.entra_service_principal_no_secrets_for_permanent_tier0_roles import ( + entra_service_principal_no_secrets_for_permanent_tier0_roles, + ) + + entra_client.service_principals = { + sp_id: ServicePrincipal( + id=sp_id, + name="CertBasedApp", + app_id=str(uuid4()), + password_credentials=[], + key_credentials=[ + KeyCredential(key_id=str(uuid4()), display_name="cert1") + ], + directory_role_template_ids=[global_admin_role], + ) + } + + check = entra_service_principal_no_secrets_for_permanent_tier0_roles() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert "does not use client secrets" in result[0].status_extended + assert result[0].resource_id == sp_id + + def test_service_principal_with_secrets_and_tier0_role(self): + """Service principal with secrets and Tier 0 role: expected FAIL.""" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + sp_id = str(uuid4()) + global_admin_role = "62e90394-69f5-4237-9190-012177145e10" + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_service_principal_no_secrets_for_permanent_tier0_roles.entra_service_principal_no_secrets_for_permanent_tier0_roles.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_service_principal_no_secrets_for_permanent_tier0_roles.entra_service_principal_no_secrets_for_permanent_tier0_roles import ( + entra_service_principal_no_secrets_for_permanent_tier0_roles, + ) + + entra_client.service_principals = { + sp_id: ServicePrincipal( + id=sp_id, + name="VulnerableApp", + app_id=str(uuid4()), + password_credentials=[ + PasswordCredential(key_id=str(uuid4()), display_name="secret1") + ], + key_credentials=[], + directory_role_template_ids=[global_admin_role], + ) + } + + check = entra_service_principal_no_secrets_for_permanent_tier0_roles() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "uses client secrets" in result[0].status_extended + assert "Control Plane" in result[0].status_extended + assert result[0].resource_id == sp_id + assert result[0].resource_name == "VulnerableApp" + + def test_service_principal_with_secrets_and_multiple_tier0_roles(self): + """Service principal with secrets and multiple Tier 0 roles: expected FAIL.""" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + sp_id = str(uuid4()) + global_admin_role = "62e90394-69f5-4237-9190-012177145e10" + priv_role_admin = "e8611ab8-c189-46e8-94e1-60213ab1f814" + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_service_principal_no_secrets_for_permanent_tier0_roles.entra_service_principal_no_secrets_for_permanent_tier0_roles.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_service_principal_no_secrets_for_permanent_tier0_roles.entra_service_principal_no_secrets_for_permanent_tier0_roles import ( + entra_service_principal_no_secrets_for_permanent_tier0_roles, + ) + + entra_client.service_principals = { + sp_id: ServicePrincipal( + id=sp_id, + name="HighRiskApp", + app_id=str(uuid4()), + password_credentials=[ + PasswordCredential(key_id=str(uuid4()), display_name="secret1"), + PasswordCredential(key_id=str(uuid4()), display_name="secret2"), + ], + key_credentials=[], + directory_role_template_ids=[ + global_admin_role, + priv_role_admin, + ], + ) + } + + check = entra_service_principal_no_secrets_for_permanent_tier0_roles() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "2 Control Plane" in result[0].status_extended + + def test_multiple_service_principals_mixed(self): + """Multiple service principals with mixed states: mixed results.""" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + sp_id_pass = str(uuid4()) + sp_id_fail = str(uuid4()) + global_admin_role = "62e90394-69f5-4237-9190-012177145e10" + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_service_principal_no_secrets_for_permanent_tier0_roles.entra_service_principal_no_secrets_for_permanent_tier0_roles.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_service_principal_no_secrets_for_permanent_tier0_roles.entra_service_principal_no_secrets_for_permanent_tier0_roles import ( + entra_service_principal_no_secrets_for_permanent_tier0_roles, + ) + + entra_client.service_principals = { + sp_id_pass: ServicePrincipal( + id=sp_id_pass, + name="SafeApp", + app_id=str(uuid4()), + password_credentials=[], + key_credentials=[ + KeyCredential(key_id=str(uuid4()), display_name="cert1") + ], + directory_role_template_ids=[global_admin_role], + ), + sp_id_fail: ServicePrincipal( + id=sp_id_fail, + name="UnsafeApp", + app_id=str(uuid4()), + password_credentials=[ + PasswordCredential(key_id=str(uuid4()), display_name="secret1") + ], + key_credentials=[], + directory_role_template_ids=[global_admin_role], + ), + } + + check = entra_service_principal_no_secrets_for_permanent_tier0_roles() + result = check.execute() + + assert len(result) == 2 + statuses = {r.resource_id: r.status for r in result} + assert statuses[sp_id_pass] == "PASS" + assert statuses[sp_id_fail] == "FAIL" + + def test_service_principal_only_expired_secrets_with_tier0_role(self): + """Service principal whose only client secrets are expired: expected PASS.""" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + sp_id = str(uuid4()) + global_admin_role = "62e90394-69f5-4237-9190-012177145e10" + expired = datetime.now(timezone.utc) - timedelta(days=1) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_service_principal_no_secrets_for_permanent_tier0_roles.entra_service_principal_no_secrets_for_permanent_tier0_roles.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_service_principal_no_secrets_for_permanent_tier0_roles.entra_service_principal_no_secrets_for_permanent_tier0_roles import ( + entra_service_principal_no_secrets_for_permanent_tier0_roles, + ) + + entra_client.service_principals = { + sp_id: ServicePrincipal( + id=sp_id, + name="LegacyApp", + app_id=str(uuid4()), + password_credentials=[ + PasswordCredential( + key_id=str(uuid4()), + display_name="expired-secret", + end_date_time=expired, + ) + ], + key_credentials=[], + directory_role_template_ids=[global_admin_role], + ) + } + + check = entra_service_principal_no_secrets_for_permanent_tier0_roles() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert "does not use client secrets" in result[0].status_extended + + def test_service_principal_active_and_expired_secrets_with_tier0_role(self): + """Service principal with at least one active client secret: expected FAIL.""" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + sp_id = str(uuid4()) + global_admin_role = "62e90394-69f5-4237-9190-012177145e10" + expired = datetime.now(timezone.utc) - timedelta(days=1) + future = datetime.now(timezone.utc) + timedelta(days=180) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_service_principal_no_secrets_for_permanent_tier0_roles.entra_service_principal_no_secrets_for_permanent_tier0_roles.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_service_principal_no_secrets_for_permanent_tier0_roles.entra_service_principal_no_secrets_for_permanent_tier0_roles import ( + entra_service_principal_no_secrets_for_permanent_tier0_roles, + ) + + entra_client.service_principals = { + sp_id: ServicePrincipal( + id=sp_id, + name="MixedSecretsApp", + app_id=str(uuid4()), + password_credentials=[ + PasswordCredential( + key_id=str(uuid4()), + display_name="old", + end_date_time=expired, + ), + PasswordCredential( + key_id=str(uuid4()), + display_name="current", + end_date_time=future, + ), + ], + key_credentials=[], + directory_role_template_ids=[global_admin_role], + ) + } + + check = entra_service_principal_no_secrets_for_permanent_tier0_roles() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "uses client secrets" in result[0].status_extended diff --git a/tests/providers/m365/services/entra/entra_users_mfa_capable/entra_users_mfa_capable_test.py b/tests/providers/m365/services/entra/entra_users_mfa_capable/entra_users_mfa_capable_test.py index 28cc266347..8bf0c88f77 100644 --- a/tests/providers/m365/services/entra/entra_users_mfa_capable/entra_users_mfa_capable_test.py +++ b/tests/providers/m365/services/entra/entra_users_mfa_capable/entra_users_mfa_capable_test.py @@ -11,6 +11,7 @@ class Test_entra_users_mfa_capable: entra_client = mock.MagicMock entra_client.audited_tenant = "audited_tenant" entra_client.audited_domain = DOMAIN + entra_client.user_registration_details_error = None with ( mock.patch( @@ -53,6 +54,7 @@ class Test_entra_users_mfa_capable: entra_client = mock.MagicMock entra_client.audited_tenant = "audited_tenant" entra_client.audited_domain = DOMAIN + entra_client.user_registration_details_error = None with ( mock.patch( @@ -95,6 +97,7 @@ class Test_entra_users_mfa_capable: entra_client = mock.MagicMock entra_client.audited_tenant = "audited_tenant" entra_client.audited_domain = DOMAIN + entra_client.user_registration_details_error = None with ( mock.patch( @@ -153,6 +156,7 @@ class Test_entra_users_mfa_capable: entra_client = mock.MagicMock entra_client.audited_tenant = "audited_tenant" entra_client.audited_domain = DOMAIN + entra_client.user_registration_details_error = None with ( mock.patch( @@ -191,6 +195,7 @@ class Test_entra_users_mfa_capable: entra_client = mock.MagicMock entra_client.audited_tenant = "audited_tenant" entra_client.audited_domain = DOMAIN + entra_client.user_registration_details_error = None with ( mock.patch( @@ -248,6 +253,7 @@ class Test_entra_users_mfa_capable: entra_client = mock.MagicMock entra_client.audited_tenant = "audited_tenant" entra_client.audited_domain = DOMAIN + entra_client.user_registration_details_error = None with ( mock.patch( @@ -286,6 +292,7 @@ class Test_entra_users_mfa_capable: entra_client = mock.MagicMock entra_client.audited_tenant = "audited_tenant" entra_client.audited_domain = DOMAIN + entra_client.user_registration_details_error = None with ( mock.patch( @@ -324,6 +331,7 @@ class Test_entra_users_mfa_capable: entra_client = mock.MagicMock entra_client.audited_tenant = "audited_tenant" entra_client.audited_domain = DOMAIN + entra_client.user_registration_details_error = None with ( mock.patch( @@ -383,6 +391,7 @@ class Test_entra_users_mfa_capable: entra_client = mock.MagicMock entra_client.audited_tenant = "audited_tenant" entra_client.audited_domain = DOMAIN + entra_client.user_registration_details_error = None with ( mock.patch( @@ -420,3 +429,125 @@ class Test_entra_users_mfa_capable: assert result[0].resource == entra_client.users[user_id] assert result[0].resource_name == "Test User" assert result[0].resource_id == user_id + + def test_user_registration_details_permission_error(self): + """Test FAIL when there's a permission error reading user registration details.""" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + entra_client.user_registration_details_error = "Insufficient privileges to read user registration details. Required permission: AuditLog.Read.All" + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_users_mfa_capable.entra_users_mfa_capable.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_users_mfa_capable.entra_users_mfa_capable import ( + entra_users_mfa_capable, + ) + + user_id = str(uuid4()) + entra_client.users = { + user_id: User( + id=user_id, + name="Test User", + on_premises_sync_enabled=False, + directory_roles_ids=[], + is_mfa_capable=False, + account_enabled=True, + ) + } + + check = entra_users_mfa_capable() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + "Cannot verify MFA capability for user Test User" + in result[0].status_extended + ) + assert "AuditLog.Read.All" in result[0].status_extended + assert result[0].resource == entra_client.users[user_id] + assert result[0].resource_name == "Test User" + assert result[0].resource_id == user_id + + def test_user_registration_details_permission_error_skips_guest_and_disabled(self): + """CIS-scope skip (Guest, disabled) still applies on the permission-error path. + + With ``user_registration_details_error`` set, only enabled member users + should receive a per-user "Cannot verify MFA capability" FAIL — guests + and disabled members are filtered out before the error branch runs. + """ + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + entra_client.user_registration_details_error = "Insufficient privileges to read user registration details. Required permission: AuditLog.Read.All" + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_users_mfa_capable.entra_users_mfa_capable.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_users_mfa_capable.entra_users_mfa_capable import ( + entra_users_mfa_capable, + ) + + member_id = str(uuid4()) + guest_id = str(uuid4()) + disabled_member_id = str(uuid4()) + entra_client.users = { + member_id: User( + id=member_id, + name="Enabled Member", + on_premises_sync_enabled=False, + directory_roles_ids=[], + is_mfa_capable=False, + account_enabled=True, + user_type="Member", + ), + guest_id: User( + id=guest_id, + name="Guest User", + on_premises_sync_enabled=False, + directory_roles_ids=[], + is_mfa_capable=False, + account_enabled=True, + user_type="Guest", + ), + disabled_member_id: User( + id=disabled_member_id, + name="Disabled Member", + on_premises_sync_enabled=False, + directory_roles_ids=[], + is_mfa_capable=False, + account_enabled=False, + user_type="Member", + ), + } + + check = entra_users_mfa_capable() + result = check.execute() + + # Only the enabled member should be reported — Guest and + # disabled member are skipped before the error branch. + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + "Cannot verify MFA capability for user Enabled Member" + in result[0].status_extended + ) + assert "AuditLog.Read.All" in result[0].status_extended + assert result[0].resource == entra_client.users[member_id] + assert result[0].resource_name == "Enabled Member" + assert result[0].resource_id == member_id diff --git a/tests/providers/m365/services/entra/microsoft365_entra_service_test.py b/tests/providers/m365/services/entra/microsoft365_entra_service_test.py index d83219793e..f2ee0b2c84 100644 --- a/tests/providers/m365/services/entra/microsoft365_entra_service_test.py +++ b/tests/providers/m365/services/entra/microsoft365_entra_service_test.py @@ -1,4 +1,5 @@ import asyncio +from datetime import datetime, timezone from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock, patch @@ -521,10 +522,26 @@ class Test_Entra_Service: assert len(users) == 6 assert users_builder.get.await_count == 1 - assert users_builder.get.await_args.kwargs == {} + # The Graph users.get() call must request accountEnabled, userType and + # onPremisesSyncEnabled via $select. They are not part of the default + # property set, and omitting them causes disabled guest users to leak + # into checks like entra_users_mfa_capable (issue #10921). + request_configuration = users_builder.get.await_args.kwargs[ + "request_configuration" + ] + assert set(request_configuration.query_parameters.select) == { + "id", + "displayName", + "userType", + "accountEnabled", + "onPremisesSyncEnabled", + } with_url_mock.assert_called_once_with("next-link") assert users["user-1"].directory_roles_ids == ["role-template-1"] assert users["user-6"].directory_roles_ids == ["role-template-1"] + # When Graph does not return accountEnabled (legacy SimpleNamespace + # fixtures) we still honour the EXO PowerShell fallback for backwards + # compatibility. assert users["user-6"].account_enabled is False assert users["user-1"].is_mfa_capable is True assert users["user-2"].is_mfa_capable is False @@ -532,6 +549,81 @@ class Test_Entra_Service: assert users["user-6"].authentication_methods == ["mobilePhone"] assert users["user-2"].authentication_methods == [] + def test__get_users_uses_graph_account_enabled_for_disabled_guests(self): + """Regression test for https://github.com/prowler-cloud/prowler/issues/10921. + + Disabled guest users do not appear in EXO's ``Get-User`` output, so the + previous code resolved their ``account_enabled`` from the EXO map, + defaulted it to ``True`` and surfaced them as failing findings in + ``entra_users_mfa_capable``. The Graph ``accountEnabled`` value must be + used as the source of truth so disabled guests are excluded. + """ + entra_service = Entra.__new__(Entra) + # Empty EXO map mirrors the production scenario where the disabled guest + # is absent from Get-User results. + entra_service.user_accounts_status = {} + + graph_users = [ + SimpleNamespace( + id="member-1", + display_name="Member User", + on_premises_sync_enabled=False, + account_enabled=True, + user_type="Member", + ), + SimpleNamespace( + id="guest-1", + display_name="Disabled Guest", + on_premises_sync_enabled=False, + account_enabled=False, + user_type="Guest", + ), + SimpleNamespace( + id="guest-2", + display_name="Enabled Guest", + on_premises_sync_enabled=False, + account_enabled=True, + user_type="Guest", + ), + ] + users_response = SimpleNamespace( + value=graph_users, + odata_next_link=None, + ) + users_builder = SimpleNamespace( + get=AsyncMock(return_value=users_response), + with_url=MagicMock(), + ) + directory_roles_builder = SimpleNamespace( + get=AsyncMock(return_value=SimpleNamespace(value=[])), + by_directory_role_id=MagicMock(), + ) + registration_details_builder = SimpleNamespace( + get=AsyncMock(return_value=SimpleNamespace(value=[], odata_next_link=None)), + with_url=MagicMock(), + ) + reports_builder = SimpleNamespace( + authentication_methods=SimpleNamespace( + user_registration_details=registration_details_builder + ) + ) + + entra_service.client = SimpleNamespace( + users=users_builder, + directory_roles=directory_roles_builder, + reports=reports_builder, + ) + + users = asyncio.run(entra_service._get_users()) + + assert len(users) == 3 + assert users["member-1"].account_enabled is True + assert users["member-1"].user_type == "Member" + assert users["guest-1"].account_enabled is False + assert users["guest-1"].user_type == "Guest" + assert users["guest-2"].account_enabled is True + assert users["guest-2"].user_type == "Guest" + def test__get_user_registration_details_handles_pagination(self): entra_service = Entra.__new__(Entra) @@ -573,10 +665,11 @@ class Test_Entra_Service: ) ) - registration_details = asyncio.run( + registration_details, error_message = asyncio.run( entra_service._get_user_registration_details() ) + assert error_message is None assert registration_details == { "user-1": { "is_mfa_capable": True, @@ -593,3 +686,190 @@ class Test_Entra_Service: registration_builder.get.assert_awaited() registration_builder.with_url.assert_called_once_with("next-link") registration_builder_next.get.assert_awaited() + + def test__get_user_registration_details_returns_error_on_permission_denied(self): + """Test that 403 Authorization_RequestDenied returns an empty dict and + a descriptive error message naming the missing AuditLog.Read.All permission. + """ + from msgraph.generated.models.o_data_errors.main_error import MainError + from msgraph.generated.models.o_data_errors.o_data_error import ODataError + + odata_error = ODataError() + odata_error.error = MainError() + odata_error.error.code = "Authorization_RequestDenied" + + registration_builder = SimpleNamespace(get=AsyncMock(side_effect=odata_error)) + + entra_service = Entra.__new__(Entra) + entra_service.client = SimpleNamespace( + reports=SimpleNamespace( + authentication_methods=SimpleNamespace( + user_registration_details=registration_builder + ) + ) + ) + + registration_details, error_message = asyncio.run( + entra_service._get_user_registration_details() + ) + + assert registration_details == {} + assert error_message is not None + assert "AuditLog.Read.All" in error_message + assert "user registration details" in error_message + + def test__get_service_principals_filters_third_party_owners(self): + """Service principals owned by another tenant must not be returned.""" + # Mixed-case input to verify the service normalizes both sides before + # comparison, so a Graph response that returns the owner id in upper + # case still matches the lower-case identity in the provider. + tenant_id_in = "AAAAAAAA-1111-1111-1111-111111111111" + tenant_id_lower = tenant_id_in.lower() + microsoft_tenant_id = "f8cdef31-a31e-4b4a-93e4-5f571e91255a" + + owned_sp = SimpleNamespace( + id="sp-owned", + display_name="Customer App", + app_id="app-owned", + app_owner_organization_id=tenant_id_in, + password_credentials=[ + SimpleNamespace( + key_id="cred-1", + display_name="secret", + end_date_time=None, + ) + ], + key_credentials=[], + ) + first_party_sp = SimpleNamespace( + id="sp-first-party", + display_name="Microsoft Graph", + app_id="app-graph", + app_owner_organization_id=microsoft_tenant_id, + password_credentials=[ + SimpleNamespace( + key_id="cred-2", + display_name="secret", + end_date_time=None, + ) + ], + key_credentials=[], + ) + third_party_sp = SimpleNamespace( + id="sp-third-party", + display_name="Some Vendor App", + app_id="app-vendor", + app_owner_organization_id="22222222-2222-2222-2222-222222222222", + password_credentials=[], + key_credentials=[], + ) + + sp_response = SimpleNamespace( + value=[owned_sp, first_party_sp, third_party_sp], + odata_next_link=None, + ) + + empty_assignments_response = SimpleNamespace(value=[], odata_next_link=None) + + role_assignments_builder = SimpleNamespace( + get=AsyncMock(return_value=empty_assignments_response) + ) + role_management_builder = SimpleNamespace( + directory=SimpleNamespace( + role_assignments=role_assignments_builder, + ) + ) + + service_principals_builder = SimpleNamespace( + get=AsyncMock(return_value=sp_response), + with_url=MagicMock(), + ) + + # The /applications endpoint returns no entries for this case, so the + # service-level test just exercises the customer-owned filter, not the + # secret join. + applications_response = SimpleNamespace(value=[], odata_next_link=None) + applications_builder = SimpleNamespace( + get=AsyncMock(return_value=applications_response), + with_url=MagicMock(), + ) + + entra_service = Entra.__new__(Entra) + entra_service.tenant_id = tenant_id_lower + entra_service.client = SimpleNamespace( + service_principals=service_principals_builder, + role_management=role_management_builder, + applications=applications_builder, + ) + + result = asyncio.run(entra_service._get_service_principals()) + + assert set(result.keys()) == {"sp-owned"} + assert result["sp-owned"].app_owner_organization_id == tenant_id_lower + + def test__get_service_principals_merges_application_credentials(self): + """Secrets registered on the parent Application must be attributed to the SP.""" + tenant_id = "11111111-1111-1111-1111-111111111111" + + # SP returned by Graph with NO password_credentials of its own (the + # common case in production when the secret was added through "App + # registrations > Certificates & secrets"). + sp_without_sp_level_secret = SimpleNamespace( + id="sp-owned", + display_name="m365-dev", + app_id="app-owned", + app_owner_organization_id=tenant_id, + password_credentials=[], + key_credentials=[], + ) + sp_response = SimpleNamespace( + value=[sp_without_sp_level_secret], odata_next_link=None + ) + + # The corresponding Application carries the actual secret. + future = datetime(2099, 1, 1, tzinfo=timezone.utc) + application = SimpleNamespace( + id="app-object", + app_id="app-owned", + password_credentials=[ + SimpleNamespace( + key_id="cred-app", + display_name="app-level-secret", + end_date_time=future, + ), + ], + key_credentials=[], + ) + applications_response = SimpleNamespace( + value=[application], odata_next_link=None + ) + + empty_assignments_response = SimpleNamespace(value=[], odata_next_link=None) + + entra_service = Entra.__new__(Entra) + entra_service.tenant_id = tenant_id + entra_service.client = SimpleNamespace( + service_principals=SimpleNamespace( + get=AsyncMock(return_value=sp_response), + with_url=MagicMock(), + ), + applications=SimpleNamespace( + get=AsyncMock(return_value=applications_response), + with_url=MagicMock(), + ), + role_management=SimpleNamespace( + directory=SimpleNamespace( + role_assignments=SimpleNamespace( + get=AsyncMock(return_value=empty_assignments_response), + ), + ) + ), + ) + + result = asyncio.run(entra_service._get_service_principals()) + + merged = result["sp-owned"] + assert len(merged.password_credentials) == 1 + assert merged.password_credentials[0].key_id == "cred-app" + assert merged.password_credentials[0].display_name == "app-level-secret" + assert merged.password_credentials[0].is_active() diff --git a/tests/providers/okta/exceptions/okta_exceptions_test.py b/tests/providers/okta/exceptions/okta_exceptions_test.py new file mode 100644 index 0000000000..28ea120d4e --- /dev/null +++ b/tests/providers/okta/exceptions/okta_exceptions_test.py @@ -0,0 +1,78 @@ +import pytest + +from prowler.providers.okta.exceptions.exceptions import ( + OktaBaseException, + OktaCredentialsError, + OktaEnvironmentVariableError, + OktaInsufficientPermissionsError, + OktaInvalidCredentialsError, + OktaInvalidOrgDomainError, + OktaInvalidProviderIdError, + OktaPrivateKeyFileError, + OktaSetUpIdentityError, + OktaSetUpSessionError, +) + +EXPECTED_CODES = { + OktaEnvironmentVariableError: 14000, + OktaSetUpSessionError: 14001, + OktaSetUpIdentityError: 14002, + OktaInvalidCredentialsError: 14003, + OktaInvalidOrgDomainError: 14004, + OktaPrivateKeyFileError: 14005, + OktaInsufficientPermissionsError: 14006, + OktaInvalidProviderIdError: 14007, +} + + +class Test_OktaExceptions: + def test_all_codes_in_reserved_range(self): + codes = [c for c, _ in OktaBaseException.OKTA_ERROR_CODES.keys()] + assert all(14000 <= c <= 14999 for c in codes) + assert len(codes) == len(set(codes)) # unique + + def test_all_subclasses_inherit_from_credentials_error(self): + for exc_cls in EXPECTED_CODES: + assert issubclass(exc_cls, OktaCredentialsError) + assert issubclass(exc_cls, OktaBaseException) + + @pytest.mark.parametrize("exc_cls,code", list(EXPECTED_CODES.items())) + def test_each_exception_carries_its_code(self, exc_cls, code): + exc = exc_cls() + assert exc.code == code + assert exc.source == "Okta" + assert exc.message # populated from OKTA_ERROR_CODES + assert exc.remediation # populated from OKTA_ERROR_CODES + + @pytest.mark.parametrize("exc_cls", list(EXPECTED_CODES.keys())) + def test_custom_message_overrides_default(self, exc_cls): + custom = "specific error context" + exc = exc_cls(message=custom) + assert exc.message == custom + + def test_str_format_includes_class_code_and_message(self): + exc = OktaInvalidOrgDomainError(message="bad url") + rendered = str(exc) + assert "OktaInvalidOrgDomainError" in rendered + assert "[14004]" in rendered + assert "bad url" in rendered + + def test_original_exception_appended_to_str(self): + original = ValueError("network down") + exc = OktaSetUpIdentityError(original_exception=original) + rendered = str(exc) + assert "network down" in rendered + + def test_can_be_raised_and_caught(self): + with pytest.raises(OktaInvalidCredentialsError) as info: + raise OktaInvalidCredentialsError(message="bad token") + assert info.value.code == 14003 + assert "bad token" in str(info.value) + + def test_caught_as_credentials_error_base(self): + with pytest.raises(OktaCredentialsError): + raise OktaPrivateKeyFileError(message="empty") + + def test_caught_as_okta_base_exception(self): + with pytest.raises(OktaBaseException): + raise OktaEnvironmentVariableError(message="missing org url") diff --git a/tests/providers/okta/lib/arguments/okta_arguments_test.py b/tests/providers/okta/lib/arguments/okta_arguments_test.py new file mode 100644 index 0000000000..0e3fb9c1a1 --- /dev/null +++ b/tests/providers/okta/lib/arguments/okta_arguments_test.py @@ -0,0 +1,62 @@ +from unittest.mock import MagicMock + +from prowler.providers.okta.lib.arguments import arguments + + +class TestOktaArguments: + def setup_method(self): + self.mock_parser = MagicMock() + self.mock_subparsers = MagicMock() + self.mock_okta_parser = MagicMock() + + self.mock_parser.add_subparsers.return_value = self.mock_subparsers + self.mock_subparsers.add_parser.return_value = self.mock_okta_parser + + def test_init_parser_creates_subparser(self): + mock_args = MagicMock() + mock_args.subparsers = self.mock_subparsers + mock_args.common_providers_parser = MagicMock() + + arguments.init_parser(mock_args) + + self.mock_subparsers.add_parser.assert_called_once_with( + "okta", + parents=[mock_args.common_providers_parser], + help="Okta Provider", + ) + + def test_init_parser_registers_non_secret_flags(self): + mock_args = MagicMock() + mock_args.subparsers = self.mock_subparsers + mock_args.common_providers_parser = MagicMock() + + auth_group = MagicMock() + self.mock_okta_parser.add_argument_group.return_value = auth_group + + arguments.init_parser(mock_args) + + registered = {call.args[0] for call in auth_group.add_argument.call_args_list} + assert registered == { + "--okta-org-domain", + "--okta-client-id", + "--okta-scopes", + } + + def test_secret_flags_not_registered(self): + """Private key material must never be a CLI flag — env-only.""" + mock_args = MagicMock() + mock_args.subparsers = self.mock_subparsers + mock_args.common_providers_parser = MagicMock() + + auth_group = MagicMock() + self.mock_okta_parser.add_argument_group.return_value = auth_group + + arguments.init_parser(mock_args) + + registered = {call.args[0] for call in auth_group.add_argument.call_args_list} + assert "--okta-private-key" not in registered + assert "--okta-private-key-file" not in registered + + def test_no_sensitive_arguments_constant(self): + """No SENSITIVE_ARGUMENTS frozenset needed — no secret flags exist.""" + assert not hasattr(arguments, "SENSITIVE_ARGUMENTS") diff --git a/tests/providers/okta/lib/mutelist/fixtures/okta_mutelist.yaml b/tests/providers/okta/lib/mutelist/fixtures/okta_mutelist.yaml new file mode 100644 index 0000000000..f443b6182b --- /dev/null +++ b/tests/providers/okta/lib/mutelist/fixtures/okta_mutelist.yaml @@ -0,0 +1,9 @@ +Mutelist: + Accounts: + "acme.okta.com": + Checks: + "signon_global_session_idle_timeout_15min": + Regions: + - "*" + Resources: + - "pol-default" diff --git a/tests/providers/okta/lib/mutelist/okta_mutelist_test.py b/tests/providers/okta/lib/mutelist/okta_mutelist_test.py new file mode 100644 index 0000000000..d41bfbcc03 --- /dev/null +++ b/tests/providers/okta/lib/mutelist/okta_mutelist_test.py @@ -0,0 +1,104 @@ +from unittest.mock import MagicMock + +import yaml + +from prowler.providers.okta.lib.mutelist.mutelist import OktaMutelist + +MUTELIST_FIXTURE_PATH = "tests/providers/okta/lib/mutelist/fixtures/okta_mutelist.yaml" + + +class TestOktaMutelist: + def test_get_mutelist_file_from_local_file(self): + mutelist = OktaMutelist(mutelist_path=MUTELIST_FIXTURE_PATH) + + with open(MUTELIST_FIXTURE_PATH) as f: + mutelist_fixture = yaml.safe_load(f)["Mutelist"] + + assert mutelist.mutelist == mutelist_fixture + assert mutelist.mutelist_file_path == MUTELIST_FIXTURE_PATH + + def test_get_mutelist_file_from_local_file_non_existent(self): + mutelist_path = "tests/providers/okta/lib/mutelist/fixtures/not_present" + mutelist = OktaMutelist(mutelist_path=mutelist_path) + + assert mutelist.mutelist == {} + assert mutelist.mutelist_file_path == mutelist_path + + def test_validate_mutelist_not_valid_key(self): + with open(MUTELIST_FIXTURE_PATH) as f: + mutelist_fixture = yaml.safe_load(f)["Mutelist"] + + mutelist_fixture["Accounts1"] = mutelist_fixture["Accounts"] + del mutelist_fixture["Accounts"] + + mutelist = OktaMutelist(mutelist_content=mutelist_fixture) + + assert len(mutelist.validate_mutelist(mutelist_fixture)) == 0 + assert mutelist.mutelist == {} + assert mutelist.mutelist_file_path is None + + def test_is_finding_muted_match(self): + mutelist_content = { + "Accounts": { + "acme.okta.com": { + "Checks": { + "signon_global_session_idle_timeout_15min": { + "Regions": ["*"], + "Resources": ["Default Policy"], + } + } + } + } + } + mutelist = OktaMutelist(mutelist_content=mutelist_content) + + finding = MagicMock() + finding.check_metadata.CheckID = "signon_global_session_idle_timeout_15min" + finding.resource_name = "Default Policy" + finding.resource_tags = [] + + assert mutelist.is_finding_muted(finding, org_domain="acme.okta.com") is True + + def test_is_finding_muted_no_match(self): + mutelist_content = { + "Accounts": { + "acme.okta.com": { + "Checks": { + "signon_global_session_idle_timeout_15min": { + "Regions": ["*"], + "Resources": ["Default Policy"], + } + } + } + } + } + mutelist = OktaMutelist(mutelist_content=mutelist_content) + + finding = MagicMock() + finding.check_metadata.CheckID = "signon_global_session_idle_timeout_15min" + finding.resource_name = "Some Other Policy" + finding.resource_tags = [] + + assert mutelist.is_finding_muted(finding, org_domain="acme.okta.com") is False + + def test_is_finding_muted_no_match_on_different_org(self): + mutelist_content = { + "Accounts": { + "acme.okta.com": { + "Checks": { + "signon_global_session_idle_timeout_15min": { + "Regions": ["*"], + "Resources": ["*"], + } + } + } + } + } + mutelist = OktaMutelist(mutelist_content=mutelist_content) + + finding = MagicMock() + finding.check_metadata.CheckID = "signon_global_session_idle_timeout_15min" + finding.resource_name = "Default Policy" + finding.resource_tags = [] + + assert mutelist.is_finding_muted(finding, org_domain="other.okta.com") is False diff --git a/tests/providers/okta/okta_fixtures.py b/tests/providers/okta/okta_fixtures.py new file mode 100644 index 0000000000..23d770cf88 --- /dev/null +++ b/tests/providers/okta/okta_fixtures.py @@ -0,0 +1,39 @@ +from unittest.mock import MagicMock + +from prowler.providers.okta.models import OktaIdentityInfo, OktaSession + +OKTA_ORG_DOMAIN = "acme.okta.com" +OKTA_CLIENT_ID = "0oa1234567890abcdef" +OKTA_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\nMOCK\n-----END PRIVATE KEY-----" + + +def set_mocked_okta_provider( + session: OktaSession = None, + identity: OktaIdentityInfo = None, + audit_config: dict = None, +): + if session is None: + session = OktaSession( + org_domain=OKTA_ORG_DOMAIN, + client_id=OKTA_CLIENT_ID, + scopes=["okta.policies.read", "okta.brands.read", "okta.apps.read"], + private_key=OKTA_PRIVATE_KEY, + ) + if identity is None: + identity = OktaIdentityInfo( + org_domain=OKTA_ORG_DOMAIN, + client_id=OKTA_CLIENT_ID, + granted_scopes=[ + "okta.policies.read", + "okta.brands.read", + "okta.apps.read", + ], + ) + + provider = MagicMock() + provider.type = "okta" + provider.auth_method = "OAuth 2.0 (private-key JWT)" + provider.session = session + provider.identity = identity + provider.audit_config = audit_config or {} + return provider diff --git a/tests/providers/okta/okta_provider_test.py b/tests/providers/okta/okta_provider_test.py new file mode 100644 index 0000000000..5f2757edae --- /dev/null +++ b/tests/providers/okta/okta_provider_test.py @@ -0,0 +1,594 @@ +import base64 +import json +from unittest import mock + +import pytest + +from prowler.providers.okta.exceptions.exceptions import ( + OktaEnvironmentVariableError, + OktaInsufficientPermissionsError, + OktaInvalidCredentialsError, + OktaInvalidOrgDomainError, + OktaInvalidProviderIdError, + OktaPrivateKeyFileError, + OktaSetUpIdentityError, +) +from prowler.providers.okta.models import OktaIdentityInfo, OktaSession +from prowler.providers.okta.okta_provider import DEFAULT_SCOPES, OktaProvider +from tests.providers.okta.okta_fixtures import ( + OKTA_CLIENT_ID, + OKTA_ORG_DOMAIN, + OKTA_PRIVATE_KEY, +) + + +def _make_jwt(payload: dict) -> str: + """Build an unsigned JWT carrying the given payload dict. + + The signature segment is irrelevant — `_decode_token_scopes` reads + the payload without verification. + """ + + def _b64u(data: bytes) -> str: + return base64.urlsafe_b64encode(data).rstrip(b"=").decode() + + header = _b64u(json.dumps({"alg": "none"}).encode()) + body = _b64u(json.dumps(payload).encode()) + return f"{header}.{body}.sig" + + +class Test_OktaProvider_decode_token_scopes: + def test_returns_scopes_from_list_scp_claim(self): + token = _make_jwt({"scp": ["okta.policies.read", "okta.brands.read"]}) + assert OktaProvider._decode_token_scopes(token) == [ + "okta.policies.read", + "okta.brands.read", + ] + + def test_returns_scopes_from_space_separated_scp_string(self): + token = _make_jwt({"scp": "okta.policies.read okta.brands.read"}) + assert OktaProvider._decode_token_scopes(token) == [ + "okta.policies.read", + "okta.brands.read", + ] + + def test_returns_empty_list_when_token_is_none(self): + assert OktaProvider._decode_token_scopes(None) == [] + + def test_returns_empty_list_when_token_is_empty_string(self): + assert OktaProvider._decode_token_scopes("") == [] + + def test_returns_empty_list_when_scp_claim_missing(self): + token = _make_jwt({"sub": "client-id"}) + assert OktaProvider._decode_token_scopes(token) == [] + + def test_returns_empty_list_when_token_is_malformed(self): + assert OktaProvider._decode_token_scopes("not.a.jwt-with-bad-base64!!") == [] + + def test_returns_empty_list_when_payload_is_not_json(self): + bad = base64.urlsafe_b64encode(b"not json").rstrip(b"=").decode() + assert OktaProvider._decode_token_scopes(f"hdr.{bad}.sig") == [] + + +@pytest.fixture +def _clear_okta_env(monkeypatch): + for var in ( + "OKTA_ORG_DOMAIN", + "OKTA_CLIENT_ID", + "OKTA_PRIVATE_KEY", + "OKTA_PRIVATE_KEY_FILE", + "OKTA_SCOPES", + ): + monkeypatch.delenv(var, raising=False) + + +class Test_OktaProvider_validate_arguments: + def test_missing_all_three_raises_combined(self, _clear_okta_env): + with pytest.raises(OktaEnvironmentVariableError) as exc: + OktaProvider.validate_arguments() + msg = str(exc.value) + assert "OKTA_ORG_DOMAIN" in msg + assert "OKTA_CLIENT_ID" in msg + assert "OKTA_PRIVATE_KEY" in msg + + def test_only_org_domain_missing(self, _clear_okta_env, tmp_path): + key_file = tmp_path / "key.pem" + key_file.write_text(OKTA_PRIVATE_KEY) + with pytest.raises(OktaEnvironmentVariableError) as exc: + OktaProvider.validate_arguments( + okta_client_id=OKTA_CLIENT_ID, + okta_private_key_file=str(key_file), + ) + assert "OKTA_ORG_DOMAIN" in str(exc.value) + + def test_accepts_private_key_content_in_place_of_file(self, _clear_okta_env): + OktaProvider.validate_arguments( + okta_org_domain=OKTA_ORG_DOMAIN, + okta_client_id=OKTA_CLIENT_ID, + okta_private_key=OKTA_PRIVATE_KEY, + ) + + def test_all_present_via_args(self, _clear_okta_env, tmp_path): + key_file = tmp_path / "key.pem" + key_file.write_text(OKTA_PRIVATE_KEY) + OktaProvider.validate_arguments( + okta_org_domain=OKTA_ORG_DOMAIN, + okta_client_id=OKTA_CLIENT_ID, + okta_private_key_file=str(key_file), + ) + + def test_all_present_via_env(self, monkeypatch, tmp_path): + key_file = tmp_path / "key.pem" + key_file.write_text(OKTA_PRIVATE_KEY) + monkeypatch.setenv("OKTA_ORG_DOMAIN", OKTA_ORG_DOMAIN) + monkeypatch.setenv("OKTA_CLIENT_ID", OKTA_CLIENT_ID) + monkeypatch.setenv("OKTA_PRIVATE_KEY_FILE", str(key_file)) + OktaProvider.validate_arguments() + + +class Test_OktaProvider_setup_session: + def test_rejects_domain_with_scheme(self, _clear_okta_env, tmp_path): + key_file = tmp_path / "key.pem" + key_file.write_text(OKTA_PRIVATE_KEY) + with pytest.raises(OktaInvalidOrgDomainError): + OktaProvider.setup_session( + org_domain="https://acme.okta.com", + client_id=OKTA_CLIENT_ID, + private_key_file=str(key_file), + ) + + def test_rejects_domain_with_trailing_slash(self, _clear_okta_env, tmp_path): + key_file = tmp_path / "key.pem" + key_file.write_text(OKTA_PRIVATE_KEY) + with pytest.raises(OktaInvalidOrgDomainError): + OktaProvider.setup_session( + org_domain="acme.okta.com/", + client_id=OKTA_CLIENT_ID, + private_key_file=str(key_file), + ) + + def test_rejects_non_okta_tld(self, _clear_okta_env, tmp_path): + key_file = tmp_path / "key.pem" + key_file.write_text(OKTA_PRIVATE_KEY) + with pytest.raises(OktaInvalidOrgDomainError): + OktaProvider.setup_session( + org_domain="login.example.com", + client_id=OKTA_CLIENT_ID, + private_key_file=str(key_file), + ) + + def test_accepts_all_okta_managed_tlds(self, _clear_okta_env, tmp_path): + # Mirrors the domain whitelist used by the Okta SDK + # (okta.config.config_validator) so that gov/mil tenants — exactly the + # audience most likely to care about the DISA STIG check — are not + # turned away at provider init. + key_file = tmp_path / "key.pem" + key_file.write_text(OKTA_PRIVATE_KEY) + for domain in ( + "acme.oktapreview.com", + "acme.okta-emea.com", + "acme.okta-gov.com", + "acme.okta.mil", + "acme.okta-miltest.com", + "acme.trex-govcloud.com", + ): + session = OktaProvider.setup_session( + org_domain=domain, + client_id=OKTA_CLIENT_ID, + private_key_file=str(key_file), + ) + assert session.org_domain == domain + + def test_unreadable_private_key_file_raises(self, _clear_okta_env): + with pytest.raises(OktaPrivateKeyFileError): + OktaProvider.setup_session( + org_domain=OKTA_ORG_DOMAIN, + client_id=OKTA_CLIENT_ID, + private_key_file="/nonexistent/path.pem", + ) + + def test_happy_path_uses_default_scopes(self, _clear_okta_env, tmp_path): + key_file = tmp_path / "key.pem" + key_file.write_text(OKTA_PRIVATE_KEY) + session = OktaProvider.setup_session( + org_domain=OKTA_ORG_DOMAIN, + client_id=OKTA_CLIENT_ID, + private_key_file=str(key_file), + ) + assert session.org_domain == OKTA_ORG_DOMAIN + assert session.client_id == OKTA_CLIENT_ID + assert session.private_key == OKTA_PRIVATE_KEY + assert session.scopes == DEFAULT_SCOPES + + def test_custom_scopes_parsed_from_csv(self, _clear_okta_env, tmp_path): + key_file = tmp_path / "key.pem" + key_file.write_text(OKTA_PRIVATE_KEY) + session = OktaProvider.setup_session( + org_domain=OKTA_ORG_DOMAIN, + client_id=OKTA_CLIENT_ID, + private_key_file=str(key_file), + scopes="okta.policies.read, okta.apps.read ,okta.users.read", + ) + assert session.scopes == [ + "okta.policies.read", + "okta.apps.read", + "okta.users.read", + ] + + def test_custom_scopes_accepts_list_input(self, _clear_okta_env, tmp_path): + key_file = tmp_path / "key.pem" + key_file.write_text(OKTA_PRIVATE_KEY) + session = OktaProvider.setup_session( + org_domain=OKTA_ORG_DOMAIN, + client_id=OKTA_CLIENT_ID, + private_key_file=str(key_file), + scopes=["okta.policies.read", "okta.apps.read", "okta.users.read"], + ) + assert session.scopes == [ + "okta.policies.read", + "okta.apps.read", + "okta.users.read", + ] + + def test_custom_scopes_flattens_mixed_list_and_csv(self, _clear_okta_env, tmp_path): + # Mirrors how argparse nargs="+" delivers values when a user + # passes "--okta-scopes a,b c" — a list whose first element still + # contains a comma. + key_file = tmp_path / "key.pem" + key_file.write_text(OKTA_PRIVATE_KEY) + session = OktaProvider.setup_session( + org_domain=OKTA_ORG_DOMAIN, + client_id=OKTA_CLIENT_ID, + private_key_file=str(key_file), + scopes=["okta.policies.read,okta.apps.read", "okta.users.read"], + ) + assert session.scopes == [ + "okta.policies.read", + "okta.apps.read", + "okta.users.read", + ] + + def test_org_domain_normalized_lowercase_and_trimmed( + self, _clear_okta_env, tmp_path + ): + # The provider lowercases and strips whitespace so that + # " ACME.okta.com " is accepted as "acme.okta.com". + key_file = tmp_path / "key.pem" + key_file.write_text(OKTA_PRIVATE_KEY) + session = OktaProvider.setup_session( + org_domain=" ACME.okta.com ", + client_id=OKTA_CLIENT_ID, + private_key_file=str(key_file), + ) + assert session.org_domain == OKTA_ORG_DOMAIN + + def test_accepts_private_key_via_content_arg(self, _clear_okta_env): + session = OktaProvider.setup_session( + org_domain=OKTA_ORG_DOMAIN, + client_id=OKTA_CLIENT_ID, + private_key=OKTA_PRIVATE_KEY, + ) + assert session.private_key == OKTA_PRIVATE_KEY + + def test_accepts_private_key_via_env_var(self, monkeypatch): + monkeypatch.setenv("OKTA_PRIVATE_KEY", OKTA_PRIVATE_KEY) + monkeypatch.delenv("OKTA_PRIVATE_KEY_FILE", raising=False) + session = OktaProvider.setup_session( + org_domain=OKTA_ORG_DOMAIN, + client_id=OKTA_CLIENT_ID, + ) + assert session.private_key == OKTA_PRIVATE_KEY + + def test_content_takes_precedence_over_file(self, _clear_okta_env, tmp_path): + # File has stale content; explicit content arg should win. + key_file = tmp_path / "stale.pem" + key_file.write_text("STALE CONTENT FROM FILE") + fresh_key = "-----BEGIN PRIVATE KEY-----\nFRESH\n-----END PRIVATE KEY-----" + session = OktaProvider.setup_session( + org_domain=OKTA_ORG_DOMAIN, + client_id=OKTA_CLIENT_ID, + private_key=fresh_key, + private_key_file=str(key_file), + ) + assert session.private_key == fresh_key + + +class Test_OktaProvider_setup_identity: + def _session(self, tmp_path): + key_file = tmp_path / "key.pem" + key_file.write_text(OKTA_PRIVATE_KEY) + return OktaProvider.setup_session( + org_domain=OKTA_ORG_DOMAIN, + client_id=OKTA_CLIENT_ID, + private_key_file=str(key_file), + ) + + def test_synthesizes_identity_and_probes_successfully( + self, _clear_okta_env, tmp_path + ): + session = self._session(tmp_path) + + async def fake_list_policies(*_a, **_k): + return ([], mock.MagicMock(headers={}), None) + + with mock.patch( + "prowler.providers.okta.okta_provider.OktaSDKClient" + ) as mocked_client_cls: + mocked = mock.MagicMock() + mocked.list_policies = fake_list_policies + mocked_client_cls.return_value = mocked + identity = OktaProvider.setup_identity(session) + + assert identity.org_domain == OKTA_ORG_DOMAIN + assert identity.client_id == OKTA_CLIENT_ID + + def test_populates_granted_scopes_from_access_token_scp_claim( + self, _clear_okta_env, tmp_path + ): + session = self._session(tmp_path) + + async def fake_list_policies(*_a, **_k): + return ([], mock.MagicMock(headers={}), None) + + with mock.patch( + "prowler.providers.okta.okta_provider.OktaSDKClient" + ) as mocked_client_cls: + mocked = mock.MagicMock() + mocked.list_policies = fake_list_policies + mocked._request_executor._oauth._access_token = _make_jwt( + {"scp": ["okta.policies.read", "okta.brands.read"]} + ) + mocked_client_cls.return_value = mocked + identity = OktaProvider.setup_identity(session) + + assert identity.granted_scopes == [ + "okta.policies.read", + "okta.brands.read", + ] + + def test_granted_scopes_empty_when_token_unavailable( + self, _clear_okta_env, tmp_path + ): + session = self._session(tmp_path) + + async def fake_list_policies(*_a, **_k): + return ([], mock.MagicMock(headers={}), None) + + with mock.patch( + "prowler.providers.okta.okta_provider.OktaSDKClient" + ) as mocked_client_cls: + mocked = mock.MagicMock() + mocked.list_policies = fake_list_policies + mocked._request_executor._oauth._access_token = None + mocked_client_cls.return_value = mocked + identity = OktaProvider.setup_identity(session) + + assert identity.granted_scopes == [] + + def test_raises_invalid_credentials_when_probe_returns_error( + self, _clear_okta_env, tmp_path + ): + session = self._session(tmp_path) + + async def failing_list_policies(*_a, **_k): + return ([], None, Exception("E0000011: Invalid token")) + + with mock.patch( + "prowler.providers.okta.okta_provider.OktaSDKClient" + ) as mocked_client_cls: + mocked = mock.MagicMock() + mocked.list_policies = failing_list_policies + mocked_client_cls.return_value = mocked + with pytest.raises(OktaInvalidCredentialsError): + OktaProvider.setup_identity(session) + + def test_raises_insufficient_permissions_on_scope_error( + self, _clear_okta_env, tmp_path + ): + session = self._session(tmp_path) + + async def failing_list_policies(*_a, **_k): + return ([], None, Exception("invalid_scope: policies.read missing")) + + with mock.patch( + "prowler.providers.okta.okta_provider.OktaSDKClient" + ) as mocked_client_cls: + mocked = mock.MagicMock() + mocked.list_policies = failing_list_policies + mocked_client_cls.return_value = mocked + with pytest.raises(OktaInsufficientPermissionsError): + OktaProvider.setup_identity(session) + + def test_raises_insufficient_permissions_on_forbidden( + self, _clear_okta_env, tmp_path + ): + session = self._session(tmp_path) + + async def failing_list_policies(*_a, **_k): + return ([], None, Exception("403 Forbidden")) + + with mock.patch( + "prowler.providers.okta.okta_provider.OktaSDKClient" + ) as mocked_client_cls: + mocked = mock.MagicMock() + mocked.list_policies = failing_list_policies + mocked_client_cls.return_value = mocked + with pytest.raises(OktaInsufficientPermissionsError): + OktaProvider.setup_identity(session) + + def test_raises_insufficient_permissions_on_consent_required( + self, _clear_okta_env, tmp_path + ): + # When zero requested scopes are consented on the service app, Okta + # rejects the token request with HTTP 400 `consent_required` rather + # than `invalid_scope` — must still be classified as a permission + # gap so the user is pointed at the Okta API Scopes tab, not at + # credential troubleshooting. + session = self._session(tmp_path) + + async def failing_list_policies(*_a, **_k): + return ( + [], + None, + Exception( + "Okta HTTP 400 consent_required You are not allowed any " + "of the requested scopes." + ), + ) + + with mock.patch( + "prowler.providers.okta.okta_provider.OktaSDKClient" + ) as mocked_client_cls: + mocked = mock.MagicMock() + mocked.list_policies = failing_list_policies + mocked_client_cls.return_value = mocked + with pytest.raises(OktaInsufficientPermissionsError): + OktaProvider.setup_identity(session) + + def test_wraps_unexpected_errors_in_setup_identity_error( + self, _clear_okta_env, tmp_path + ): + session = self._session(tmp_path) + + async def boom(*_a, **_k): + raise RuntimeError("network down") + + with mock.patch( + "prowler.providers.okta.okta_provider.OktaSDKClient" + ) as mocked_client_cls: + mocked = mock.MagicMock() + mocked.list_policies = boom + mocked_client_cls.return_value = mocked + with pytest.raises(OktaSetUpIdentityError): + OktaProvider.setup_identity(session) + + +def _mock_setup_paths(): + """Patches that bypass the real SDK during provider construction.""" + session = OktaSession( + org_domain=OKTA_ORG_DOMAIN, + client_id=OKTA_CLIENT_ID, + scopes=list(DEFAULT_SCOPES), + private_key=OKTA_PRIVATE_KEY, + ) + identity = OktaIdentityInfo(org_domain=OKTA_ORG_DOMAIN, client_id=OKTA_CLIENT_ID) + return ( + mock.patch.object(OktaProvider, "validate_arguments"), + mock.patch.object(OktaProvider, "setup_session", return_value=session), + mock.patch.object(OktaProvider, "setup_identity", return_value=identity), + ) + + +class Test_OktaProvider_init: + def test_init_end_to_end(self, _clear_okta_env, tmp_path): + validate_p, session_p, identity_p = _mock_setup_paths() + with validate_p, session_p, identity_p: + provider = OktaProvider( + okta_org_domain=OKTA_ORG_DOMAIN, + okta_client_id=OKTA_CLIENT_ID, + okta_private_key_file="/tmp/key.pem", + ) + + assert provider.type == "okta" + assert provider.auth_method == "OAuth 2.0 (private-key JWT)" + assert provider.identity.org_domain == OKTA_ORG_DOMAIN + assert provider.identity.client_id == OKTA_CLIENT_ID + assert provider.session.scopes == DEFAULT_SCOPES + assert provider.audit_config is not None + assert provider.mutelist is not None + + +class Test_OktaProvider_test_connection: + def test_success(self, _clear_okta_env, tmp_path): + validate_p, session_p, identity_p = _mock_setup_paths() + with validate_p, session_p, identity_p: + connection = OktaProvider.test_connection( + okta_org_domain=OKTA_ORG_DOMAIN, + okta_client_id=OKTA_CLIENT_ID, + okta_private_key_file="/tmp/key.pem", + ) + assert connection.is_connected is True + assert connection.error is None + + def test_returns_error_when_raise_disabled(self, _clear_okta_env): + connection = OktaProvider.test_connection(raise_on_exception=False) + assert connection.is_connected is False + assert connection.error is not None + + def test_raises_when_raise_enabled(self, _clear_okta_env): + with pytest.raises(OktaEnvironmentVariableError): + OktaProvider.test_connection() + + def test_provider_id_match_succeeds(self, _clear_okta_env, tmp_path): + validate_p, session_p, identity_p = _mock_setup_paths() + with validate_p, session_p, identity_p: + connection = OktaProvider.test_connection( + okta_org_domain=OKTA_ORG_DOMAIN, + okta_client_id=OKTA_CLIENT_ID, + okta_private_key_file="/tmp/key.pem", + provider_id=OKTA_ORG_DOMAIN, + ) + assert connection.is_connected is True + assert connection.error is None + + def test_provider_id_match_is_case_insensitive(self, _clear_okta_env, tmp_path): + validate_p, session_p, identity_p = _mock_setup_paths() + with validate_p, session_p, identity_p: + connection = OktaProvider.test_connection( + okta_org_domain=OKTA_ORG_DOMAIN, + okta_client_id=OKTA_CLIENT_ID, + okta_private_key_file="/tmp/key.pem", + provider_id=OKTA_ORG_DOMAIN.upper(), + ) + assert connection.is_connected is True + + def test_provider_id_mismatch_raises(self, _clear_okta_env, tmp_path): + validate_p, session_p, identity_p = _mock_setup_paths() + with validate_p, session_p, identity_p: + with pytest.raises(OktaInvalidProviderIdError): + OktaProvider.test_connection( + okta_org_domain=OKTA_ORG_DOMAIN, + okta_client_id=OKTA_CLIENT_ID, + okta_private_key_file="/tmp/key.pem", + provider_id="other.okta.com", + ) + + def test_provider_id_mismatch_returns_error_when_raise_disabled( + self, _clear_okta_env, tmp_path + ): + validate_p, session_p, identity_p = _mock_setup_paths() + with validate_p, session_p, identity_p: + connection = OktaProvider.test_connection( + okta_org_domain=OKTA_ORG_DOMAIN, + okta_client_id=OKTA_CLIENT_ID, + okta_private_key_file="/tmp/key.pem", + provider_id="other.okta.com", + raise_on_exception=False, + ) + assert connection.is_connected is False + assert isinstance(connection.error, OktaInvalidProviderIdError) + + +class Test_OktaProvider_print_credentials: + def test_invokes_print_boxes_with_org_and_client(self, _clear_okta_env, tmp_path): + validate_p, session_p, identity_p = _mock_setup_paths() + with ( + validate_p, + session_p, + identity_p, + mock.patch( + "prowler.providers.okta.okta_provider.print_boxes" + ) as mock_print, + ): + provider = OktaProvider( + okta_org_domain=OKTA_ORG_DOMAIN, + okta_client_id=OKTA_CLIENT_ID, + okta_private_key_file="/tmp/key.pem", + ) + provider.print_credentials() + + mock_print.assert_called_once() + rendered = " ".join(mock_print.call_args.args[0]) + assert OKTA_ORG_DOMAIN in rendered + assert OKTA_CLIENT_ID in rendered + assert "OAuth 2.0" in rendered diff --git a/tests/providers/okta/services/application/application_admin_console_mfa_required/application_admin_console_mfa_required_test.py b/tests/providers/okta/services/application/application_admin_console_mfa_required/application_admin_console_mfa_required_test.py new file mode 100644 index 0000000000..e968c991a3 --- /dev/null +++ b/tests/providers/okta/services/application/application_admin_console_mfa_required/application_admin_console_mfa_required_test.py @@ -0,0 +1,149 @@ +from unittest import mock + +from prowler.providers.okta.services.application.application_service import ( + ADMIN_CONSOLE_APP_NAME, +) +from tests.providers.okta.okta_fixtures import set_mocked_okta_provider +from tests.providers.okta.services.application.application_fixtures import ( + admin_console_app, + auth_policy_rule, + build_application_client, + catch_all_rule, +) + +CHECK_PATH = ( + "prowler.providers.okta.services.application." + "application_admin_console_mfa_required." + "application_admin_console_mfa_required.application_client" +) + + +def _run_check(client): + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_okta_provider(), + ), + mock.patch(CHECK_PATH, new=client), + ): + from prowler.providers.okta.services.application.application_admin_console_mfa_required.application_admin_console_mfa_required import ( + application_admin_console_mfa_required, + ) + + return application_admin_console_mfa_required().execute() + + +class Test_application_admin_console_mfa_required: + def test_pass_when_top_rule_enforces_2fa(self): + app = admin_console_app( + rules=[ + auth_policy_rule(name="MFA Required", priority=1, factor_mode="2FA"), + catch_all_rule(priority=2), + ] + ) + client = build_application_client(built_in_apps={ADMIN_CONSOLE_APP_NAME: app}) + findings = _run_check(client) + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "MFA Required" in findings[0].status_extended + assert "factorMode=2FA" in findings[0].status_extended + + def test_fail_when_top_rule_is_1fa(self): + app = admin_console_app( + rules=[ + auth_policy_rule(name="Password Only", priority=1, factor_mode="1FA"), + catch_all_rule(priority=2), + ] + ) + client = build_application_client(built_in_apps={ADMIN_CONSOLE_APP_NAME: app}) + findings = _run_check(client) + assert findings[0].status == "FAIL" + assert "Password Only" in findings[0].status_extended + assert "factorMode=1FA" in findings[0].status_extended + + def test_pass_when_top_rule_is_default_and_enforces_2fa(self): + app = admin_console_app(rules=[catch_all_rule(priority=1, factor_mode="2FA")]) + client = build_application_client(built_in_apps={ADMIN_CONSOLE_APP_NAME: app}) + findings = _run_check(client) + assert findings[0].status == "PASS" + assert "Catch-all Rule" in findings[0].status_extended + assert "built-in Catch-all Rule" in findings[0].status_extended + + def test_pass_when_top_active_rule_is_not_priority_one(self): + # Top active rule is whichever has the lowest priority value; the + # check does not pin to `priority == 1` specifically because Okta + # indexes Access Policy rule priorities inconsistently. Here the + # only non-default rule sits at priority=2 and is still the top. + app = admin_console_app( + rules=[ + auth_policy_rule(name="MFA Required", priority=2, factor_mode="2FA"), + catch_all_rule(priority=3), + ] + ) + client = build_application_client(built_in_apps={ADMIN_CONSOLE_APP_NAME: app}) + findings = _run_check(client) + assert findings[0].status == "PASS" + assert "MFA Required" in findings[0].status_extended + assert "factorMode=2FA" in findings[0].status_extended + + def test_fail_when_no_access_policy_bound(self): + app = admin_console_app(rules=[], access_policy_id=None) + client = build_application_client(built_in_apps={ADMIN_CONSOLE_APP_NAME: app}) + findings = _run_check(client) + assert findings[0].status == "FAIL" + assert "no Authentication Policy bound" in findings[0].status_extended + + def test_manual_when_app_not_returned(self): + client = build_application_client(built_in_apps={}) + findings = _run_check(client) + assert findings[0].status == "MANUAL" + assert "not returned by the Okta API" in findings[0].status_extended + + def test_manual_when_apps_scope_missing(self): + client = build_application_client( + missing_scope={ + "admin_console_app_settings": None, + "built_in_apps": "okta.apps.read", + "integrated_apps": None, + "access_policies": None, + } + ) + findings = _run_check(client) + assert findings[0].status == "MANUAL" + assert "okta.apps.read" in findings[0].status_extended + + def test_manual_when_policies_scope_missing(self): + client = build_application_client( + missing_scope={ + "admin_console_app_settings": None, + "built_in_apps": None, + "integrated_apps": None, + "access_policies": "okta.policies.read", + } + ) + findings = _run_check(client) + assert findings[0].status == "MANUAL" + assert "okta.policies.read" in findings[0].status_extended + + def test_inactive_rule_skipped(self): + # An inactive custom rule must be skipped; the active Catch-all + # then becomes the top rule. The check evaluates the catch-all + # directly (no `factor_mode` set on it in the fixture) and FAILs. + app = admin_console_app( + rules=[ + auth_policy_rule( + name="MFA Required", + priority=1, + factor_mode="2FA", + status="INACTIVE", + ), + catch_all_rule(priority=2), + ] + ) + client = build_application_client(built_in_apps={ADMIN_CONSOLE_APP_NAME: app}) + findings = _run_check(client) + assert findings[0].status == "FAIL" + assert "Catch-all Rule" in findings[0].status_extended + assert "does not enforce multifactor authentication" in ( + findings[0].status_extended + ) diff --git a/tests/providers/okta/services/application/application_admin_console_phishing_resistant_authentication/application_admin_console_phishing_resistant_authentication_test.py b/tests/providers/okta/services/application/application_admin_console_phishing_resistant_authentication/application_admin_console_phishing_resistant_authentication_test.py new file mode 100644 index 0000000000..9ceac0697d --- /dev/null +++ b/tests/providers/okta/services/application/application_admin_console_phishing_resistant_authentication/application_admin_console_phishing_resistant_authentication_test.py @@ -0,0 +1,129 @@ +from unittest import mock + +from prowler.providers.okta.services.application.application_service import ( + ADMIN_CONSOLE_APP_NAME, +) +from tests.providers.okta.okta_fixtures import set_mocked_okta_provider +from tests.providers.okta.services.application.application_fixtures import ( + admin_console_app, + auth_policy_rule, + build_application_client, + catch_all_rule, +) + +CHECK_PATH = ( + "prowler.providers.okta.services.application." + "application_admin_console_phishing_resistant_authentication." + "application_admin_console_phishing_resistant_authentication.application_client" +) + + +def _run_check(client): + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_okta_provider(), + ), + mock.patch(CHECK_PATH, new=client), + ): + from prowler.providers.okta.services.application.application_admin_console_phishing_resistant_authentication.application_admin_console_phishing_resistant_authentication import ( + application_admin_console_phishing_resistant_authentication, + ) + + return application_admin_console_phishing_resistant_authentication().execute() + + +class Test_application_admin_console_phishing_resistant_authentication: + def test_pass_when_top_rule_requires_phishing_resistant(self): + app = admin_console_app( + rules=[ + auth_policy_rule( + name="Phishing Resistant", priority=1, phishing_resistant=True + ), + catch_all_rule(priority=2), + ] + ) + client = build_application_client(built_in_apps={ADMIN_CONSOLE_APP_NAME: app}) + findings = _run_check(client) + assert findings[0].status == "PASS" + assert "Phishing Resistant" in findings[0].status_extended + + def test_fail_when_top_rule_does_not_require(self): + app = admin_console_app( + rules=[ + auth_policy_rule( + name="Loose Rule", priority=1, phishing_resistant=False + ), + catch_all_rule(priority=2), + ] + ) + client = build_application_client(built_in_apps={ADMIN_CONSOLE_APP_NAME: app}) + findings = _run_check(client) + assert findings[0].status == "FAIL" + assert "does not enforce phishing-resistant" in findings[0].status_extended + + def test_pass_when_top_rule_is_default_and_requires_phishing_resistant(self): + app = admin_console_app( + rules=[catch_all_rule(priority=1, phishing_resistant=True)] + ) + client = build_application_client(built_in_apps={ADMIN_CONSOLE_APP_NAME: app}) + findings = _run_check(client) + assert findings[0].status == "PASS" + assert "Catch-all Rule" in findings[0].status_extended + assert "built-in Catch-all Rule" in findings[0].status_extended + + def test_pass_when_top_active_rule_is_not_priority_one(self): + # The check does not pin to `priority == 1` — Okta indexes Access + # Policy rule priorities inconsistently. The top active rule is + # whichever has the lowest priority value among active rules. + app = admin_console_app( + rules=[ + auth_policy_rule( + name="Phishing Resistant", priority=2, phishing_resistant=True + ), + catch_all_rule(priority=3), + ] + ) + client = build_application_client(built_in_apps={ADMIN_CONSOLE_APP_NAME: app}) + findings = _run_check(client) + assert findings[0].status == "PASS" + assert "Phishing Resistant" in findings[0].status_extended + + def test_fail_when_no_access_policy_bound(self): + app = admin_console_app(rules=[], access_policy_id=None) + client = build_application_client(built_in_apps={ADMIN_CONSOLE_APP_NAME: app}) + findings = _run_check(client) + assert findings[0].status == "FAIL" + assert "no Authentication Policy bound" in findings[0].status_extended + + def test_manual_when_app_not_returned(self): + client = build_application_client(built_in_apps={}) + findings = _run_check(client) + assert findings[0].status == "MANUAL" + assert "not returned by the Okta API" in findings[0].status_extended + + def test_manual_when_apps_scope_missing(self): + client = build_application_client( + missing_scope={ + "admin_console_app_settings": None, + "built_in_apps": "okta.apps.read", + "integrated_apps": None, + "access_policies": None, + } + ) + findings = _run_check(client) + assert findings[0].status == "MANUAL" + assert "okta.apps.read" in findings[0].status_extended + + def test_manual_when_policies_scope_missing(self): + client = build_application_client( + missing_scope={ + "admin_console_app_settings": None, + "built_in_apps": None, + "integrated_apps": None, + "access_policies": "okta.policies.read", + } + ) + findings = _run_check(client) + assert findings[0].status == "MANUAL" + assert "okta.policies.read" in findings[0].status_extended diff --git a/tests/providers/okta/services/application/application_admin_console_session_idle_timeout_15min/application_admin_console_session_idle_timeout_15min_test.py b/tests/providers/okta/services/application/application_admin_console_session_idle_timeout_15min/application_admin_console_session_idle_timeout_15min_test.py new file mode 100644 index 0000000000..08943cd690 --- /dev/null +++ b/tests/providers/okta/services/application/application_admin_console_session_idle_timeout_15min/application_admin_console_session_idle_timeout_15min_test.py @@ -0,0 +1,90 @@ +from unittest import mock + +from tests.providers.okta.okta_fixtures import set_mocked_okta_provider +from tests.providers.okta.services.application.application_fixtures import ( + admin_console_settings, + build_application_client, +) + +CHECK_PATH = ( + "prowler.providers.okta.services.application." + "application_admin_console_session_idle_timeout_15min." + "application_admin_console_session_idle_timeout_15min.application_client" +) + + +def _run_check(client): + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_okta_provider(), + ), + mock.patch(CHECK_PATH, new=client), + ): + from prowler.providers.okta.services.application.application_admin_console_session_idle_timeout_15min.application_admin_console_session_idle_timeout_15min import ( + application_admin_console_session_idle_timeout_15min, + ) + + return application_admin_console_session_idle_timeout_15min().execute() + + +class Test_application_admin_console_session_idle_timeout_15min: + def test_pass_at_threshold(self): + client = build_application_client( + admin_console_settings=admin_console_settings(idle_timeout=15) + ) + findings = _run_check(client) + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "15 minutes" in findings[0].status_extended + + def test_pass_below_threshold(self): + client = build_application_client( + admin_console_settings=admin_console_settings(idle_timeout=10) + ) + findings = _run_check(client) + assert findings[0].status == "PASS" + assert "10 minutes" in findings[0].status_extended + + def test_fail_above_threshold(self): + client = build_application_client( + admin_console_settings=admin_console_settings(idle_timeout=60) + ) + findings = _run_check(client) + assert findings[0].status == "FAIL" + assert "exceeding the configured threshold" in findings[0].status_extended + + def test_fail_when_idle_timeout_missing(self): + client = build_application_client( + admin_console_settings=admin_console_settings(idle_timeout=None) + ) + findings = _run_check(client) + assert findings[0].status == "FAIL" + assert "does not define" in findings[0].status_extended + + def test_manual_when_settings_unavailable(self): + client = build_application_client(admin_console_settings=None) + findings = _run_check(client) + assert findings[0].status == "MANUAL" + assert "Could not retrieve" in findings[0].status_extended + + def test_manual_when_scope_missing(self): + client = build_application_client( + missing_scope={ + "admin_console_app_settings": "okta.apps.read", + "built_in_apps": None, + "access_policies": None, + } + ) + findings = _run_check(client) + assert findings[0].status == "MANUAL" + assert "okta.apps.read" in findings[0].status_extended + + def test_threshold_overridden_via_audit_config(self): + client = build_application_client( + admin_console_settings=admin_console_settings(idle_timeout=30), + audit_config={"okta_admin_console_idle_timeout_max_minutes": 60}, + ) + findings = _run_check(client) + assert findings[0].status == "PASS" + assert "threshold of 60 minutes" in findings[0].status_extended diff --git a/tests/providers/okta/services/application/application_authentication_policy_network_zone_enforced/application_authentication_policy_network_zone_enforced_test.py b/tests/providers/okta/services/application/application_authentication_policy_network_zone_enforced/application_authentication_policy_network_zone_enforced_test.py new file mode 100644 index 0000000000..92e75a6e17 --- /dev/null +++ b/tests/providers/okta/services/application/application_authentication_policy_network_zone_enforced/application_authentication_policy_network_zone_enforced_test.py @@ -0,0 +1,192 @@ +from unittest import mock + +from tests.providers.okta.okta_fixtures import set_mocked_okta_provider +from tests.providers.okta.services.application.application_fixtures import ( + auth_policy_rule, + build_application_client, + catch_all_rule, + integrated_app, +) + +CHECK_PATH = ( + "prowler.providers.okta.services.application." + "application_authentication_policy_network_zone_enforced." + "application_authentication_policy_network_zone_enforced.application_client" +) + + +def _run_check(client): + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_okta_provider(), + ), + mock.patch(CHECK_PATH, new=client), + ): + from prowler.providers.okta.services.application.application_authentication_policy_network_zone_enforced.application_authentication_policy_network_zone_enforced import ( + application_authentication_policy_network_zone_enforced, + ) + + return application_authentication_policy_network_zone_enforced().execute() + + +class Test_application_authentication_policy_network_zone_enforced: + def test_pass_when_all_active_nondefault_rules_are_zoned_and_catch_all_denies(self): + app = integrated_app( + "0oa-google", + "google_workspace", + label="Google Workspace", + rules=[ + auth_policy_rule( + name="Allow Corp", + priority=1, + network_connection="ZONE", + network_zones_include=["zone-corp"], + ), + auth_policy_rule( + name="Block Risky", + priority=2, + network_connection="ZONE", + network_zones_exclude=["zone-risky"], + ), + catch_all_rule(priority=3, access="DENY"), + ], + ) + findings = _run_check(build_application_client(integrated_apps={app.id: app})) + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "Google Workspace" in findings[0].status_extended + assert "Catch-all Rule" in findings[0].status_extended + + def test_fail_when_nondefault_rule_has_no_network_zone(self): + app = integrated_app( + "0oa-salesforce", + "salesforce", + label="Salesforce", + rules=[ + auth_policy_rule(name="Allow Users", priority=1), + catch_all_rule(priority=2, access="DENY"), + ], + ) + findings = _run_check(build_application_client(integrated_apps={app.id: app})) + assert findings[0].status == "FAIL" + assert "without Network Zones" in findings[0].status_extended + assert "Allow Users" in findings[0].status_extended + + def test_fail_when_catch_all_rule_does_not_deny(self): + app = integrated_app( + "0oa-box", + "box", + label="Box", + rules=[ + auth_policy_rule( + name="Allow Corp", + priority=1, + network_connection="ZONE", + network_zones_include=["zone-corp"], + ), + catch_all_rule(priority=2, access="ALLOW"), + ], + ) + findings = _run_check(build_application_client(integrated_apps={app.id: app})) + assert findings[0].status == "FAIL" + assert "`Access is` to `DENY`" in findings[0].status_extended + + def test_fail_when_no_active_nondefault_rules_exist(self): + app = integrated_app( + "0oa-slack", + "slack", + label="Slack", + rules=[catch_all_rule(priority=1, access="DENY")], + ) + findings = _run_check(build_application_client(integrated_apps={app.id: app})) + assert findings[0].status == "FAIL" + assert "no active non-default rules" in findings[0].status_extended + + def test_fail_when_no_access_policy_is_bound(self): + app = integrated_app( + "0oa-zoom", + "zoom", + label="Zoom", + rules=[], + access_policy_id=None, + ) + findings = _run_check(build_application_client(integrated_apps={app.id: app})) + assert findings[0].status == "FAIL" + assert "no Authentication Policy bound" in findings[0].status_extended + + def test_inactive_apps_are_skipped(self): + inactive = integrated_app( + "0oa-inactive", + "dropbox", + label="Dropbox", + status="INACTIVE", + rules=[ + auth_policy_rule( + name="Allow Corp", + priority=1, + network_connection="ZONE", + network_zones_include=["zone-corp"], + ), + catch_all_rule(priority=2, access="DENY"), + ], + ) + active = integrated_app( + "0oa-active", + "github", + label="GitHub", + rules=[ + auth_policy_rule( + name="Allow Corp", + priority=1, + network_connection="ZONE", + network_zones_include=["zone-corp"], + ), + catch_all_rule(priority=2, access="DENY"), + ], + ) + findings = _run_check( + build_application_client( + integrated_apps={inactive.id: inactive, active.id: active} + ) + ) + assert len(findings) == 1 + assert findings[0].resource_name == "GitHub" + assert findings[0].status == "PASS" + + def test_manual_when_apps_scope_missing(self): + findings = _run_check( + build_application_client( + missing_scope={ + "admin_console_app_settings": None, + "built_in_apps": None, + "integrated_apps": "okta.apps.read", + "access_policies": None, + } + ) + ) + assert findings[0].status == "MANUAL" + assert "okta.apps.read" in findings[0].status_extended + assert findings[0].resource_name == "Okta integrated applications" + assert findings[0].resource_id == "okta-integrated-apps-scope-missing" + + def test_manual_when_policy_scope_missing(self): + findings = _run_check( + build_application_client( + missing_scope={ + "admin_console_app_settings": None, + "built_in_apps": None, + "integrated_apps": None, + "access_policies": "okta.policies.read", + } + ) + ) + assert findings[0].status == "MANUAL" + assert "okta.policies.read" in findings[0].status_extended + assert findings[0].resource_name == "Okta integrated applications" + assert findings[0].resource_id == "okta-integrated-apps-scope-missing" + + def test_manual_when_no_active_apps_returned(self): + findings = _run_check(build_application_client(integrated_apps={})) + assert findings[0].status == "MANUAL" + assert "No active Okta applications" in findings[0].status_extended diff --git a/tests/providers/okta/services/application/application_dashboard_mfa_required/application_dashboard_mfa_required_test.py b/tests/providers/okta/services/application/application_dashboard_mfa_required/application_dashboard_mfa_required_test.py new file mode 100644 index 0000000000..2a07c82ea8 --- /dev/null +++ b/tests/providers/okta/services/application/application_dashboard_mfa_required/application_dashboard_mfa_required_test.py @@ -0,0 +1,106 @@ +from unittest import mock + +from prowler.providers.okta.services.application.application_service import ( + DASHBOARD_APP_NAME, +) +from tests.providers.okta.okta_fixtures import set_mocked_okta_provider +from tests.providers.okta.services.application.application_fixtures import ( + auth_policy_rule, + build_application_client, + catch_all_rule, + dashboard_app, +) + +CHECK_PATH = ( + "prowler.providers.okta.services.application." + "application_dashboard_mfa_required." + "application_dashboard_mfa_required.application_client" +) + + +def _run_check(client): + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_okta_provider(), + ), + mock.patch(CHECK_PATH, new=client), + ): + from prowler.providers.okta.services.application.application_dashboard_mfa_required.application_dashboard_mfa_required import ( + application_dashboard_mfa_required, + ) + + return application_dashboard_mfa_required().execute() + + +class Test_application_dashboard_mfa_required: + def test_pass_when_top_rule_enforces_2fa(self): + app = dashboard_app( + rules=[ + auth_policy_rule(name="MFA Required", priority=1, factor_mode="2FA"), + catch_all_rule(priority=2), + ] + ) + client = build_application_client(built_in_apps={DASHBOARD_APP_NAME: app}) + findings = _run_check(client) + assert findings[0].status == "PASS" + assert "MFA Required" in findings[0].status_extended + + def test_fail_when_top_rule_is_1fa(self): + app = dashboard_app( + rules=[ + auth_policy_rule(name="Password Only", priority=1, factor_mode="1FA"), + catch_all_rule(priority=2), + ] + ) + client = build_application_client(built_in_apps={DASHBOARD_APP_NAME: app}) + findings = _run_check(client) + assert findings[0].status == "FAIL" + assert "Password Only" in findings[0].status_extended + + def test_pass_when_top_rule_is_default_and_enforces_2fa(self): + app = dashboard_app(rules=[catch_all_rule(priority=1, factor_mode="2FA")]) + client = build_application_client(built_in_apps={DASHBOARD_APP_NAME: app}) + findings = _run_check(client) + assert findings[0].status == "PASS" + assert "Catch-all Rule" in findings[0].status_extended + assert "built-in Catch-all Rule" in findings[0].status_extended + + def test_fail_when_no_access_policy_bound(self): + app = dashboard_app(rules=[], access_policy_id=None) + client = build_application_client(built_in_apps={DASHBOARD_APP_NAME: app}) + findings = _run_check(client) + assert findings[0].status == "FAIL" + assert "no Authentication Policy bound" in findings[0].status_extended + + def test_manual_when_app_not_returned(self): + client = build_application_client(built_in_apps={}) + findings = _run_check(client) + assert findings[0].status == "MANUAL" + assert "not returned by the Okta API" in findings[0].status_extended + + def test_manual_when_apps_scope_missing(self): + client = build_application_client( + missing_scope={ + "admin_console_app_settings": None, + "built_in_apps": "okta.apps.read", + "integrated_apps": None, + "access_policies": None, + } + ) + findings = _run_check(client) + assert findings[0].status == "MANUAL" + assert "okta.apps.read" in findings[0].status_extended + + def test_manual_when_policies_scope_missing(self): + client = build_application_client( + missing_scope={ + "admin_console_app_settings": None, + "built_in_apps": None, + "integrated_apps": None, + "access_policies": "okta.policies.read", + } + ) + findings = _run_check(client) + assert findings[0].status == "MANUAL" + assert "okta.policies.read" in findings[0].status_extended diff --git a/tests/providers/okta/services/application/application_dashboard_phishing_resistant_authentication/application_dashboard_phishing_resistant_authentication_test.py b/tests/providers/okta/services/application/application_dashboard_phishing_resistant_authentication/application_dashboard_phishing_resistant_authentication_test.py new file mode 100644 index 0000000000..10b6a977e6 --- /dev/null +++ b/tests/providers/okta/services/application/application_dashboard_phishing_resistant_authentication/application_dashboard_phishing_resistant_authentication_test.py @@ -0,0 +1,110 @@ +from unittest import mock + +from prowler.providers.okta.services.application.application_service import ( + DASHBOARD_APP_NAME, +) +from tests.providers.okta.okta_fixtures import set_mocked_okta_provider +from tests.providers.okta.services.application.application_fixtures import ( + auth_policy_rule, + build_application_client, + catch_all_rule, + dashboard_app, +) + +CHECK_PATH = ( + "prowler.providers.okta.services.application." + "application_dashboard_phishing_resistant_authentication." + "application_dashboard_phishing_resistant_authentication.application_client" +) + + +def _run_check(client): + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_okta_provider(), + ), + mock.patch(CHECK_PATH, new=client), + ): + from prowler.providers.okta.services.application.application_dashboard_phishing_resistant_authentication.application_dashboard_phishing_resistant_authentication import ( + application_dashboard_phishing_resistant_authentication, + ) + + return application_dashboard_phishing_resistant_authentication().execute() + + +class Test_application_dashboard_phishing_resistant_authentication: + def test_pass_when_top_rule_requires_phishing_resistant(self): + app = dashboard_app( + rules=[ + auth_policy_rule( + name="Phishing Resistant", priority=1, phishing_resistant=True + ), + catch_all_rule(priority=2), + ] + ) + client = build_application_client(built_in_apps={DASHBOARD_APP_NAME: app}) + findings = _run_check(client) + assert findings[0].status == "PASS" + assert "Phishing Resistant" in findings[0].status_extended + + def test_fail_when_top_rule_does_not_require(self): + app = dashboard_app( + rules=[ + auth_policy_rule( + name="Loose Rule", priority=1, phishing_resistant=False + ), + catch_all_rule(priority=2), + ] + ) + client = build_application_client(built_in_apps={DASHBOARD_APP_NAME: app}) + findings = _run_check(client) + assert findings[0].status == "FAIL" + assert "does not enforce phishing-resistant" in findings[0].status_extended + + def test_pass_when_top_rule_is_default_and_requires_phishing_resistant(self): + app = dashboard_app(rules=[catch_all_rule(priority=1, phishing_resistant=True)]) + client = build_application_client(built_in_apps={DASHBOARD_APP_NAME: app}) + findings = _run_check(client) + assert findings[0].status == "PASS" + assert "Catch-all Rule" in findings[0].status_extended + assert "built-in Catch-all Rule" in findings[0].status_extended + + def test_fail_when_no_access_policy_bound(self): + app = dashboard_app(rules=[], access_policy_id=None) + client = build_application_client(built_in_apps={DASHBOARD_APP_NAME: app}) + findings = _run_check(client) + assert findings[0].status == "FAIL" + assert "no Authentication Policy bound" in findings[0].status_extended + + def test_manual_when_app_not_returned(self): + client = build_application_client(built_in_apps={}) + findings = _run_check(client) + assert findings[0].status == "MANUAL" + assert "not returned by the Okta API" in findings[0].status_extended + + def test_manual_when_apps_scope_missing(self): + client = build_application_client( + missing_scope={ + "admin_console_app_settings": None, + "built_in_apps": "okta.apps.read", + "integrated_apps": None, + "access_policies": None, + } + ) + findings = _run_check(client) + assert findings[0].status == "MANUAL" + assert "okta.apps.read" in findings[0].status_extended + + def test_manual_when_policies_scope_missing(self): + client = build_application_client( + missing_scope={ + "admin_console_app_settings": None, + "built_in_apps": None, + "integrated_apps": None, + "access_policies": "okta.policies.read", + } + ) + findings = _run_check(client) + assert findings[0].status == "MANUAL" + assert "okta.policies.read" in findings[0].status_extended diff --git a/tests/providers/okta/services/application/application_fixtures.py b/tests/providers/okta/services/application/application_fixtures.py new file mode 100644 index 0000000000..4c157a1255 --- /dev/null +++ b/tests/providers/okta/services/application/application_fixtures.py @@ -0,0 +1,175 @@ +"""Shared helpers for `application` service check tests. + +Mirrors `signon_fixtures.py`. The four authentication-policy checks +(MFA + phishing-resistant for Okta Admin Console and Okta Dashboard) +and the Admin Console idle-timeout check all consume the same client +shape, so the helpers stay close to the signon equivalents. +""" + +from unittest import mock + +from prowler.providers.okta.services.application.application_service import ( + ADMIN_CONSOLE_APP_NAME, + DASHBOARD_APP_NAME, + AdminConsoleAppSettings, + AuthenticationPolicy, + AuthenticationPolicyRule, + OktaBuiltInApp, +) +from tests.providers.okta.okta_fixtures import set_mocked_okta_provider + + +def build_application_client( + admin_console_settings: AdminConsoleAppSettings = None, + built_in_apps: dict = None, + integrated_apps: dict = None, + audit_config: dict = None, + missing_scope: dict = None, +): + client = mock.MagicMock() + client.admin_console_app_settings = admin_console_settings + client.built_in_apps = built_in_apps or {} + client.integrated_apps = integrated_apps or {} + client.provider = set_mocked_okta_provider() + client.audit_config = audit_config or {} + # Default to "all scopes granted" so existing tests keep working. + client.missing_scope = missing_scope or { + "admin_console_app_settings": None, + "built_in_apps": None, + "integrated_apps": None, + "access_policies": None, + } + return client + + +def admin_console_settings( + idle_timeout: int = None, + max_lifetime: int = None, +) -> AdminConsoleAppSettings: + return AdminConsoleAppSettings( + session_idle_timeout_minutes=idle_timeout, + session_max_lifetime_minutes=max_lifetime, + ) + + +def auth_policy_rule( + name: str = "Catch-all Rule", + *, + priority: int = 1, + status: str = "ACTIVE", + is_default: bool = False, + factor_mode: str = None, + phishing_resistant: bool = False, + constraints_count: int = 0, + verification_method_type: str = "ASSURANCE", + access: str = "ALLOW", + network_connection: str = None, + network_zones_include: list[str] = None, + network_zones_exclude: list[str] = None, +): + return AuthenticationPolicyRule( + id=f"rule-{name.lower().replace(' ', '-')}", + name=name, + priority=priority, + status=status, + is_default=is_default, + factor_mode=factor_mode, + possession_phishing_resistant_required=phishing_resistant, + constraints_count=constraints_count, + verification_method_type=verification_method_type, + access=access, + network_connection=network_connection, + network_zones_include=network_zones_include or [], + network_zones_exclude=network_zones_exclude or [], + ) + + +def catch_all_rule( + priority: int = 2, + *, + factor_mode: str = None, + phishing_resistant: bool = False, + access: str = "ALLOW", + network_connection: str = None, + network_zones_include: list[str] = None, + network_zones_exclude: list[str] = None, +): + return auth_policy_rule( + name="Catch-all Rule", + priority=priority, + is_default=True, + factor_mode=factor_mode, + phishing_resistant=phishing_resistant, + access=access, + network_connection=network_connection, + network_zones_include=network_zones_include, + network_zones_exclude=network_zones_exclude, + ) + + +def admin_console_app( + rules: list = None, + *, + access_policy_id: str = "rstadminconsole", + label: str = "Okta Admin Console", + status: str = "ACTIVE", +): + policy = ( + AuthenticationPolicy(id=access_policy_id, rules=rules or []) + if access_policy_id is not None + else None + ) + return OktaBuiltInApp( + id="0oaadminconsole", + name=ADMIN_CONSOLE_APP_NAME, + label=label, + status=status, + access_policy_id=access_policy_id, + access_policy=policy, + ) + + +def dashboard_app( + rules: list = None, + *, + access_policy_id: str = "rstdashboard", + label: str = "Okta Dashboard", + status: str = "ACTIVE", +): + policy = ( + AuthenticationPolicy(id=access_policy_id, rules=rules or []) + if access_policy_id is not None + else None + ) + return OktaBuiltInApp( + id="0oadashboard", + name=DASHBOARD_APP_NAME, + label=label, + status=status, + access_policy_id=access_policy_id, + access_policy=policy, + ) + + +def integrated_app( + app_id: str, + name: str, + *, + rules: list = None, + access_policy_id: str = "rstapp", + label: str = "", + status: str = "ACTIVE", +): + policy = ( + AuthenticationPolicy(id=access_policy_id, rules=rules or []) + if access_policy_id is not None + else None + ) + return OktaBuiltInApp( + id=app_id, + name=name, + label=label or name, + status=status, + access_policy_id=access_policy_id, + access_policy=policy, + ) diff --git a/tests/providers/okta/services/application/application_service_test.py b/tests/providers/okta/services/application/application_service_test.py new file mode 100644 index 0000000000..042882f285 --- /dev/null +++ b/tests/providers/okta/services/application/application_service_test.py @@ -0,0 +1,750 @@ +import json +from unittest import mock + +from prowler.providers.okta.models import OktaIdentityInfo +from prowler.providers.okta.services.application.application_service import ( + Application, + AuthenticationPolicy, + OktaBuiltInApp, + _policy_id_from_href, +) +from tests.providers.okta.okta_fixtures import ( + OKTA_CLIENT_ID, + OKTA_ORG_DOMAIN, + set_mocked_okta_provider, +) + + +def _resp(headers: dict = None): + r = mock.MagicMock() + r.headers = headers or {} + return r + + +def _fake_app( + app_id: str, + name: str, + *, + access_policy_href: str = None, + label: str = "", + status: str = "ACTIVE", +): + a = mock.MagicMock() + a.id = app_id + a.name = name + a.label = label + a.status = status + if access_policy_href is None: + a.links = None + else: + a.links.access_policy.href = access_policy_href + return a + + +def _fake_rule( + *, + rule_id: str = "rule-1", + name: str = "Catch-all Rule", + priority: int = 1, + status: str = "ACTIVE", + system: bool = False, + factor_mode: str = None, + phishing_resistant: str = None, + access: str = "ALLOW", + network_connection: str = None, + network_include: list[str] = None, + network_exclude: list[str] = None, +): + r = mock.MagicMock() + r.id = rule_id + r.name = name + r.priority = priority + r.status = status + r.system = system + r.actions.app_sign_on.verification_method.factor_mode = factor_mode + r.actions.app_sign_on.verification_method.type = "ASSURANCE" + r.actions.app_sign_on.access = access + r.conditions.network.connection = network_connection + r.conditions.network.include = network_include or [] + r.conditions.network.exclude = network_exclude or [] + if phishing_resistant is None: + r.actions.app_sign_on.verification_method.constraints = [] + else: + constraint = mock.MagicMock() + constraint.possession.phishing_resistant = phishing_resistant + r.actions.app_sign_on.verification_method.constraints = [constraint] + return r + + +def _fake_admin_console_settings(idle: int = 15, lifetime: int = 720): + s = mock.MagicMock() + s.session_idle_timeout_minutes = idle + s.session_max_lifetime_minutes = lifetime + return s + + +class Test_policy_id_from_href: + def test_returns_trailing_segment(self): + href = "https://acme.okta.com/api/v1/policies/rst123" + assert _policy_id_from_href(href) == "rst123" + + def test_strips_trailing_slash(self): + assert ( + _policy_id_from_href("https://acme.okta.com/api/v1/policies/rst123/") + == "rst123" + ) + + def test_handles_relative_path(self): + assert _policy_id_from_href("/api/v1/policies/rst-abc") == "rst-abc" + + def test_none_returns_none(self): + assert _policy_id_from_href(None) is None + + def test_empty_returns_none(self): + assert _policy_id_from_href("") is None + + +def _patch_sdk(**methods): + """Returns a context manager that patches OktaSDKClient with the given async methods.""" + return mock.patch( + "prowler.providers.okta.lib.service.service.OktaSDKClient", + return_value=mock.MagicMock(**methods), + ) + + +class Test_Application_service: + def test_fetches_admin_console_settings_and_built_in_apps(self): + provider = set_mocked_okta_provider() + + admin_console_app = _fake_app( + "0oaadminconsole", + "saasure", + access_policy_href="https://acme.okta.com/api/v1/policies/rstadminconsole", + label="Okta Admin Console", + ) + dashboard_app = _fake_app( + "0oadashboard", + "okta_enduser", + access_policy_href="https://acme.okta.com/api/v1/policies/rstdashboard", + label="Okta Dashboard", + ) + + async def fake_get_first_party(_app_name): + return ( + _fake_admin_console_settings(idle=15, lifetime=720), + _resp({}), + None, + ) + + async def fake_list_applications(*_a, **kwargs): + name_filter = kwargs.get("filter", "") + if "saasure" in name_filter: + return ([admin_console_app], _resp({}), None) + if "okta_enduser" in name_filter: + return ([dashboard_app], _resp({}), None) + return ([], _resp({}), None) + + async def fake_list_policy_rules(_policy_id, **_k): + rule = _fake_rule(name="Top", priority=1, factor_mode="2FA") + return ([rule], _resp({}), None) + + with _patch_sdk( + get_first_party_app_settings=fake_get_first_party, + list_applications=fake_list_applications, + list_policy_rules=fake_list_policy_rules, + ): + service = Application(provider) + + assert service.admin_console_app_settings.session_idle_timeout_minutes == 15 + assert service.admin_console_app_settings.session_max_lifetime_minutes == 720 + assert set(service.built_in_apps.keys()) == {"saasure", "okta_enduser"} + admin = service.built_in_apps["saasure"] + assert isinstance(admin, OktaBuiltInApp) + assert admin.access_policy_id == "rstadminconsole" + assert isinstance(admin.access_policy, AuthenticationPolicy) + assert admin.access_policy.rules[0].factor_mode == "2FA" + + def test_missing_admin_console_settings_endpoint_returns_none(self): + provider = set_mocked_okta_provider() + + async def failing_settings(_app_name): + return (None, _resp({}), Exception("404 Not Found")) + + async def fake_list_applications(*_a, **_k): + return ([], _resp({}), None) + + with _patch_sdk( + get_first_party_app_settings=failing_settings, + list_applications=fake_list_applications, + list_policy_rules=mock.AsyncMock(), + ): + service = Application(provider) + + assert service.admin_console_app_settings is None + assert service.built_in_apps == {} + + def test_built_in_app_without_access_policy_link(self): + provider = set_mocked_okta_provider() + admin_console_app = _fake_app( + "0oaadminconsole", + "saasure", + access_policy_href=None, + label="Okta Admin Console", + ) + + async def fake_settings(_app_name): + return (_fake_admin_console_settings(), _resp({}), None) + + async def fake_list_applications(*_a, **kwargs): + if "saasure" in kwargs.get("filter", ""): + return ([admin_console_app], _resp({}), None) + return ([], _resp({}), None) + + async def fake_list_policy_rules(*_a, **_k): + return ([], _resp({}), None) + + with _patch_sdk( + get_first_party_app_settings=fake_settings, + list_applications=fake_list_applications, + list_policy_rules=fake_list_policy_rules, + ): + service = Application(provider) + + admin = service.built_in_apps["saasure"] + assert admin.access_policy_id is None + assert admin.access_policy is None + + def test_paginates_list_applications_via_link_header(self): + provider = set_mocked_okta_provider() + page1 = _fake_app("0oa-page-1", "saasure") + page2 = _fake_app( + "0oa-page-2", + "saasure", + access_policy_href="https://acme.okta.com/api/v1/policies/rst1", + ) + next_link = '; rel="next"' + + calls = [] + + async def fake_settings(_app_name): + return (_fake_admin_console_settings(), _resp({}), None) + + async def fake_list_applications(*_a, **kwargs): + name_filter = kwargs.get("filter", "") + if "saasure" in name_filter: + if kwargs.get("after") is None: + calls.append("page1") + return ([page1], _resp({"link": next_link}), None) + calls.append("page2") + return ([page2], _resp({}), None) + return ([], _resp({}), None) + + async def fake_list_policy_rules(*_a, **_k): + return ([], _resp({}), None) + + with _patch_sdk( + get_first_party_app_settings=fake_settings, + list_applications=fake_list_applications, + list_policy_rules=fake_list_policy_rules, + ): + service = Application(provider) + + assert calls == ["page1", "page2"] + # Pagination returns both, but we only keep the first match per + # canonical name. Make sure that path doesn't break. + assert "saasure" in service.built_in_apps + + def test_returns_empty_on_apps_api_error(self): + provider = set_mocked_okta_provider() + + async def fake_settings(_app_name): + return (_fake_admin_console_settings(), _resp({}), None) + + async def failing_apps(*_a, **_k): + return ([], _resp({}), Exception("E0000007: scope not found")) + + with _patch_sdk( + get_first_party_app_settings=fake_settings, + list_applications=failing_apps, + list_policy_rules=mock.AsyncMock(), + ): + service = Application(provider) + + assert service.built_in_apps == {} + + def test_skips_fetch_when_apps_scope_missing(self): + identity = OktaIdentityInfo( + org_domain=OKTA_ORG_DOMAIN, + client_id=OKTA_CLIENT_ID, + granted_scopes=["okta.policies.read"], + ) + provider = set_mocked_okta_provider(identity=identity) + + list_apps_called = False + get_settings_called = False + + async def fake_settings(_app_name): + nonlocal get_settings_called + get_settings_called = True + return (_fake_admin_console_settings(), _resp({}), None) + + async def fake_apps(*_a, **_k): + nonlocal list_apps_called + list_apps_called = True + return ([], _resp({}), None) + + with _patch_sdk( + get_first_party_app_settings=fake_settings, + list_applications=fake_apps, + list_policy_rules=mock.AsyncMock(), + ): + service = Application(provider) + + assert list_apps_called is False + assert get_settings_called is False + assert service.built_in_apps == {} + assert service.admin_console_app_settings is None + assert service.missing_scope["admin_console_app_settings"] == "okta.apps.read" + assert service.missing_scope["built_in_apps"] == "okta.apps.read" + assert service.missing_scope["integrated_apps"] == "okta.apps.read" + assert service.missing_scope["access_policies"] is None + + def test_skips_policy_fetch_when_policies_scope_missing(self): + identity = OktaIdentityInfo( + org_domain=OKTA_ORG_DOMAIN, + client_id=OKTA_CLIENT_ID, + granted_scopes=["okta.apps.read"], + ) + provider = set_mocked_okta_provider(identity=identity) + + async def fake_settings(_app_name): + return (_fake_admin_console_settings(), _resp({}), None) + + async def fake_apps(*_a, **_k): + return ([], _resp({}), None) + + with _patch_sdk( + get_first_party_app_settings=fake_settings, + list_applications=fake_apps, + list_policy_rules=mock.AsyncMock(), + ): + service = Application(provider) + + # When only one scope is missing, we still expose + # admin_console_app_settings (uses okta.apps.read which IS granted) + # but skip the joint built_in_apps+policies path. + assert service.admin_console_app_settings is not None + assert service.built_in_apps == {} + assert service.missing_scope["admin_console_app_settings"] is None + assert service.missing_scope["built_in_apps"] is None + assert service.missing_scope["integrated_apps"] is None + assert service.missing_scope["access_policies"] == "okta.policies.read" + + def test_unknown_granted_scopes_falls_back_to_attempting_fetch(self): + identity = OktaIdentityInfo( + org_domain=OKTA_ORG_DOMAIN, + client_id=OKTA_CLIENT_ID, + granted_scopes=[], + ) + provider = set_mocked_okta_provider(identity=identity) + + called = {"settings": False, "apps": False} + + async def fake_settings(_app_name): + called["settings"] = True + return (_fake_admin_console_settings(), _resp({}), None) + + async def fake_apps(*_a, **_k): + called["apps"] = True + return ([], _resp({}), None) + + with _patch_sdk( + get_first_party_app_settings=fake_settings, + list_applications=fake_apps, + list_policy_rules=mock.AsyncMock(), + ): + Application(provider) + + assert called["settings"] is True + assert called["apps"] is True + + def test_phishing_resistant_constraint_picked_up_from_rule(self): + provider = set_mocked_okta_provider() + app = _fake_app( + "0oaadminconsole", + "saasure", + access_policy_href="https://acme.okta.com/api/v1/policies/rst-pr", + ) + + async def fake_settings(_app_name): + return (_fake_admin_console_settings(), _resp({}), None) + + async def fake_apps(*_a, **kwargs): + if "saasure" in kwargs.get("filter", ""): + return ([app], _resp({}), None) + return ([], _resp({}), None) + + async def fake_rules(*_a, **_k): + rule = _fake_rule( + factor_mode="2FA", phishing_resistant="REQUIRED", priority=1 + ) + return ([rule], _resp({}), None) + + with _patch_sdk( + get_first_party_app_settings=fake_settings, + list_applications=fake_apps, + list_policy_rules=fake_rules, + ): + service = Application(provider) + + admin = service.built_in_apps["saasure"] + assert ( + admin.access_policy.rules[0].possession_phishing_resistant_required is True + ) + assert admin.access_policy.rules[0].factor_mode == "2FA" + + def test_network_zone_condition_picked_up_from_rule(self): + provider = set_mocked_okta_provider() + app = _fake_app( + "0oaadminconsole", + "saasure", + access_policy_href="https://acme.okta.com/api/v1/policies/rst-net", + ) + + async def fake_settings(_app_name): + return (_fake_admin_console_settings(), _resp({}), None) + + async def fake_apps(*_a, **kwargs): + if "saasure" in kwargs.get("filter", ""): + return ([app], _resp({}), None) + return ([], _resp({}), None) + + async def fake_rules(*_a, **_k): + rule = _fake_rule( + priority=1, + access="DENY", + network_connection="ZONE", + network_exclude=["zone-blocked"], + ) + return ([rule], _resp({}), None) + + with _patch_sdk( + get_first_party_app_settings=fake_settings, + list_applications=fake_apps, + list_policy_rules=fake_rules, + ): + service = Application(provider) + + admin = service.built_in_apps["saasure"] + assert admin.access_policy.rules[0].access == "DENY" + assert admin.access_policy.rules[0].network_connection == "ZONE" + assert admin.access_policy.rules[0].network_zones_exclude == ["zone-blocked"] + + def test_optional_phishing_resistant_not_treated_as_required(self): + provider = set_mocked_okta_provider() + app = _fake_app( + "0oaadminconsole", + "saasure", + access_policy_href="https://acme.okta.com/api/v1/policies/rst-opt", + ) + + async def fake_settings(_app_name): + return (_fake_admin_console_settings(), _resp({}), None) + + async def fake_apps(*_a, **kwargs): + if "saasure" in kwargs.get("filter", ""): + return ([app], _resp({}), None) + return ([], _resp({}), None) + + async def fake_rules(*_a, **_k): + rule = _fake_rule( + factor_mode="2FA", phishing_resistant="OPTIONAL", priority=1 + ) + return ([rule], _resp({}), None) + + with _patch_sdk( + get_first_party_app_settings=fake_settings, + list_applications=fake_apps, + list_policy_rules=fake_rules, + ): + service = Application(provider) + + admin = service.built_in_apps["saasure"] + assert ( + admin.access_policy.rules[0].possession_phishing_resistant_required is False + ) + + def test_lists_integrated_apps_on_demand(self): + provider = set_mocked_okta_provider() + built_in_admin = _fake_app( + "0oaadminconsole", + "saasure", + access_policy_href="https://acme.okta.com/api/v1/policies/rst-admin", + label="Okta Admin Console", + ) + custom_app = _fake_app( + "0oacustom", + "google_workspace", + access_policy_href="https://acme.okta.com/api/v1/policies/rst-custom", + label="Google Workspace", + ) + next_link = '; rel="next"' + + async def fake_settings(_app_name): + return (_fake_admin_console_settings(), _resp({}), None) + + async def fake_list_applications(*_a, **kwargs): + name_filter = kwargs.get("filter", "") + if name_filter: + if "saasure" in name_filter: + return ([built_in_admin], _resp({}), None) + if "okta_enduser" in name_filter: + return ([], _resp({}), None) + if kwargs.get("after") is None: + return ([built_in_admin], _resp({"link": next_link}), None) + return ([custom_app], _resp({}), None) + + async def fake_list_policy_rules(_policy_id, **_k): + return ( + [_fake_rule(priority=1, network_include=["zone-corp"])], + _resp({}), + None, + ) + + with _patch_sdk( + get_first_party_app_settings=fake_settings, + list_applications=fake_list_applications, + list_policy_rules=fake_list_policy_rules, + ): + service = Application(provider) + apps = service.integrated_apps + + assert set(apps.keys()) == {"0oaadminconsole", "0oacustom"} + assert apps["0oacustom"].label == "Google Workspace" + assert apps["0oacustom"].access_policy_id == "rst-custom" + + +class Test_Application_service_sdk_validation_fallback: + """Verifies the raw-JSON fallback for the Okta SDK enum-validator bug. + + The Okta Management API returns values (e.g. lowercase `"password"` + in `KnowledgeConstraint.types`) that the SDK's pydantic field + validators reject as ValidationError. Without a fallback the entire + policy fetch crashes; with the fallback we evaluate the rules + correctly via raw JSON. + """ + + def _build_service_with_validation_error_then_raw_success( + self, raw_rules_payload, app_filter_match="saasure" + ): + from pydantic import ValidationError + + provider = set_mocked_okta_provider() + admin = _fake_app( + "0oaadminconsole", + "saasure", + access_policy_href="https://acme.okta.com/api/v1/policies/rst-admin", + label="Okta Admin Console", + ) + + async def fake_settings(_app_name): + return (_fake_admin_console_settings(), _resp({}), None) + + async def fake_apps(*_a, **kwargs): + if app_filter_match in (kwargs.get("filter") or ""): + return ([admin], _resp({}), None) + return ([], _resp({}), None) + + async def failing_list_policy_rules(*_a, **_k): + try: + # Trigger a real pydantic ValidationError so we exercise + # the exact exception type the SDK raises in production. + from okta.models.knowledge_constraint import KnowledgeConstraint + + KnowledgeConstraint(types=["password"]) + except ValidationError as ve: + raise ve + return ([], _resp({}), None) + + async def fake_raw_create(*_a, **_k): + return ({"url": "/api/v1/policies/rst-admin/rules"}, None) + + async def fake_raw_execute(_request): + return (None, json.dumps(raw_rules_payload), None) + + sdk_mock = mock.MagicMock() + sdk_mock.get_first_party_app_settings = fake_settings + sdk_mock.list_applications = fake_apps + sdk_mock.list_policy_rules = failing_list_policy_rules + sdk_mock._request_executor.create_request = fake_raw_create + sdk_mock._request_executor.execute = fake_raw_execute + + with mock.patch( + "prowler.providers.okta.lib.service.service.OktaSDKClient", + return_value=sdk_mock, + ): + from prowler.providers.okta.services.application.application_service import ( + Application as _Application, + ) + + return _Application(provider) + + def test_raw_fallback_projects_factor_mode_and_phishing_resistant(self): + rules_payload = [ + { + "id": "rul-1", + "name": "Top Rule", + "priority": 1, + "status": "ACTIVE", + "system": False, + "actions": { + "appSignOn": { + "access": "ALLOW", + "verificationMethod": { + "type": "ASSURANCE", + "factorMode": "2FA", + "constraints": [ + { + "knowledge": {"types": ["password"]}, + "possession": {"phishingResistant": "REQUIRED"}, + } + ], + }, + } + }, + "conditions": { + "network": { + "connection": "ZONE", + "include": ["nzo-corp"], + "exclude": [], + } + }, + } + ] + service = self._build_service_with_validation_error_then_raw_success( + rules_payload + ) + + admin = service.built_in_apps["saasure"] + assert admin.access_policy is not None + assert len(admin.access_policy.rules) == 1 + rule = admin.access_policy.rules[0] + assert rule.factor_mode == "2FA" + assert rule.possession_phishing_resistant_required is True + assert rule.network_connection == "ZONE" + assert rule.network_zones_include == ["nzo-corp"] + assert rule.is_default is False + assert rule.priority == 1 + + def test_raw_fallback_handles_empty_rules(self): + service = self._build_service_with_validation_error_then_raw_success([]) + admin = service.built_in_apps["saasure"] + assert admin.access_policy is not None + assert admin.access_policy.rules == [] + + +class Test_Application_service_per_app_isolation: + """One app's fetch failure must not erase the other app's findings.""" + + def test_dashboard_still_returned_when_admin_console_policy_fetch_fails(self): + provider = set_mocked_okta_provider() + admin = _fake_app( + "0oaadminconsole", + "saasure", + access_policy_href="https://acme.okta.com/api/v1/policies/rst-broken", + label="Okta Admin Console", + ) + dashboard = _fake_app( + "0oadashboard", + "okta_enduser", + access_policy_href="https://acme.okta.com/api/v1/policies/rst-dash", + label="Okta Dashboard", + ) + + async def fake_settings(_app_name): + return (_fake_admin_console_settings(), _resp({}), None) + + async def fake_apps(*_a, **kwargs): + f = kwargs.get("filter") or "" + if "saasure" in f: + return ([admin], _resp({}), None) + if "okta_enduser" in f: + return ([dashboard], _resp({}), None) + return ([], _resp({}), None) + + async def fake_policy_rules(policy_id, **_k): + if policy_id == "rst-broken": + raise RuntimeError("simulated unexpected SDK failure") + return ( + [ + _fake_rule( + name="Top", + priority=1, + factor_mode="2FA", + phishing_resistant="REQUIRED", + ) + ], + _resp({}), + None, + ) + + with _patch_sdk( + get_first_party_app_settings=fake_settings, + list_applications=fake_apps, + list_policy_rules=fake_policy_rules, + ): + service = Application(provider) + + # Admin Console: app captured, access_policy set to None due to + # isolated failure during rule fetch. + admin_model = service.built_in_apps["saasure"] + assert admin_model.access_policy is None + # Dashboard: succeeded — its rule is fully resolved. + dashboard_model = service.built_in_apps["okta_enduser"] + assert dashboard_model.access_policy is not None + assert dashboard_model.access_policy.rules[0].factor_mode == "2FA" + + def test_integrated_apps_one_app_failure_does_not_drop_others(self): + provider = set_mocked_okta_provider() + good = _fake_app( + "0oa-good", + "custom_good", + access_policy_href="https://acme.okta.com/api/v1/policies/rst-good", + label="Good App", + ) + bad = _fake_app( + "0oa-bad", + "custom_bad", + access_policy_href="https://acme.okta.com/api/v1/policies/rst-bad", + label="Bad App", + ) + + async def fake_settings(_): + return (_fake_admin_console_settings(), _resp({}), None) + + async def fake_apps(*_a, **kwargs): + f = kwargs.get("filter") or "" + if f: + return ([], _resp({}), None) + return ([good, bad], _resp({}), None) + + async def fake_policy_rules(policy_id, **_k): + if policy_id == "rst-bad": + raise RuntimeError("simulated failure") + return ( + [_fake_rule(name="Top", priority=1, factor_mode="1FA")], + _resp({}), + None, + ) + + with _patch_sdk( + get_first_party_app_settings=fake_settings, + list_applications=fake_apps, + list_policy_rules=fake_policy_rules, + ): + service = Application(provider) + apps = service.integrated_apps + + assert set(apps.keys()) == {"0oa-good", "0oa-bad"} + assert apps["0oa-good"].access_policy is not None + assert apps["0oa-bad"].access_policy is None diff --git a/tests/providers/okta/services/signon/signon_dod_warning_banner_configured/signon_dod_warning_banner_configured_test.py b/tests/providers/okta/services/signon/signon_dod_warning_banner_configured/signon_dod_warning_banner_configured_test.py new file mode 100644 index 0000000000..96406a5fd1 --- /dev/null +++ b/tests/providers/okta/services/signon/signon_dod_warning_banner_configured/signon_dod_warning_banner_configured_test.py @@ -0,0 +1,257 @@ +from unittest import mock + +from tests.providers.okta.okta_fixtures import set_mocked_okta_provider +from tests.providers.okta.services.signon.signon_fixtures import ( + DOD_BANNER_HTML_SNIPPET, + build_signon_client, + sign_in_page, +) + +CHECK_PATH = ( + "prowler.providers.okta.services.signon." + "signon_dod_warning_banner_configured." + "signon_dod_warning_banner_configured.signon_client" +) + + +class Test_signon_dod_warning_banner_configured: + def test_manual_when_no_brands_detected(self): + signon_client = build_signon_client(sign_in_pages={}) + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_okta_provider(), + ), + mock.patch(CHECK_PATH, new=signon_client), + ): + from prowler.providers.okta.services.signon.signon_dod_warning_banner_configured.signon_dod_warning_banner_configured import ( + signon_dod_warning_banner_configured, + ) + + findings = signon_dod_warning_banner_configured().execute() + assert len(findings) == 1 + assert findings[0].status == "MANUAL" + assert "No Okta brands were retrieved" in findings[0].status_extended + + def test_missing_brand_scope_returns_manual_finding_naming_the_scope(self): + signon_client = build_signon_client( + missing_scope={ + "global_session_policies": None, + "sign_in_pages": "okta.brands.read", + } + ) + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_okta_provider(), + ), + mock.patch(CHECK_PATH, new=signon_client), + ): + from prowler.providers.okta.services.signon.signon_dod_warning_banner_configured.signon_dod_warning_banner_configured import ( + signon_dod_warning_banner_configured, + ) + + findings = signon_dod_warning_banner_configured().execute() + assert len(findings) == 1 + assert findings[0].status == "MANUAL" + assert "okta.brands.read" in findings[0].status_extended + assert "missing the required" in findings[0].status_extended + + def test_pass_when_customized_page_contains_banner(self): + page = sign_in_page( + brand_id="brand-1", + brand_name="Primary", + is_customized=True, + page_content=f"{DOD_BANNER_HTML_SNIPPET}", + ) + signon_client = build_signon_client(sign_in_pages={"brand-1": page}) + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_okta_provider(), + ), + mock.patch(CHECK_PATH, new=signon_client), + ): + from prowler.providers.okta.services.signon.signon_dod_warning_banner_configured.signon_dod_warning_banner_configured import ( + signon_dod_warning_banner_configured, + ) + + findings = signon_dod_warning_banner_configured().execute() + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "DOD Notice and Consent Banner detected" in ( + findings[0].status_extended + ) + assert "customized sign-in page" in findings[0].status_extended + + def test_fail_when_customized_page_missing_banner(self): + page = sign_in_page( + brand_id="brand-1", + brand_name="Primary", + is_customized=True, + page_content="

    Welcome to ACME

    ", + ) + signon_client = build_signon_client(sign_in_pages={"brand-1": page}) + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_okta_provider(), + ), + mock.patch(CHECK_PATH, new=signon_client), + ): + from prowler.providers.okta.services.signon.signon_dod_warning_banner_configured.signon_dod_warning_banner_configured import ( + signon_dod_warning_banner_configured, + ) + + findings = signon_dod_warning_banner_configured().execute() + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "does not contain" in findings[0].status_extended + + def test_pass_when_default_page_contains_banner(self): + page = sign_in_page( + brand_id="brand-1", + brand_name="Primary", + is_customized=False, + page_content=f"{DOD_BANNER_HTML_SNIPPET}", + ) + signon_client = build_signon_client(sign_in_pages={"brand-1": page}) + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_okta_provider(), + ), + mock.patch(CHECK_PATH, new=signon_client), + ): + from prowler.providers.okta.services.signon.signon_dod_warning_banner_configured.signon_dod_warning_banner_configured import ( + signon_dod_warning_banner_configured, + ) + + findings = signon_dod_warning_banner_configured().execute() + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "default sign-in page" in findings[0].status_extended + + def test_manual_when_page_content_missing(self): + page = sign_in_page( + brand_id="brand-1", + brand_name="Primary", + is_customized=False, + page_content=None, + ) + signon_client = build_signon_client(sign_in_pages={"brand-1": page}) + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_okta_provider(), + ), + mock.patch(CHECK_PATH, new=signon_client), + ): + from prowler.providers.okta.services.signon.signon_dod_warning_banner_configured.signon_dod_warning_banner_configured import ( + signon_dod_warning_banner_configured, + ) + + findings = signon_dod_warning_banner_configured().execute() + assert len(findings) == 1 + assert findings[0].status == "MANUAL" + assert "could not be retrieved from the Okta API" in ( + findings[0].status_extended + ) + + def test_manual_when_fetch_error(self): + page = sign_in_page( + brand_id="brand-1", + brand_name="Primary", + is_customized=False, + fetch_error="403 Forbidden: invalid_scope", + ) + signon_client = build_signon_client(sign_in_pages={"brand-1": page}) + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_okta_provider(), + ), + mock.patch(CHECK_PATH, new=signon_client), + ): + from prowler.providers.okta.services.signon.signon_dod_warning_banner_configured.signon_dod_warning_banner_configured import ( + signon_dod_warning_banner_configured, + ) + + findings = signon_dod_warning_banner_configured().execute() + assert len(findings) == 1 + assert findings[0].status == "MANUAL" + assert "Could not retrieve" in findings[0].status_extended + assert "403" in findings[0].status_extended + + def test_fail_when_only_partial_banner_markers_are_present(self): + page = sign_in_page( + brand_id="brand-1", + brand_name="Primary", + is_customized=True, + page_content=( + "This U.S. Government portal is for authorized use " + "only." + ), + ) + signon_client = build_signon_client(sign_in_pages={"brand-1": page}) + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_okta_provider(), + ), + mock.patch(CHECK_PATH, new=signon_client), + ): + from prowler.providers.okta.services.signon.signon_dod_warning_banner_configured.signon_dod_warning_banner_configured import ( + signon_dod_warning_banner_configured, + ) + + findings = signon_dod_warning_banner_configured().execute() + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "does not contain" in findings[0].status_extended + + def test_emits_one_finding_per_brand(self): + compliant = sign_in_page( + brand_id="brand-prod", + brand_name="Prod", + is_customized=True, + page_content=f"{DOD_BANNER_HTML_SNIPPET}", + ) + missing = sign_in_page( + brand_id="brand-sandbox", + brand_name="Sandbox", + is_customized=True, + page_content="No banner here", + ) + no_custom = sign_in_page( + brand_id="brand-legacy", + brand_name="Legacy", + is_customized=False, + page_content=f"{DOD_BANNER_HTML_SNIPPET}", + ) + signon_client = build_signon_client( + sign_in_pages={ + "brand-prod": compliant, + "brand-sandbox": missing, + "brand-legacy": no_custom, + } + ) + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_okta_provider(), + ), + mock.patch(CHECK_PATH, new=signon_client), + ): + from prowler.providers.okta.services.signon.signon_dod_warning_banner_configured.signon_dod_warning_banner_configured import ( + signon_dod_warning_banner_configured, + ) + + findings = signon_dod_warning_banner_configured().execute() + assert len(findings) == 3 + by_brand = {f.resource_id: f.status for f in findings} + assert by_brand == { + "brand-prod": "PASS", + "brand-sandbox": "FAIL", + "brand-legacy": "PASS", + } diff --git a/tests/providers/okta/services/signon/signon_fixtures.py b/tests/providers/okta/services/signon/signon_fixtures.py new file mode 100644 index 0000000000..c2188670bc --- /dev/null +++ b/tests/providers/okta/services/signon/signon_fixtures.py @@ -0,0 +1,130 @@ +"""Shared helpers for `signon` service check tests. + +The original idle-timeout check test file defined these helpers locally; +they were extracted here so the four checks added on top of the same +service (`signon_global_session_lifetime_18h`, +`signon_global_session_cookies_not_persistent`, +`signon_global_session_policy_network_zone_enforced`, +`signon_dod_warning_banner_configured`) can reuse them without copy-paste. +""" + +from unittest import mock + +from prowler.providers.okta.services.signon.signon_service import ( + GlobalSessionPolicy, + GlobalSessionPolicyRule, + SignInPage, +) +from tests.providers.okta.okta_fixtures import set_mocked_okta_provider + + +def build_signon_client( + policies: dict = None, + audit_config: dict = None, + sign_in_pages: dict = None, + missing_scope: dict = None, +): + client = mock.MagicMock() + client.global_session_policies = policies or {} + client.provider = set_mocked_okta_provider() + client.audit_config = audit_config or {} + client.sign_in_pages = sign_in_pages or {} + # Default to "all scopes granted" so existing tests keep working. + client.missing_scope = missing_scope or { + "global_session_policies": None, + "sign_in_pages": None, + } + return client + + +def default_policy(rules): + return GlobalSessionPolicy( + id="pol-default", + name="Default Policy", + priority=99, + status="ACTIVE", + is_default=True, + rules=rules, + ) + + +def custom_policy(rules, name: str = "Admins Policy"): + return GlobalSessionPolicy( + id="pol-custom", + name=name, + priority=1, + status="ACTIVE", + is_default=False, + rules=rules, + ) + + +def default_rule( + idle_min: int = 480, + lifetime_min: int = None, + use_persistent_cookie: bool = None, + priority: int = 2, + status: str = "ACTIVE", +): + return GlobalSessionPolicyRule( + id="rule-default", + name="Default Rule", + priority=priority, + status=status, + is_default=True, + max_session_idle_minutes=idle_min, + max_session_lifetime_minutes=lifetime_min, + use_persistent_cookie=use_persistent_cookie, + ) + + +def non_default_rule( + name: str, + *, + idle_min: int = None, + lifetime_min: int = None, + use_persistent_cookie: bool = None, + network_zones_include: list = None, + network_zones_exclude: list = None, + priority: int = 1, + status: str = "ACTIVE", +): + return GlobalSessionPolicyRule( + id=f"rule-{name.lower().replace(' ', '-')}", + name=name, + priority=priority, + status=status, + is_default=False, + max_session_idle_minutes=idle_min, + max_session_lifetime_minutes=lifetime_min, + use_persistent_cookie=use_persistent_cookie, + network_zones_include=network_zones_include or [], + network_zones_exclude=network_zones_exclude or [], + ) + + +def sign_in_page( + brand_id: str = "brand-1", + brand_name: str = "Default Brand", + is_customized: bool = True, + page_content: str = None, + fetch_error: str = None, +): + return SignInPage( + brand_id=brand_id, + brand_name=brand_name, + is_customized=is_customized, + page_content=page_content, + fetch_error=fetch_error, + ) + + +# Condensed DTM-08-060 banner that covers all four marker groups the check +# requires (see BANNER_MARKER_GROUPS in the check module). Lets PASS tests +# avoid pasting the full ~1300-char banner verbatim. +DOD_BANNER_HTML_SNIPPET = ( + "
    You are accessing a U.S. Government (USG) Information System " + "(IS) that is provided for USG-authorized use only. " + "Communications using, or data stored on, this IS may be intercepted, " + "searched, monitored, and recorded.
    " +) diff --git a/tests/providers/okta/services/signon/signon_global_session_cookies_not_persistent/signon_global_session_cookies_not_persistent_test.py b/tests/providers/okta/services/signon/signon_global_session_cookies_not_persistent/signon_global_session_cookies_not_persistent_test.py new file mode 100644 index 0000000000..cd10d84be8 --- /dev/null +++ b/tests/providers/okta/services/signon/signon_global_session_cookies_not_persistent/signon_global_session_cookies_not_persistent_test.py @@ -0,0 +1,189 @@ +from unittest import mock + +from prowler.providers.okta.services.signon.signon_service import ( + GlobalSessionPolicy, + GlobalSessionPolicyRule, +) +from tests.providers.okta.okta_fixtures import set_mocked_okta_provider +from tests.providers.okta.services.signon.signon_fixtures import ( + build_signon_client, + custom_policy, + default_policy, + default_rule, + non_default_rule, +) + +CHECK_PATH = ( + "prowler.providers.okta.services.signon." + "signon_global_session_cookies_not_persistent." + "signon_global_session_cookies_not_persistent.signon_client" +) + + +def _run_check(signon_client): + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_okta_provider(), + ), + mock.patch(CHECK_PATH, new=signon_client), + ): + from prowler.providers.okta.services.signon.signon_global_session_cookies_not_persistent.signon_global_session_cookies_not_persistent import ( + signon_global_session_cookies_not_persistent, + ) + + return signon_global_session_cookies_not_persistent().execute() + + +class Test_signon_global_session_cookies_not_persistent: + def test_no_policies(self): + findings = _run_check(build_signon_client({})) + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "No active Okta Global Session Policies" in findings[0].status_extended + + def test_pass_when_priority_one_rule_disables_persistent_cookies(self): + policy = default_policy( + [ + non_default_rule( + "Non-persistent cookies", + use_persistent_cookie=False, + priority=1, + ), + default_rule(priority=2), + ] + ) + findings = _run_check(build_signon_client({"pol-default": policy})) + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "disables persistent global session cookies" in ( + findings[0].status_extended + ) + assert "priority 99, default" in findings[0].status_extended + + def test_fail_when_priority_one_rule_uses_persistent_cookies(self): + policy = default_policy( + [ + non_default_rule( + "Persistent cookies enabled", + use_persistent_cookie=True, + priority=1, + ), + default_rule(priority=2), + ] + ) + findings = _run_check(build_signon_client({"pol-default": policy})) + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "allows persistent global session cookies" in ( + findings[0].status_extended + ) + + def test_fail_when_priority_one_rule_does_not_assert_setting(self): + policy = default_policy( + [ + GlobalSessionPolicyRule( + id="rule-no-session", + name="No Session Block", + priority=1, + status="ACTIVE", + is_default=False, + use_persistent_cookie=None, + ), + default_rule(priority=2), + ] + ) + findings = _run_check(build_signon_client({"pol-default": policy})) + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "does not assert" in findings[0].status_extended + + def test_emits_one_finding_per_policy(self): + admins_policy = custom_policy( + [ + non_default_rule( + "Sticky admin", + use_persistent_cookie=True, + priority=1, + ) + ], + name="Admins Policy", + ) + strict_default = default_policy( + [ + non_default_rule( + "Non-persistent", + use_persistent_cookie=False, + priority=1, + ), + default_rule(priority=2), + ] + ) + findings = _run_check( + build_signon_client( + {"pol-custom": admins_policy, "pol-default": strict_default} + ) + ) + assert len(findings) == 2 + by_name = {f.resource_name: f for f in findings} + assert by_name["Admins Policy"].status == "FAIL" + assert "priority 1, custom" in by_name["Admins Policy"].status_extended + assert by_name["Default Policy"].status == "PASS" + + def test_inactive_policy_is_skipped(self): + inactive = GlobalSessionPolicy( + id="pol-inactive", + name="Disabled Policy", + priority=1, + status="INACTIVE", + is_default=False, + rules=[non_default_rule("Sticky", use_persistent_cookie=True, priority=1)], + ) + active_default = default_policy( + [ + non_default_rule( + "Non-persistent", + use_persistent_cookie=False, + priority=1, + ), + default_rule(priority=2), + ] + ) + findings = _run_check( + build_signon_client( + {"pol-inactive": inactive, "pol-default": active_default} + ) + ) + assert len(findings) == 1 + assert findings[0].resource_name == "Default Policy" + assert findings[0].status == "PASS" + + def test_missing_scope_returns_manual_finding_naming_the_scope(self): + findings = _run_check( + build_signon_client( + missing_scope={ + "global_session_policies": "okta.policies.read", + "sign_in_pages": None, + } + ) + ) + assert len(findings) == 1 + assert findings[0].status == "MANUAL" + assert "okta.policies.read" in findings[0].status_extended + assert "missing the required" in findings[0].status_extended + + def test_fail_when_all_policies_inactive(self): + only_inactive = GlobalSessionPolicy( + id="pol-default", + name="Default Policy", + priority=99, + status="INACTIVE", + is_default=True, + rules=[ + non_default_rule("Compliant", use_persistent_cookie=False, priority=1) + ], + ) + findings = _run_check(build_signon_client({"pol-default": only_inactive})) + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "No active Okta Global Session Policies" in findings[0].status_extended diff --git a/tests/providers/okta/services/signon/signon_global_session_idle_timeout_15min/signon_global_session_idle_timeout_15min_test.py b/tests/providers/okta/services/signon/signon_global_session_idle_timeout_15min/signon_global_session_idle_timeout_15min_test.py new file mode 100644 index 0000000000..177a4b1c8f --- /dev/null +++ b/tests/providers/okta/services/signon/signon_global_session_idle_timeout_15min/signon_global_session_idle_timeout_15min_test.py @@ -0,0 +1,210 @@ +from unittest import mock + +from prowler.providers.okta.services.signon.signon_service import ( + GlobalSessionPolicy, + GlobalSessionPolicyRule, +) +from tests.providers.okta.okta_fixtures import set_mocked_okta_provider +from tests.providers.okta.services.signon.signon_fixtures import ( + build_signon_client, + custom_policy, + default_policy, + default_rule, + non_default_rule, +) + +CHECK_PATH = ( + "prowler.providers.okta.services.signon." + "signon_global_session_idle_timeout_15min." + "signon_global_session_idle_timeout_15min.signon_client" +) + + +def _run_check(signon_client): + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_okta_provider(), + ), + mock.patch(CHECK_PATH, new=signon_client), + ): + from prowler.providers.okta.services.signon.signon_global_session_idle_timeout_15min.signon_global_session_idle_timeout_15min import ( + signon_global_session_idle_timeout_15min, + ) + + return signon_global_session_idle_timeout_15min().execute() + + +class Test_signon_global_session_idle_timeout_15min: + def test_no_policies(self): + findings = _run_check(build_signon_client({})) + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "No active Okta Global Session Policies" in findings[0].status_extended + + def test_pass_when_priority_one_non_default_rule_is_compliant(self): + policy = default_policy( + [ + non_default_rule("Strict 15min", idle_min=15, priority=1), + default_rule(priority=2), + ] + ) + findings = _run_check(build_signon_client({"pol-default": policy})) + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "Strict 15min" in findings[0].status_extended + assert "Default Policy" in findings[0].status_extended + assert "priority 99, default" in findings[0].status_extended + + def test_fail_when_only_default_rule(self): + policy = default_policy([default_rule(priority=1)]) + findings = _run_check(build_signon_client({"pol-default": policy})) + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "uses 'Default Rule' as its active Priority 1 rule" in ( + findings[0].status_extended + ) + + def test_fail_when_priority_one_non_default_rule_has_null_idle(self): + policy = default_policy( + [ + GlobalSessionPolicyRule( + id="rule-no-session", + name="No Session Block", + priority=1, + status="ACTIVE", + is_default=False, + max_session_idle_minutes=None, + ), + default_rule(priority=2), + ] + ) + findings = _run_check(build_signon_client({"pol-default": policy})) + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "No Session Block" in findings[0].status_extended + assert "does not define" in findings[0].status_extended + + def test_fail_when_priority_one_non_default_rule_exceeds_threshold(self): + policy = default_policy( + [ + non_default_rule("Loose 60min", idle_min=60, priority=1), + default_rule(priority=2), + ] + ) + findings = _run_check(build_signon_client({"pol-default": policy})) + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "Loose 60min" in findings[0].status_extended + assert "exceeding the configured threshold" in findings[0].status_extended + + def test_fail_when_compliant_non_default_rule_is_not_priority_one(self): + policy = default_policy( + [ + default_rule(priority=1), + non_default_rule("Strict 15min", idle_min=15, priority=2), + ] + ) + findings = _run_check(build_signon_client({"pol-default": policy})) + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "uses 'Default Rule' as its active Priority 1 rule" in ( + findings[0].status_extended + ) + + def test_emits_one_finding_per_policy(self): + # Custom policy at priority 1 with a permissive rule + Default Policy + # with a strict rule -> two findings, ordered by policy priority. + admins_policy = custom_policy( + [ + non_default_rule("Admin Loose", idle_min=120, priority=1), + default_rule(priority=2), + ], + name="Admins Policy", + ) + strict_default = default_policy( + [ + non_default_rule("Strict 15min", idle_min=15, priority=1), + default_rule(priority=2), + ] + ) + findings = _run_check( + build_signon_client( + {"pol-custom": admins_policy, "pol-default": strict_default} + ) + ) + assert len(findings) == 2 + by_name = {f.resource_name: f for f in findings} + assert by_name["Admins Policy"].status == "FAIL" + assert "priority 1, custom" in by_name["Admins Policy"].status_extended + assert by_name["Default Policy"].status == "PASS" + assert "priority 99, default" in by_name["Default Policy"].status_extended + + def test_inactive_policy_is_skipped(self): + inactive = GlobalSessionPolicy( + id="pol-inactive", + name="Disabled Policy", + priority=1, + status="INACTIVE", + is_default=False, + rules=[non_default_rule("Loose 120min", idle_min=120, priority=1)], + ) + active_default = default_policy( + [ + non_default_rule("Strict 15min", idle_min=15, priority=1), + default_rule(priority=2), + ] + ) + findings = _run_check( + build_signon_client( + {"pol-inactive": inactive, "pol-default": active_default} + ) + ) + assert len(findings) == 1 + assert findings[0].resource_name == "Default Policy" + assert findings[0].status == "PASS" + + def test_fail_when_all_policies_inactive(self): + only_inactive = GlobalSessionPolicy( + id="pol-default", + name="Default Policy", + priority=99, + status="INACTIVE", + is_default=True, + rules=[non_default_rule("Strict 15min", idle_min=15, priority=1)], + ) + findings = _run_check(build_signon_client({"pol-default": only_inactive})) + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "No active Okta Global Session Policies" in findings[0].status_extended + + def test_missing_scope_returns_manual_finding_naming_the_scope(self): + findings = _run_check( + build_signon_client( + missing_scope={ + "global_session_policies": "okta.policies.read", + "sign_in_pages": None, + } + ) + ) + assert len(findings) == 1 + assert findings[0].status == "MANUAL" + assert "okta.policies.read" in findings[0].status_extended + assert "missing the required" in findings[0].status_extended + + def test_threshold_overridden_via_audit_config(self): + policy = default_policy( + [ + non_default_rule("Relaxed 30min", idle_min=30, priority=1), + default_rule(priority=2), + ] + ) + findings = _run_check( + build_signon_client( + {"pol-default": policy}, + audit_config={"okta_max_session_idle_minutes": 60}, + ) + ) + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "threshold of 60 minutes" in findings[0].status_extended diff --git a/tests/providers/okta/services/signon/signon_global_session_lifetime_18h/signon_global_session_lifetime_18h_test.py b/tests/providers/okta/services/signon/signon_global_session_lifetime_18h/signon_global_session_lifetime_18h_test.py new file mode 100644 index 0000000000..1a22c20573 --- /dev/null +++ b/tests/providers/okta/services/signon/signon_global_session_lifetime_18h/signon_global_session_lifetime_18h_test.py @@ -0,0 +1,212 @@ +from unittest import mock + +from prowler.providers.okta.services.signon.signon_service import ( + GlobalSessionPolicy, + GlobalSessionPolicyRule, +) +from tests.providers.okta.okta_fixtures import set_mocked_okta_provider +from tests.providers.okta.services.signon.signon_fixtures import ( + build_signon_client, + custom_policy, + default_policy, + default_rule, + non_default_rule, +) + +CHECK_PATH = ( + "prowler.providers.okta.services.signon." + "signon_global_session_lifetime_18h." + "signon_global_session_lifetime_18h.signon_client" +) + + +def _run_check(signon_client): + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_okta_provider(), + ), + mock.patch(CHECK_PATH, new=signon_client), + ): + from prowler.providers.okta.services.signon.signon_global_session_lifetime_18h.signon_global_session_lifetime_18h import ( + signon_global_session_lifetime_18h, + ) + + return signon_global_session_lifetime_18h().execute() + + +class Test_signon_global_session_lifetime_18h: + def test_no_policies(self): + findings = _run_check(build_signon_client({})) + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "No active Okta Global Session Policies" in findings[0].status_extended + + def test_pass_when_priority_one_non_default_rule_is_compliant(self): + policy = default_policy( + [ + non_default_rule("18h rule", lifetime_min=1080, priority=1), + default_rule(priority=2), + ] + ) + findings = _run_check(build_signon_client({"pol-default": policy})) + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "18h rule" in findings[0].status_extended + assert "1080 minutes" in findings[0].status_extended + assert "priority 99, default" in findings[0].status_extended + + def test_fail_when_lifetime_exceeds_threshold(self): + policy = default_policy( + [ + non_default_rule("Loose 24h rule", lifetime_min=1440, priority=1), + default_rule(priority=2), + ] + ) + findings = _run_check(build_signon_client({"pol-default": policy})) + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "1440 minutes" in findings[0].status_extended + assert "exceeding the configured threshold" in findings[0].status_extended + + def test_fail_when_priority_one_rule_has_no_lifetime(self): + policy = default_policy( + [ + GlobalSessionPolicyRule( + id="rule-no-session", + name="No Session Block", + priority=1, + status="ACTIVE", + is_default=False, + max_session_lifetime_minutes=None, + ), + default_rule(priority=2), + ] + ) + findings = _run_check(build_signon_client({"pol-default": policy})) + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "does not define" in findings[0].status_extended + + def test_fail_when_lifetime_is_disabled_with_zero(self): + policy = default_policy( + [ + non_default_rule("Unlimited Lifetime", lifetime_min=0, priority=1), + default_rule(priority=2), + ] + ) + findings = _run_check(build_signon_client({"pol-default": policy})) + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "0 minutes" in findings[0].status_extended + assert "disables the maximum Okta global session lifetime" in ( + findings[0].status_extended + ) + + def test_fail_when_default_rule_is_priority_one(self): + policy = default_policy( + [ + default_rule(priority=1, lifetime_min=1080), + non_default_rule("Compliant", lifetime_min=1080, priority=2), + ] + ) + findings = _run_check(build_signon_client({"pol-default": policy})) + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "uses 'Default Rule' as its active Priority 1 rule" in ( + findings[0].status_extended + ) + + def test_emits_one_finding_per_policy(self): + admins_policy = custom_policy( + [ + non_default_rule("Admin Long Lived", lifetime_min=2880, priority=1), + default_rule(priority=2), + ], + name="Admins Policy", + ) + strict_default = default_policy( + [ + non_default_rule("18h rule", lifetime_min=1080, priority=1), + default_rule(priority=2), + ] + ) + findings = _run_check( + build_signon_client( + {"pol-custom": admins_policy, "pol-default": strict_default} + ) + ) + assert len(findings) == 2 + by_name = {f.resource_name: f for f in findings} + assert by_name["Admins Policy"].status == "FAIL" + assert "priority 1, custom" in by_name["Admins Policy"].status_extended + assert by_name["Default Policy"].status == "PASS" + + def test_inactive_policy_is_skipped(self): + inactive = GlobalSessionPolicy( + id="pol-inactive", + name="Disabled Policy", + priority=1, + status="INACTIVE", + is_default=False, + rules=[non_default_rule("Loose", lifetime_min=2880, priority=1)], + ) + active_default = default_policy( + [ + non_default_rule("18h rule", lifetime_min=1080, priority=1), + default_rule(priority=2), + ] + ) + findings = _run_check( + build_signon_client( + {"pol-inactive": inactive, "pol-default": active_default} + ) + ) + assert len(findings) == 1 + assert findings[0].resource_name == "Default Policy" + assert findings[0].status == "PASS" + + def test_fail_when_all_policies_inactive(self): + only_inactive = GlobalSessionPolicy( + id="pol-default", + name="Default Policy", + priority=99, + status="INACTIVE", + is_default=True, + rules=[non_default_rule("18h rule", lifetime_min=1080, priority=1)], + ) + findings = _run_check(build_signon_client({"pol-default": only_inactive})) + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "No active Okta Global Session Policies" in findings[0].status_extended + + def test_missing_scope_returns_manual_finding_naming_the_scope(self): + findings = _run_check( + build_signon_client( + missing_scope={ + "global_session_policies": "okta.policies.read", + "sign_in_pages": None, + } + ) + ) + assert len(findings) == 1 + assert findings[0].status == "MANUAL" + assert "okta.policies.read" in findings[0].status_extended + assert "missing the required" in findings[0].status_extended + + def test_threshold_overridden_via_audit_config(self): + policy = default_policy( + [ + non_default_rule("Relaxed 24h", lifetime_min=1440, priority=1), + default_rule(priority=2), + ] + ) + findings = _run_check( + build_signon_client( + {"pol-default": policy}, + audit_config={"okta_max_session_lifetime_minutes": 1440}, + ) + ) + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "threshold of 1440 minutes" in findings[0].status_extended diff --git a/tests/providers/okta/services/signon/signon_global_session_policy_network_zone_enforced/signon_global_session_policy_network_zone_enforced_test.py b/tests/providers/okta/services/signon/signon_global_session_policy_network_zone_enforced/signon_global_session_policy_network_zone_enforced_test.py new file mode 100644 index 0000000000..1d3067a257 --- /dev/null +++ b/tests/providers/okta/services/signon/signon_global_session_policy_network_zone_enforced/signon_global_session_policy_network_zone_enforced_test.py @@ -0,0 +1,222 @@ +from unittest import mock + +from prowler.providers.okta.services.signon.signon_service import ( + GlobalSessionPolicy, + GlobalSessionPolicyRule, +) +from tests.providers.okta.okta_fixtures import set_mocked_okta_provider +from tests.providers.okta.services.signon.signon_fixtures import ( + build_signon_client, + custom_policy, + default_policy, + default_rule, + non_default_rule, +) + +CHECK_PATH = ( + "prowler.providers.okta.services.signon." + "signon_global_session_policy_network_zone_enforced." + "signon_global_session_policy_network_zone_enforced.signon_client" +) + + +def _run_check(signon_client): + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_okta_provider(), + ), + mock.patch(CHECK_PATH, new=signon_client), + ): + from prowler.providers.okta.services.signon.signon_global_session_policy_network_zone_enforced.signon_global_session_policy_network_zone_enforced import ( + signon_global_session_policy_network_zone_enforced, + ) + + return signon_global_session_policy_network_zone_enforced().execute() + + +class Test_signon_global_session_policy_network_zone_enforced: + def test_no_policies(self): + findings = _run_check(build_signon_client({})) + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "No active Okta Global Session Policies" in findings[0].status_extended + + def test_pass_when_priority_one_non_default_rule_includes_zone(self): + policy = default_policy( + [ + non_default_rule( + "Allow-from-VPN", + network_zones_include=["zone-corp"], + priority=1, + ), + default_rule(priority=2), + ] + ) + findings = _run_check(build_signon_client({"pol-default": policy})) + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "Allow-from-VPN" in findings[0].status_extended + assert "non-default rule" in findings[0].status_extended + assert "priority 99, default" in findings[0].status_extended + + def test_pass_when_priority_one_non_default_rule_excludes_zone(self): + policy = default_policy( + [ + non_default_rule( + "Block-blacklist", + network_zones_exclude=["zone-blocked"], + priority=1, + ), + ] + ) + findings = _run_check(build_signon_client({"pol-default": policy})) + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "Block-blacklist" in findings[0].status_extended + + def test_pass_when_only_default_rule_has_zones(self): + policy = default_policy( + [ + GlobalSessionPolicyRule( + id="rule-default-zoned", + name="Default Rule", + priority=1, + status="ACTIVE", + is_default=True, + network_zones_include=["zone-corp"], + ), + ] + ) + findings = _run_check(build_signon_client({"pol-default": policy})) + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "built-in Default Rule" in findings[0].status_extended + + def test_fail_when_priority_one_rule_has_no_zones(self): + policy = default_policy( + [ + non_default_rule("Plain non-default", priority=1), + default_rule(priority=2), + ] + ) + findings = _run_check(build_signon_client({"pol-default": policy})) + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "Plain non-default" in findings[0].status_extended + assert "does not map" in findings[0].status_extended + + def test_fail_when_only_lower_priority_rule_has_zones(self): + policy = default_policy( + [ + non_default_rule("No-zones top", priority=1), + non_default_rule( + "Zoned-but-low", + network_zones_include=["zone-corp"], + priority=2, + ), + default_rule(priority=3), + ] + ) + findings = _run_check(build_signon_client({"pol-default": policy})) + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "No-zones top" in findings[0].status_extended + + def test_fail_when_only_default_rule_has_no_zones(self): + policy = default_policy([default_rule(priority=1)]) + findings = _run_check(build_signon_client({"pol-default": policy})) + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "built-in Default Rule" in findings[0].status_extended + + def test_emits_one_finding_per_policy(self): + admins_policy = custom_policy( + [ + non_default_rule("No-zones admin", priority=1), + default_rule(priority=2), + ], + name="Admins Policy", + ) + zoned_default = default_policy( + [ + non_default_rule( + "Allow-corp", + network_zones_include=["zone-corp"], + priority=1, + ), + default_rule(priority=2), + ] + ) + findings = _run_check( + build_signon_client( + {"pol-custom": admins_policy, "pol-default": zoned_default} + ) + ) + assert len(findings) == 2 + by_name = {f.resource_name: f for f in findings} + assert by_name["Admins Policy"].status == "FAIL" + assert "priority 1, custom" in by_name["Admins Policy"].status_extended + assert by_name["Default Policy"].status == "PASS" + + def test_inactive_policy_is_skipped(self): + inactive = GlobalSessionPolicy( + id="pol-inactive", + name="Disabled Policy", + priority=1, + status="INACTIVE", + is_default=False, + rules=[non_default_rule("No-zones", priority=1)], + ) + active_default = default_policy( + [ + non_default_rule( + "Allow-corp", + network_zones_include=["zone-corp"], + priority=1, + ), + default_rule(priority=2), + ] + ) + findings = _run_check( + build_signon_client( + {"pol-inactive": inactive, "pol-default": active_default} + ) + ) + assert len(findings) == 1 + assert findings[0].resource_name == "Default Policy" + assert findings[0].status == "PASS" + + def test_fail_when_all_policies_inactive(self): + only_inactive = GlobalSessionPolicy( + id="pol-default", + name="Default Policy", + priority=99, + status="INACTIVE", + is_default=True, + rules=[ + non_default_rule( + "Allow-corp", + network_zones_include=["zone-corp"], + priority=1, + ) + ], + ) + findings = _run_check(build_signon_client({"pol-default": only_inactive})) + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "No active Okta Global Session Policies" in findings[0].status_extended + + def test_missing_scope_returns_manual_finding_naming_the_scope(self): + findings = _run_check( + build_signon_client( + missing_scope={ + "global_session_policies": "okta.policies.read", + "sign_in_pages": None, + } + ) + ) + assert len(findings) == 1 + assert findings[0].status == "MANUAL" + assert "okta.policies.read" in findings[0].status_extended + assert "missing the required" in findings[0].status_extended diff --git a/tests/providers/okta/services/signon/signon_service_test.py b/tests/providers/okta/services/signon/signon_service_test.py new file mode 100644 index 0000000000..c408bc1221 --- /dev/null +++ b/tests/providers/okta/services/signon/signon_service_test.py @@ -0,0 +1,432 @@ +from unittest import mock + +from prowler.providers.okta.models import OktaIdentityInfo +from prowler.providers.okta.services.signon.signon_service import ( + GlobalSessionPolicy, + GlobalSessionPolicyRule, + SignInPage, + Signon, + _next_after_cursor, +) +from tests.providers.okta.okta_fixtures import ( + OKTA_CLIENT_ID, + OKTA_ORG_DOMAIN, + set_mocked_okta_provider, +) + + +def _fake_policy( + policy_id: str, + name: str, + system: bool = True, + priority: int | None = 1, + status: str = "ACTIVE", +): + p = mock.MagicMock() + p.id = policy_id + p.name = name + p.priority = priority + p.status = status + p.system = system + return p + + +def _fake_rule( + rule_id: str, + name: str, + *, + system: bool = False, + priority: int | None = 1, + status: str = "ACTIVE", + max_session_idle_minutes: int = None, +): + r = mock.MagicMock() + r.id = rule_id + r.name = name + r.priority = priority + r.status = status + r.system = system + r.actions.signon.session.max_session_idle_minutes = max_session_idle_minutes + r.actions.signon.session.max_session_lifetime_minutes = None + r.actions.signon.session.use_persistent_cookie = None + r.conditions.network.include = [] + r.conditions.network.exclude = [] + return r + + +def _fake_brand(brand_id: str, name: str): + b = mock.MagicMock() + b.id = brand_id + b.name = name + return b + + +def _fake_sign_in_page(page_content: str): + p = mock.MagicMock() + p.page_content = page_content + return p + + +def _resp(headers: dict = None): + r = mock.MagicMock() + r.headers = headers or {} + return r + + +async def _empty_brands(*_a, **_k): + return ([], _resp({}), None) + + +class Test_next_after_cursor: + def test_no_resp_returns_none(self): + assert _next_after_cursor(None) is None + + def test_no_link_header_returns_none(self): + assert _next_after_cursor(_resp({})) is None + + def test_extracts_after_param(self): + link = ( + '; rel="self", ' + '; rel="next"' + ) + assert _next_after_cursor(_resp({"link": link})) == "abc123" + + def test_link_without_next_returns_none(self): + link = '; rel="self"' + assert _next_after_cursor(_resp({"link": link})) is None + + +class Test_Signon_service: + def test_fetches_policies_and_rules(self): + provider = set_mocked_okta_provider() + + policy = _fake_policy("pol-default", "Default Policy", system=True) + rule_default = _fake_rule( + "rule-default", "Default Rule", system=True, max_session_idle_minutes=480 + ) + rule_compliant = _fake_rule( + "rule-15", "Strict 15min", system=False, max_session_idle_minutes=15 + ) + + async def fake_list_policies(*_a, **_k): + return ([policy], _resp({}), None) + + async def fake_list_rules(*_a, **_k): + return ([rule_default, rule_compliant], _resp({}), None) + + with mock.patch( + "prowler.providers.okta.lib.service.service.OktaSDKClient" + ) as mocked_client_cls: + mocked = mock.MagicMock() + mocked.list_policies = fake_list_policies + mocked.list_policy_rules = fake_list_rules + mocked.list_brands = _empty_brands + mocked_client_cls.return_value = mocked + + service = Signon(provider) + + assert "pol-default" in service.global_session_policies + policy_obj = service.global_session_policies["pol-default"] + assert isinstance(policy_obj, GlobalSessionPolicy) + assert policy_obj.is_default is True + assert policy_obj.priority == 1 + assert policy_obj.status == "ACTIVE" + assert len(policy_obj.rules) == 2 + rules_by_name = {r.name: r for r in policy_obj.rules} + assert isinstance(rules_by_name["Default Rule"], GlobalSessionPolicyRule) + assert rules_by_name["Default Rule"].is_default is True + assert rules_by_name["Default Rule"].priority == 1 + assert rules_by_name["Default Rule"].status == "ACTIVE" + assert rules_by_name["Strict 15min"].is_default is False + assert rules_by_name["Strict 15min"].max_session_idle_minutes == 15 + + def test_paginates_via_link_header(self): + provider = set_mocked_okta_provider() + + page1_policy = _fake_policy("pol-1", "Default Policy") + page2_policy = _fake_policy("pol-2", "Custom Policy", system=False) + next_link = '; rel="next"' + + calls = [] + + async def fake_list_policies(*_a, **kwargs): + calls.append(kwargs.get("after")) + if kwargs.get("after") is None: + return ([page1_policy], _resp({"link": next_link}), None) + return ([page2_policy], _resp({}), None) + + async def fake_list_rules(*_a, **_k): + return ([], _resp({}), None) + + with mock.patch( + "prowler.providers.okta.lib.service.service.OktaSDKClient" + ) as mocked_client_cls: + mocked = mock.MagicMock() + mocked.list_policies = fake_list_policies + mocked.list_policy_rules = fake_list_rules + mocked.list_brands = _empty_brands + mocked_client_cls.return_value = mocked + service = Signon(provider) + + assert calls == [None, "cursor-2"] + assert set(service.global_session_policies.keys()) == {"pol-1", "pol-2"} + + def test_returns_empty_on_api_error(self): + provider = set_mocked_okta_provider() + + async def failing(*_a, **_k): + return ([], _resp({}), Exception("E0000007: not found")) + + with mock.patch( + "prowler.providers.okta.lib.service.service.OktaSDKClient" + ) as mocked_client_cls: + mocked = mock.MagicMock() + mocked.list_policies = failing + mocked.list_brands = _empty_brands + mocked_client_cls.return_value = mocked + service = Signon(provider) + + assert service.global_session_policies == {} + + def test_skips_policy_fetch_when_scope_missing(self): + identity = OktaIdentityInfo( + org_domain=OKTA_ORG_DOMAIN, + client_id=OKTA_CLIENT_ID, + granted_scopes=["okta.brands.read"], # policies scope missing + ) + provider = set_mocked_okta_provider(identity=identity) + + list_policies_called = False + + async def fake_list_policies(*_a, **_k): + nonlocal list_policies_called + list_policies_called = True + return ([], _resp({}), None) + + with mock.patch( + "prowler.providers.okta.lib.service.service.OktaSDKClient" + ) as mocked_client_cls: + mocked = mock.MagicMock() + mocked.list_policies = fake_list_policies + mocked.list_brands = _empty_brands + mocked_client_cls.return_value = mocked + service = Signon(provider) + + assert list_policies_called is False + assert service.global_session_policies == {} + assert service.missing_scope["global_session_policies"] == "okta.policies.read" + assert service.missing_scope["sign_in_pages"] is None + + def test_unknown_granted_scopes_falls_back_to_attempting_fetch(self): + # When the JWT couldn't be decoded, granted_scopes is empty and the + # service must still attempt the fetch — preserves prior behavior. + identity = OktaIdentityInfo( + org_domain=OKTA_ORG_DOMAIN, + client_id=OKTA_CLIENT_ID, + granted_scopes=[], + ) + provider = set_mocked_okta_provider(identity=identity) + + list_policies_called = False + + async def fake_list_policies(*_a, **_k): + nonlocal list_policies_called + list_policies_called = True + return ([], _resp({}), None) + + with mock.patch( + "prowler.providers.okta.lib.service.service.OktaSDKClient" + ) as mocked_client_cls: + mocked = mock.MagicMock() + mocked.list_policies = fake_list_policies + mocked.list_brands = _empty_brands + mocked_client_cls.return_value = mocked + service = Signon(provider) + + assert list_policies_called is True + assert service.missing_scope["global_session_policies"] is None + assert service.missing_scope["sign_in_pages"] is None + + +class Test_Signon_service_brands: + """Brand sign-in page fetching for the DOD banner check.""" + + def _build_with_brands( + self, + provider, + brands_response, + sign_in_page_responses: dict, + default_sign_in_page_responses: dict | None = None, + ): + async def fake_list_policies(*_a, **_k): + return ([], _resp({}), None) + + async def fake_list_brands(*_a, **_k): + return brands_response + + async def fake_get_sign_in_page(brand_id, *_a, **_k): + return sign_in_page_responses[brand_id] + + async def fake_get_default_sign_in_page(brand_id, *_a, **_k): + return default_sign_in_page_responses[brand_id] + + with mock.patch( + "prowler.providers.okta.lib.service.service.OktaSDKClient" + ) as mocked_client_cls: + mocked = mock.MagicMock() + mocked.list_policies = fake_list_policies + mocked.list_brands = fake_list_brands + mocked.get_customized_sign_in_page = fake_get_sign_in_page + mocked.get_default_sign_in_page = fake_get_default_sign_in_page + mocked_client_cls.return_value = mocked + return Signon(provider) + + def test_fetches_brand_with_customized_page(self): + provider = set_mocked_okta_provider() + brand = _fake_brand("brand-1", "Primary") + page = _fake_sign_in_page("banner here") + service = self._build_with_brands( + provider, + brands_response=([brand], _resp({}), None), + sign_in_page_responses={"brand-1": (page, _resp({}), None)}, + ) + + assert "brand-1" in service.sign_in_pages + result = service.sign_in_pages["brand-1"] + assert isinstance(result, SignInPage) + assert result.is_customized is True + assert result.page_content == "banner here" + assert result.fetch_error is None + + def test_404_falls_back_to_default_sign_in_page(self): + provider = set_mocked_okta_provider() + brand = _fake_brand("brand-1", "Primary") + default_page = _fake_sign_in_page("default banner here") + service = self._build_with_brands( + provider, + brands_response=([brand], _resp({}), None), + sign_in_page_responses={ + "brand-1": (None, _resp({}), Exception("404 Not Found")) + }, + default_sign_in_page_responses={"brand-1": (default_page, _resp({}), None)}, + ) + + assert service.sign_in_pages["brand-1"].is_customized is False + assert service.sign_in_pages["brand-1"].fetch_error is None + assert ( + service.sign_in_pages["brand-1"].page_content + == "default banner here" + ) + + def test_default_sign_in_page_error_captured_when_customized_page_missing(self): + provider = set_mocked_okta_provider() + brand = _fake_brand("brand-1", "Primary") + service = self._build_with_brands( + provider, + brands_response=([brand], _resp({}), None), + sign_in_page_responses={ + "brand-1": (None, _resp({}), Exception("404 Not Found")) + }, + default_sign_in_page_responses={ + "brand-1": (None, _resp({}), Exception("403 Forbidden")) + }, + ) + + result = service.sign_in_pages["brand-1"] + assert result.is_customized is False + assert "403" in result.fetch_error + + def test_403_captured_into_fetch_error(self): + provider = set_mocked_okta_provider() + brand = _fake_brand("brand-1", "Primary") + service = self._build_with_brands( + provider, + brands_response=([brand], _resp({}), None), + sign_in_page_responses={ + "brand-1": (None, _resp({}), Exception("403 Forbidden: invalid_scope")) + }, + default_sign_in_page_responses={}, + ) + + result = service.sign_in_pages["brand-1"] + assert result.is_customized is False + assert "403" in result.fetch_error + + def test_returns_empty_on_brands_api_error(self): + provider = set_mocked_okta_provider() + + async def fake_list_policies(*_a, **_k): + return ([], _resp({}), None) + + async def failing_brands(*_a, **_k): + return ([], _resp({}), Exception("Brands API unavailable")) + + with mock.patch( + "prowler.providers.okta.lib.service.service.OktaSDKClient" + ) as mocked_client_cls: + mocked = mock.MagicMock() + mocked.list_policies = fake_list_policies + mocked.list_brands = failing_brands + mocked_client_cls.return_value = mocked + service = Signon(provider) + + assert service.sign_in_pages == {} + + def test_skips_brand_fetch_when_scope_missing(self): + identity = OktaIdentityInfo( + org_domain=OKTA_ORG_DOMAIN, + client_id=OKTA_CLIENT_ID, + granted_scopes=["okta.policies.read"], # brands scope missing + ) + provider = set_mocked_okta_provider(identity=identity) + + async def fake_list_policies(*_a, **_k): + return ([], _resp({}), None) + + list_brands_called = False + + async def fake_list_brands(*_a, **_k): + nonlocal list_brands_called + list_brands_called = True + return ([], _resp({}), None) + + with mock.patch( + "prowler.providers.okta.lib.service.service.OktaSDKClient" + ) as mocked_client_cls: + mocked = mock.MagicMock() + mocked.list_policies = fake_list_policies + mocked.list_brands = fake_list_brands + mocked_client_cls.return_value = mocked + service = Signon(provider) + + assert list_brands_called is False + assert service.sign_in_pages == {} + assert service.missing_scope["sign_in_pages"] == "okta.brands.read" + assert service.missing_scope["global_session_policies"] is None + + def test_handles_multiple_brands(self): + provider = set_mocked_okta_provider() + brand_a = _fake_brand("brand-a", "Brand A") + brand_b = _fake_brand("brand-b", "Brand B") + page_a = _fake_sign_in_page("A") + + service = self._build_with_brands( + provider, + brands_response=([brand_a, brand_b], _resp({}), None), + sign_in_page_responses={ + "brand-a": (page_a, _resp({}), None), + "brand-b": (None, _resp({}), Exception("404 not found")), + }, + default_sign_in_page_responses={ + "brand-b": ( + _fake_sign_in_page("default B"), + _resp({}), + None, + ) + }, + ) + + assert set(service.sign_in_pages.keys()) == {"brand-a", "brand-b"} + assert service.sign_in_pages["brand-a"].page_content == "A" + assert service.sign_in_pages["brand-b"].is_customized is False + assert service.sign_in_pages["brand-b"].page_content == "default B" diff --git a/tests/providers/oraclecloud/oci_fixtures.py b/tests/providers/oraclecloud/oci_fixtures.py index b1ebef62d9..5d01bc5855 100644 --- a/tests/providers/oraclecloud/oci_fixtures.py +++ b/tests/providers/oraclecloud/oci_fixtures.py @@ -37,6 +37,7 @@ def set_mocked_oraclecloud_provider( signer=MagicMock(), profile="DEFAULT", ) + provider.home_region = region # Mock identity provider.identity = OCIIdentityInfo( diff --git a/tests/providers/oraclecloud/oraclecloud_provider_test.py b/tests/providers/oraclecloud/oraclecloud_provider_test.py index dd3b7b7d27..7c437a35ac 100644 --- a/tests/providers/oraclecloud/oraclecloud_provider_test.py +++ b/tests/providers/oraclecloud/oraclecloud_provider_test.py @@ -6,7 +6,7 @@ from prowler.providers.oraclecloud.exceptions.exceptions import ( OCIAuthenticationError, OCIInvalidConfigError, ) -from prowler.providers.oraclecloud.models import OCISession +from prowler.providers.oraclecloud.models import OCIIdentityInfo, OCIRegion, OCISession from prowler.providers.oraclecloud.oraclecloud_provider import OraclecloudProvider @@ -199,3 +199,117 @@ MIIEpQIBAAKCAQEA0Z3VS5JJcds3xfn/ygWyF8n0sMcD/QHWCJ7yGSEtLN2T ) assert connection.is_connected is True + + +class TestOraclecloudProviderInit: + """Tests for OraclecloudProvider initialization""" + + def test_init_with_region_set_populates_provider_state(self): + mock_session = OCISession( + config={"region": "us-ashburn-1"}, signer=None, profile="DEFAULT" + ) + mock_identity = OCIIdentityInfo( + tenancy_id="ocid1.tenancy.oc1..aaaaaaaexample", + tenancy_name="test-tenancy", + user_id="ocid1.user.oc1..aaaaaaaexample", + region="us-ashburn-1", + profile="DEFAULT", + audited_regions=set(), + audited_compartments=[], + ) + mock_regions = [ + OCIRegion(key="us-phoenix-1", name="us-phoenix-1", is_home_region=False), + OCIRegion(key="us-ashburn-1", name="us-ashburn-1", is_home_region=True), + ] + mock_compartments = ["ocid1.compartment.oc1..aaaaaaaexample"] + with ( + patch( + "prowler.providers.oraclecloud.oraclecloud_provider.OraclecloudProvider.setup_session", + return_value=mock_session, + ) as mock_setup_session, + patch( + "prowler.providers.oraclecloud.oraclecloud_provider.OraclecloudProvider.set_identity", + return_value=mock_identity, + ), + patch( + "prowler.providers.oraclecloud.oraclecloud_provider.OraclecloudProvider.get_regions_to_audit", + return_value=mock_regions, + ), + patch( + "prowler.providers.oraclecloud.oraclecloud_provider.OraclecloudProvider.get_compartments_to_audit", + return_value=mock_compartments, + ), + patch( + "prowler.providers.common.provider.Provider.set_global_provider" + ) as mock_set_global, + ): + provider = OraclecloudProvider( + region={"us-ashburn-1"}, + config_content={"dummy": True}, + mutelist_content={"Accounts": {}}, + ) + assert mock_setup_session.call_args.kwargs["region"] == "us-ashburn-1" + assert provider.session == mock_session + assert provider.identity == mock_identity + assert provider.regions == mock_regions + assert provider.compartments == mock_compartments + assert provider.home_region == "us-ashburn-1" + mock_set_global.assert_called_once_with(provider) + + def test_home_region_uses_full_subscription_list_not_region_filter(self): + """Home region must come from the full subscription list, not the --region filter. + + When auditing a single non-home region, the tenancy home region must still be + resolved correctly so tenancy-level APIs (e.g. the Audit configuration) target it. + """ + mock_session = OCISession( + config={"region": "eu-frankfurt-1"}, signer=None, profile="DEFAULT" + ) + mock_identity = OCIIdentityInfo( + tenancy_id="ocid1.tenancy.oc1..aaaaaaaexample", + tenancy_name="test-tenancy", + user_id="ocid1.user.oc1..aaaaaaaexample", + region="eu-frankfurt-1", + profile="DEFAULT", + audited_regions=set(), + audited_compartments=[], + ) + # The audited set is the non-home region; the full subscription list includes home + audited_regions = [ + OCIRegion( + key="eu-frankfurt-1", name="eu-frankfurt-1", is_home_region=False + ), + ] + all_subscribed_regions = [ + OCIRegion( + key="eu-frankfurt-1", name="eu-frankfurt-1", is_home_region=False + ), + OCIRegion(key="us-ashburn-1", name="us-ashburn-1", is_home_region=True), + ] + with ( + patch( + "prowler.providers.oraclecloud.oraclecloud_provider.OraclecloudProvider.setup_session", + return_value=mock_session, + ), + patch( + "prowler.providers.oraclecloud.oraclecloud_provider.OraclecloudProvider.set_identity", + return_value=mock_identity, + ), + patch( + "prowler.providers.oraclecloud.oraclecloud_provider.OraclecloudProvider.get_regions_to_audit", + side_effect=[audited_regions, all_subscribed_regions], + ), + patch( + "prowler.providers.oraclecloud.oraclecloud_provider.OraclecloudProvider.get_compartments_to_audit", + return_value=["ocid1.compartment.oc1..aaaaaaaexample"], + ), + patch("prowler.providers.common.provider.Provider.set_global_provider"), + ): + provider = OraclecloudProvider( + region={"eu-frankfurt-1"}, + config_content={"dummy": True}, + mutelist_content={"Accounts": {}}, + ) + + assert provider.regions == audited_regions + assert provider.home_region == "us-ashburn-1" diff --git a/tests/providers/oraclecloud/services/audit/audit_service_test.py b/tests/providers/oraclecloud/services/audit/audit_service_test.py index ab3710fa3d..9a3e73f9fb 100644 --- a/tests/providers/oraclecloud/services/audit/audit_service_test.py +++ b/tests/providers/oraclecloud/services/audit/audit_service_test.py @@ -1,6 +1,11 @@ -from unittest.mock import patch +from unittest.mock import MagicMock, patch -from tests.providers.oraclecloud.oci_fixtures import set_mocked_oraclecloud_provider +import oci + +from tests.providers.oraclecloud.oci_fixtures import ( + OCI_TENANCY_ID, + set_mocked_oraclecloud_provider, +) class TestAuditService: @@ -8,21 +13,58 @@ class TestAuditService: """Test that audit service can be instantiated and mocked""" oraclecloud_provider = set_mocked_oraclecloud_provider() - # Mock the entire service initialization - with patch( - "prowler.providers.oraclecloud.services.audit.audit_service.Audit.__init__", - return_value=None, - ): - from prowler.providers.oraclecloud.services.audit.audit_service import Audit + mock_config_response = MagicMock() + mock_config_response.data.retention_period_days = 365 + mock_audit_client = MagicMock() + mock_audit_client.get_configuration.return_value = mock_config_response + + from prowler.providers.oraclecloud.services.audit.audit_service import Audit + + with patch.object(Audit, "_create_oci_client", return_value=mock_audit_client): audit_client = Audit(oraclecloud_provider) - # Manually set required attributes since __init__ was mocked - audit_client.service = "audit" - audit_client.provider = oraclecloud_provider - audit_client.audited_compartments = {} - audit_client.regional_clients = {} - - # Verify service name assert audit_client.service == "audit" assert audit_client.provider == oraclecloud_provider + + def test_get_configuration_uses_home_region_not_configured_region(self): + """Test Audit uses the tenancy home region instead of the configured region.""" + oraclecloud_provider = set_mocked_oraclecloud_provider(region="eu-frankfurt-1") + # The tenancy home region differs from the configured session region + oraclecloud_provider.home_region = "us-ashburn-1" + mock_config_response = MagicMock() + mock_config_response.data.retention_period_days = 365 + + mock_audit_client = MagicMock() + mock_audit_client.get_configuration.return_value = mock_config_response + + from prowler.providers.oraclecloud.services.audit.audit_service import Audit + + with patch.object( + Audit, "_create_oci_client", return_value=mock_audit_client + ) as mock_create_oci_client: + audit = Audit(oraclecloud_provider) + + mock_create_oci_client.assert_called_once_with( + oci.audit.AuditClient, + config_overrides={"region": "us-ashburn-1"}, + ) + assert audit.configuration is not None + assert audit.configuration.retention_period_days == 365 + assert audit.configuration.compartment_id == OCI_TENANCY_ID + + def test_get_configuration_handles_api_error(self): + """Test audit configuration falls back to 90 days on API errors.""" + oraclecloud_provider = set_mocked_oraclecloud_provider() + + mock_audit_client = MagicMock() + mock_audit_client.get_configuration.side_effect = Exception("404 Not Found") + + from prowler.providers.oraclecloud.services.audit.audit_service import Audit + + with patch.object(Audit, "_create_oci_client", return_value=mock_audit_client): + audit = Audit(oraclecloud_provider) + + assert audit.configuration is not None + assert audit.configuration.retention_period_days == 90 + assert audit.configuration.compartment_id == OCI_TENANCY_ID diff --git a/tests/providers/oraclecloud/services/identity/identity_service_test.py b/tests/providers/oraclecloud/services/identity/identity_service_test.py index bb695d7128..338a025d52 100644 --- a/tests/providers/oraclecloud/services/identity/identity_service_test.py +++ b/tests/providers/oraclecloud/services/identity/identity_service_test.py @@ -1,4 +1,7 @@ -from unittest.mock import patch +from concurrent.futures import ThreadPoolExecutor +from datetime import datetime +from threading import Lock +from unittest.mock import MagicMock, patch from tests.providers.oraclecloud.oci_fixtures import set_mocked_oraclecloud_provider @@ -28,3 +31,184 @@ class TestIdentityService: # Verify service name assert identity_client.service == "identity" assert identity_client.provider == oraclecloud_provider + + def test_list_domains_passwords_skipped_outside_home(self): + """Domains should be skipped when not in home region.""" + with patch( + "prowler.providers.oraclecloud.services.identity.identity_service.Identity.__init__", + return_value=None, + ): + from prowler.providers.oraclecloud.services.identity.identity_service import ( + Identity, + ) + + identity_client = Identity(None) + identity_client.service = "identity" + identity_client.provider = set_mocked_oraclecloud_provider() + identity_client.provider._home_region = "us-ashburn-1" + identity_client.audited_compartments = [ + MagicMock(id="ocid1.compartment.oc1..aaaaaaaexample") + ] + identity_client.domains = [] + identity_client._domains_lock = Lock() + identity_client.session_signer = None + identity_client.session_config = None + regional_client_ash = MagicMock() + regional_client_ash.region = "us-ashburn-1" + regional_client_chi = MagicMock() + regional_client_chi.region = "us-chicago-1" + + policy = MagicMock() + policy.id = "123" + policy.name = "Test Policy" + policy.description = "This is a test policy" + policy.min_length = 8 + policy.password_expires_after = 90 + policy.num_passwords_in_history = 5 + policy.password_expire_warning = 7 + policy.min_password_age = 1 + + domains = [] + for region in ["us-phoenix-1", "us-ashburn-1", "us-chicago-1"]: + domain = MagicMock() + domain.id = ( + "ocid1.domain.oc1.iad.aaaaaaaaexampleuniqueID" + if region == "us-chicago-1" + else "ocid1.domain.oc1.iad.aaaaaaaaexampleuniqueID2" + ) + domain.display_name = "exampledomain" + domain.description = "example" + domain.url = "https://idcs-example.identity.oraclecloud.com" + domain.home_region = region + domain.region = "us-ashburn-1" + domain.lifecycle_state = "ACTIVE" + domain.time_created = datetime.now() + domains.append(domain) + with ( + patch( + "prowler.providers.oraclecloud.services.identity.identity_service.Identity.__get_client__", + return_value=MagicMock(), + ), + patch( + "prowler.providers.oraclecloud.services.identity.identity_service.oci.pagination.list_call_get_all_results", + return_value=MagicMock(data=domains), + ), + patch( + "prowler.providers.oraclecloud.services.identity.identity_service.oci.identity_domains.IdentityDomainsClient", + return_value=MagicMock( + list_password_policies=lambda: MagicMock( + data=MagicMock(resources=[policy]) + ) + ), + ), + ): + identity_client.__list_domains__(regional_client_ash) + identity_client.__list_domains__(regional_client_chi) + identity_client.__list_domain_password_policies__(regional_client_ash) + identity_client.__list_domain_password_policies__(regional_client_chi) + + assert ( + len(identity_client.domains) == 2 + and any( + domain.home_region == "us-ashburn-1" + and domain.region == "us-ashburn-1" + for domain in identity_client.domains + ) + and any( + domain.home_region == "us-chicago-1" + and domain.region == "us-chicago-1" + for domain in identity_client.domains + ) + and all(len(d.password_policies) == 1 for d in identity_client.domains) + ) + + def test_list_domains_concurrent_dedupes_and_prefers_home_region(self): + """__list_domains__ runs across regions in parallel; the dedupe + must stay correct under concurrent calls (no duplicates, home + region wins).""" + with patch( + "prowler.providers.oraclecloud.services.identity.identity_service.Identity.__init__", + return_value=None, + ): + from prowler.providers.oraclecloud.services.identity.identity_service import ( + Identity, + ) + + identity_client = Identity(None) + identity_client.service = "identity" + identity_client.provider = set_mocked_oraclecloud_provider() + identity_client.audited_compartments = [ + MagicMock(id="ocid1.compartment.oc1..aaaaaaaexample") + ] + identity_client.domains = [] + identity_client._domains_lock = Lock() + identity_client.session_signer = None + identity_client.session_config = None + + regions = [ + "us-ashburn-1", + "us-chicago-1", + "us-phoenix-1", + "eu-frankfurt-1", + ] + home_region_by_domain = { + "ocid1.domain.oc1..domainA": "us-ashburn-1", + "ocid1.domain.oc1..domainB": "us-chicago-1", + "ocid1.domain.oc1..domainC": "eu-frankfurt-1", + } + + # Each region returns the same set of domains (every domain + # is visible from every region; only one of those regions is + # actually the domain's home region). + def make_domains_for_region(_region): + ds = [] + for domain_id, home_region in home_region_by_domain.items(): + d = MagicMock() + d.id = domain_id + d.display_name = f"name-{domain_id}" + d.description = "" + d.url = "https://example.identity.oraclecloud.com" + d.home_region = home_region + d.lifecycle_state = "ACTIVE" + d.time_created = datetime.now() + ds.append(d) + return MagicMock(data=ds) + + regional_clients = [] + for region in regions: + rc = MagicMock() + rc.region = region + regional_clients.append(rc) + + with ( + patch( + "prowler.providers.oraclecloud.services.identity.identity_service.Identity.__get_client__", + return_value=MagicMock(), + ), + patch( + "prowler.providers.oraclecloud.services.identity.identity_service.oci.pagination.list_call_get_all_results", + side_effect=lambda _list_call, compartment_id, lifecycle_state: make_domains_for_region( + compartment_id + ), + ), + ): + # Run several iterations to make any race more likely + # to surface; with the lock removed this loop fails + # frequently with duplicates. + for _ in range(20): + identity_client.domains = [] + with ThreadPoolExecutor( + max_workers=len(regional_clients) + ) as executor: + futures = [ + executor.submit(identity_client.__list_domains__, rc) + for rc in regional_clients + ] + for f in futures: + f.result() + + assert len(identity_client.domains) == len(home_region_by_domain) + by_id = {d.id: d for d in identity_client.domains} + for domain_id, home_region in home_region_by_domain.items(): + assert by_id[domain_id].region == home_region + assert by_id[domain_id].home_region == home_region diff --git a/tests/providers/scaleway/scaleway_fixtures.py b/tests/providers/scaleway/scaleway_fixtures.py new file mode 100644 index 0000000000..d65dd9f2e9 --- /dev/null +++ b/tests/providers/scaleway/scaleway_fixtures.py @@ -0,0 +1,96 @@ +from unittest.mock import MagicMock + +from prowler.providers.scaleway.models import ( + ScalewayIdentityInfo, + ScalewaySession, +) +from prowler.providers.scaleway.services.iam.iam_service import ( + ScalewayAPIKey, + ScalewayUser, +) + +# Scaleway Identity +ORGANIZATION_ID = "b4ce0bfc-38fc-4c53-8757-548be64add26" +ROOT_USER_ID = "00000000-0000-0000-0000-000000000001" +MEMBER_USER_ID = "00000000-0000-0000-0000-000000000002" +APPLICATION_ID = "00000000-0000-0000-0000-000000000003" +BEARER_EMAIL = "pedro@prowler.com" + +# Scaleway Credentials +ACCESS_KEY = "SCWAE000000000000000" +SECRET_KEY = "00000000-0000-0000-0000-000000000000" + +# API Key Constants +ROOT_API_KEY = "SCWROOT00000000000000" +USER_API_KEY = "SCWUSER00000000000000" +APP_API_KEY = "SCWAPP000000000000000" + + +def set_mocked_scaleway_provider( + access_key: str = ACCESS_KEY, + secret_key: str = SECRET_KEY, + identity: ScalewayIdentityInfo = None, + audit_config: dict = None, +): + """Create a mocked ScalewayProvider for testing.""" + provider = MagicMock() + provider.type = "scaleway" + provider.session = ScalewaySession( + access_key=access_key, + secret_key=secret_key, + organization_id=ORGANIZATION_ID, + default_project_id=None, + default_region="fr-par", + client=MagicMock(), + ) + provider.identity = identity or ScalewayIdentityInfo( + organization_id=ORGANIZATION_ID, + bearer_id=ROOT_USER_ID, + bearer_type="user", + bearer_email=BEARER_EMAIL, + account_root_user_id=ROOT_USER_ID, + ) + provider.audit_config = audit_config or {} + provider.fixer_config = {} + + return provider + + +def make_user( + user_id: str = ROOT_USER_ID, + email: str = BEARER_EMAIL, + account_root_user_id: str = ROOT_USER_ID, + mfa: bool = True, +) -> ScalewayUser: + return ScalewayUser( + id=user_id, + email=email, + username=email.split("@")[0] if email else None, + organization_id=ORGANIZATION_ID, + account_root_user_id=account_root_user_id, + mfa=mfa, + type_="owner" if user_id == account_root_user_id else "member", + status="activated", + ) + + +def make_api_key( + access_key: str = USER_API_KEY, + user_id: str = MEMBER_USER_ID, + application_id: str = None, + description: str = "test key", + expires_at: str = None, +) -> ScalewayAPIKey: + return ScalewayAPIKey( + access_key=access_key, + description=description, + user_id=user_id, + application_id=application_id, + default_project_id=None, + editable=True, + managed=False, + creation_ip=None, + created_at="2026-01-01T00:00:00Z", + updated_at="2026-01-01T00:00:00Z", + expires_at=expires_at, + ) diff --git a/tests/providers/scaleway/scaleway_provider_test.py b/tests/providers/scaleway/scaleway_provider_test.py new file mode 100644 index 0000000000..074ff9297c --- /dev/null +++ b/tests/providers/scaleway/scaleway_provider_test.py @@ -0,0 +1,106 @@ +import os +from unittest import mock + +import pytest + +from prowler.providers.scaleway.exceptions.exceptions import ( + ScalewayAuthenticationError, + ScalewayCredentialsError, + ScalewayIdentityError, +) +from prowler.providers.scaleway.models import ScalewaySession +from prowler.providers.scaleway.scaleway_provider import ScalewayProvider +from tests.providers.scaleway.scaleway_fixtures import ( + ACCESS_KEY, + BEARER_EMAIL, + ORGANIZATION_ID, + ROOT_USER_ID, + SECRET_KEY, +) + + +class Test_ScalewayProvider_setup_session: + def test_missing_access_key_raises_credentials_error(self): + with mock.patch.dict( + os.environ, {"SCW_ACCESS_KEY": "", "SCW_SECRET_KEY": ""}, clear=False + ): + os.environ.pop("SCW_ACCESS_KEY", None) + os.environ.pop("SCW_SECRET_KEY", None) + with pytest.raises(ScalewayCredentialsError): + ScalewayProvider.setup_session() + + def test_returns_session_with_credentials(self): + session = ScalewayProvider.setup_session( + access_key=ACCESS_KEY, + secret_key=SECRET_KEY, + organization_id=ORGANIZATION_ID, + ) + assert isinstance(session, ScalewaySession) + assert session.access_key == ACCESS_KEY + assert session.organization_id == ORGANIZATION_ID + assert session.default_region == "fr-par" + + +class Test_ScalewayProvider_setup_identity: + def _build_session(self): + return ScalewaySession( + access_key=ACCESS_KEY, + secret_key=SECRET_KEY, + organization_id=ORGANIZATION_ID, + default_region="fr-par", + client=mock.MagicMock(), + ) + + def test_resolves_user_bearer_identity(self): + session = self._build_session() + api_key = mock.MagicMock(user_id=ROOT_USER_ID, application_id=None) + user = mock.MagicMock( + email=BEARER_EMAIL, + organization_id=ORGANIZATION_ID, + account_root_user_id=ROOT_USER_ID, + ) + + with mock.patch( + "prowler.providers.scaleway.scaleway_provider.IamV1Alpha1API" + ) as iam_cls: + iam = iam_cls.return_value + iam.get_api_key.return_value = api_key + iam.get_user.return_value = user + + identity = ScalewayProvider.setup_identity(session) + + assert identity.organization_id == ORGANIZATION_ID + assert identity.bearer_type == "user" + assert identity.bearer_id == ROOT_USER_ID + assert identity.bearer_email == BEARER_EMAIL + assert identity.account_root_user_id == ROOT_USER_ID + + def test_missing_organization_raises_identity_error(self): + session = self._build_session() + session.organization_id = None + api_key = mock.MagicMock(user_id=None, application_id="app-id") + + with mock.patch( + "prowler.providers.scaleway.scaleway_provider.IamV1Alpha1API" + ) as iam_cls: + iam = iam_cls.return_value + iam.get_api_key.return_value = api_key + + with pytest.raises(ScalewayIdentityError): + ScalewayProvider.setup_identity(session) + + +class Test_ScalewayProvider_validate_credentials: + def test_invalid_credentials_raise_authentication_error(self): + session = ScalewaySession( + access_key=ACCESS_KEY, + secret_key=SECRET_KEY, + organization_id=ORGANIZATION_ID, + client=mock.MagicMock(), + ) + with mock.patch( + "prowler.providers.scaleway.scaleway_provider.IamV1Alpha1API" + ) as iam_cls: + iam_cls.return_value.get_api_key.side_effect = Exception("expired") + with pytest.raises(ScalewayAuthenticationError): + ScalewayProvider.validate_credentials(session) diff --git a/tests/providers/scaleway/services/iam/iam_api_keys_no_root_owned/iam_api_keys_no_root_owned_test.py b/tests/providers/scaleway/services/iam/iam_api_keys_no_root_owned/iam_api_keys_no_root_owned_test.py new file mode 100644 index 0000000000..2473c4118c --- /dev/null +++ b/tests/providers/scaleway/services/iam/iam_api_keys_no_root_owned/iam_api_keys_no_root_owned_test.py @@ -0,0 +1,204 @@ +from unittest import mock + +from tests.providers.scaleway.scaleway_fixtures import ( + APP_API_KEY, + APPLICATION_ID, + MEMBER_USER_ID, + ORGANIZATION_ID, + ROOT_API_KEY, + ROOT_USER_ID, + USER_API_KEY, + make_api_key, + set_mocked_scaleway_provider, +) + + +def _patch_clients(iam_client_mock): + """Patch both the provider and the iam_client singleton.""" + return [ + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_scaleway_provider(), + ), + mock.patch( + "prowler.providers.scaleway.services.iam.iam_api_keys_no_root_owned.iam_api_keys_no_root_owned.iam_client", + new=iam_client_mock, + ), + ] + + +class Test_iam_api_keys_no_root_owned: + def test_no_api_keys_returns_empty_findings(self): + iam_client = mock.MagicMock() + iam_client.users_loaded = True + iam_client.api_keys_loaded = True + iam_client.account_root_user_id = ROOT_USER_ID + iam_client.api_keys = [] + iam_client.organization_id = ORGANIZATION_ID + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_scaleway_provider(), + ), + mock.patch( + "prowler.providers.scaleway.services.iam.iam_api_keys_no_root_owned.iam_api_keys_no_root_owned.iam_client", + new=iam_client, + ), + ): + from prowler.providers.scaleway.services.iam.iam_api_keys_no_root_owned.iam_api_keys_no_root_owned import ( + iam_api_keys_no_root_owned, + ) + + result = iam_api_keys_no_root_owned().execute() + assert result == [] + + def test_root_api_key_fails(self): + iam_client = mock.MagicMock() + iam_client.users_loaded = True + iam_client.api_keys_loaded = True + iam_client.account_root_user_id = ROOT_USER_ID + iam_client.api_keys = [ + make_api_key(access_key=ROOT_API_KEY, user_id=ROOT_USER_ID) + ] + iam_client.organization_id = ORGANIZATION_ID + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_scaleway_provider(), + ), + mock.patch( + "prowler.providers.scaleway.services.iam.iam_api_keys_no_root_owned.iam_api_keys_no_root_owned.iam_client", + new=iam_client, + ), + ): + from prowler.providers.scaleway.services.iam.iam_api_keys_no_root_owned.iam_api_keys_no_root_owned import ( + iam_api_keys_no_root_owned, + ) + + result = iam_api_keys_no_root_owned().execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].resource_id == ROOT_API_KEY + assert ROOT_USER_ID in result[0].status_extended + + def test_user_api_key_passes(self): + iam_client = mock.MagicMock() + iam_client.users_loaded = True + iam_client.api_keys_loaded = True + iam_client.account_root_user_id = ROOT_USER_ID + iam_client.api_keys = [ + make_api_key(access_key=USER_API_KEY, user_id=MEMBER_USER_ID) + ] + iam_client.organization_id = ORGANIZATION_ID + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_scaleway_provider(), + ), + mock.patch( + "prowler.providers.scaleway.services.iam.iam_api_keys_no_root_owned.iam_api_keys_no_root_owned.iam_client", + new=iam_client, + ), + ): + from prowler.providers.scaleway.services.iam.iam_api_keys_no_root_owned.iam_api_keys_no_root_owned import ( + iam_api_keys_no_root_owned, + ) + + result = iam_api_keys_no_root_owned().execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].resource_id == USER_API_KEY + + def test_application_api_key_passes(self): + iam_client = mock.MagicMock() + iam_client.users_loaded = True + iam_client.api_keys_loaded = True + iam_client.account_root_user_id = ROOT_USER_ID + iam_client.api_keys = [ + make_api_key( + access_key=APP_API_KEY, user_id=None, application_id=APPLICATION_ID + ) + ] + iam_client.organization_id = ORGANIZATION_ID + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_scaleway_provider(), + ), + mock.patch( + "prowler.providers.scaleway.services.iam.iam_api_keys_no_root_owned.iam_api_keys_no_root_owned.iam_client", + new=iam_client, + ), + ): + from prowler.providers.scaleway.services.iam.iam_api_keys_no_root_owned.iam_api_keys_no_root_owned import ( + iam_api_keys_no_root_owned, + ) + + result = iam_api_keys_no_root_owned().execute() + assert len(result) == 1 + assert result[0].status == "PASS" + + def test_users_load_failure_returns_manual(self): + iam_client = mock.MagicMock() + iam_client.users_loaded = False + iam_client.api_keys_loaded = True + iam_client.account_root_user_id = None + iam_client.api_keys = [ + make_api_key(access_key=ROOT_API_KEY, user_id=ROOT_USER_ID) + ] + iam_client.organization_id = ORGANIZATION_ID + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_scaleway_provider(), + ), + mock.patch( + "prowler.providers.scaleway.services.iam.iam_api_keys_no_root_owned.iam_api_keys_no_root_owned.iam_client", + new=iam_client, + ), + ): + from prowler.providers.scaleway.services.iam.iam_api_keys_no_root_owned.iam_api_keys_no_root_owned import ( + iam_api_keys_no_root_owned, + ) + + result = iam_api_keys_no_root_owned().execute() + assert len(result) == 1 + assert result[0].status == "MANUAL" + assert "Could not retrieve" in result[0].status_extended + + def test_root_user_unresolved_returns_manual(self): + # Data loaded fine but account_root_user_id could not be resolved + # (e.g. application-scoped key). A root-owned key must NOT slip + # through as PASS — the check degrades to MANUAL instead. + iam_client = mock.MagicMock() + iam_client.users_loaded = True + iam_client.api_keys_loaded = True + iam_client.account_root_user_id = None + iam_client.api_keys = [ + make_api_key(access_key=ROOT_API_KEY, user_id=ROOT_USER_ID) + ] + iam_client.organization_id = ORGANIZATION_ID + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_scaleway_provider(), + ), + mock.patch( + "prowler.providers.scaleway.services.iam.iam_api_keys_no_root_owned.iam_api_keys_no_root_owned.iam_client", + new=iam_client, + ), + ): + from prowler.providers.scaleway.services.iam.iam_api_keys_no_root_owned.iam_api_keys_no_root_owned import ( + iam_api_keys_no_root_owned, + ) + + result = iam_api_keys_no_root_owned().execute() + assert len(result) == 1 + assert result[0].status == "MANUAL" + assert "account root user" in result[0].status_extended diff --git a/tests/providers/scaleway/services/iam/scaleway_iam_service_test.py b/tests/providers/scaleway/services/iam/scaleway_iam_service_test.py new file mode 100644 index 0000000000..cb3a05d205 --- /dev/null +++ b/tests/providers/scaleway/services/iam/scaleway_iam_service_test.py @@ -0,0 +1,138 @@ +from unittest import mock + +from prowler.providers.scaleway.models import ScalewayIdentityInfo +from prowler.providers.scaleway.services.iam.iam_service import IAM +from tests.providers.scaleway.scaleway_fixtures import ( + APPLICATION_ID, + MEMBER_USER_ID, + ORGANIZATION_ID, + ROOT_USER_ID, + USER_API_KEY, + set_mocked_scaleway_provider, +) + + +def _application_identity() -> ScalewayIdentityInfo: + """Identity produced by an application-scoped API key: the IAM API + never exposes account_root_user_id for an application bearer.""" + return ScalewayIdentityInfo( + organization_id=ORGANIZATION_ID, + bearer_id=APPLICATION_ID, + bearer_type="application", + bearer_email=None, + account_root_user_id=None, + ) + + +def _mock_user( + user_id: str, account_root_user_id: str = ROOT_USER_ID, email: str = "u@example.com" +): + user = mock.MagicMock() + user.id = user_id + user.email = email + user.username = email.split("@")[0] + user.organization_id = ORGANIZATION_ID + user.account_root_user_id = account_root_user_id + user.mfa = True + user.type_ = "owner" if user_id == account_root_user_id else "member" + user.status = "activated" + return user + + +def _mock_api_key(access_key: str, user_id: str = None, application_id: str = None): + key = mock.MagicMock() + key.access_key = access_key + key.description = "test" + key.user_id = user_id + key.application_id = application_id + key.default_project_id = None + key.editable = True + key.managed = False + key.creation_ip = None + key.created_at = None + key.updated_at = None + key.expires_at = None + return key + + +class Test_IAM_service: + def test_loads_users_and_api_keys(self): + provider = set_mocked_scaleway_provider() + + with mock.patch( + "prowler.providers.scaleway.services.iam.iam_service.IamV1Alpha1API" + ) as iam_cls: + api = iam_cls.return_value + api.list_users_all.return_value = [ + _mock_user(ROOT_USER_ID), + _mock_user(MEMBER_USER_ID, email="m@example.com"), + ] + api.list_api_keys_all.return_value = [ + _mock_api_key(USER_API_KEY, user_id=MEMBER_USER_ID), + _mock_api_key("SCWAPP", application_id=APPLICATION_ID), + ] + + iam = IAM(provider) + + assert iam.users_loaded is True + assert iam.api_keys_loaded is True + assert iam.account_root_user_id == ROOT_USER_ID + assert len(iam.users) == 2 + assert len(iam.api_keys) == 2 + + def test_marks_users_unloaded_on_error(self): + provider = set_mocked_scaleway_provider() + + with mock.patch( + "prowler.providers.scaleway.services.iam.iam_service.IamV1Alpha1API" + ) as iam_cls: + api = iam_cls.return_value + api.list_users_all.side_effect = Exception("denied") + api.list_api_keys_all.return_value = [] + + iam = IAM(provider) + + assert iam.users_loaded is False + assert iam.api_keys_loaded is True + # account_root_user_id comes from the audit identity, not the user + # list, so a failed user listing must not blind the root-key check. + assert iam.account_root_user_id == ROOT_USER_ID + + def test_application_key_resolves_root_user_from_user_list(self): + # Application-scoped API key: identity.account_root_user_id is None, + # so it must be recovered from the loaded user list. Otherwise the + # root-key check would silently PASS root-owned keys. + provider = set_mocked_scaleway_provider(identity=_application_identity()) + + with mock.patch( + "prowler.providers.scaleway.services.iam.iam_service.IamV1Alpha1API" + ) as iam_cls: + api = iam_cls.return_value + api.list_users_all.return_value = [ + _mock_user(ROOT_USER_ID), + _mock_user(MEMBER_USER_ID, email="m@example.com"), + ] + api.list_api_keys_all.return_value = [] + + iam = IAM(provider) + + assert iam.account_root_user_id == ROOT_USER_ID + + def test_account_root_user_id_none_when_unresolvable(self): + # Application key + no user record exposes account_root_user_id: + # nothing to fall back to, so it stays None and the root-key check + # will degrade to MANUAL downstream. + provider = set_mocked_scaleway_provider(identity=_application_identity()) + + with mock.patch( + "prowler.providers.scaleway.services.iam.iam_service.IamV1Alpha1API" + ) as iam_cls: + api = iam_cls.return_value + api.list_users_all.return_value = [ + _mock_user(MEMBER_USER_ID, account_root_user_id=None) + ] + api.list_api_keys_all.return_value = [] + + iam = IAM(provider) + + assert iam.account_root_user_id is None diff --git a/tests/providers/vercel/vercel_provider_test.py b/tests/providers/vercel/vercel_provider_test.py index 56117ae6c4..a09e0b6d14 100644 --- a/tests/providers/vercel/vercel_provider_test.py +++ b/tests/providers/vercel/vercel_provider_test.py @@ -222,3 +222,52 @@ class TestVercelProviderTestConnection: with pytest.raises(VercelCredentialsError): VercelProvider.test_connection(raise_on_exception=True) + + +class TestVercelSessionTokenSecurity: + """The Vercel API token must never leak through serialization or repr.""" + + def test_token_is_still_accessible_as_attribute(self): + session = VercelSession(token=API_TOKEN, team_id=TEAM_ID) + + assert session.token == API_TOKEN + + def test_token_excluded_from_dict(self): + session = VercelSession(token=API_TOKEN, team_id=TEAM_ID) + + serialized = session.dict() + + assert "token" not in serialized + assert API_TOKEN not in str(serialized) + assert serialized["team_id"] == TEAM_ID + + def test_token_excluded_from_model_dump(self): + session = VercelSession(token=API_TOKEN, team_id=TEAM_ID) + + serialized = session.model_dump() + + assert "token" not in serialized + assert API_TOKEN not in str(serialized) + + def test_token_excluded_from_json(self): + session = VercelSession(token=API_TOKEN, team_id=TEAM_ID) + + serialized = session.json() + + assert "token" not in serialized + assert API_TOKEN not in serialized + + def test_token_excluded_from_model_dump_json(self): + session = VercelSession(token=API_TOKEN, team_id=TEAM_ID) + + serialized = session.model_dump_json() + + assert "token" not in serialized + assert API_TOKEN not in serialized + + def test_token_excluded_from_repr_and_str(self): + session = VercelSession(token=API_TOKEN, team_id=TEAM_ID) + + assert API_TOKEN not in repr(session) + assert API_TOKEN not in str(session) + assert "token" not in repr(session) diff --git a/ui/.gitignore b/ui/.gitignore index 3b905e64d8..b6c86be41e 100644 --- a/ui/.gitignore +++ b/ui/.gitignore @@ -7,6 +7,7 @@ # testing /coverage +__screenshots__/ # next.js /.next/ @@ -28,6 +29,9 @@ yarn-error.log* .env*.local .env +# Claude Code local settings +.claude/ + # vercel .vercel diff --git a/ui/.husky/pre-commit b/ui/.husky/pre-commit deleted file mode 100755 index 0755cd1872..0000000000 --- a/ui/.husky/pre-commit +++ /dev/null @@ -1,237 +0,0 @@ -#!/bin/bash - -# Prowler UI - Pre-Commit Hook -# Optionally validates ONLY staged files against AGENTS.md standards using Claude Code -# Controlled by CODE_REVIEW_ENABLED in .env - -set -e - -# The Python pre-commit framework (see .pre-commit-config.yaml, hook "ui-checks") -# exports GIT_WORK_TREE, GIT_DIR, and GIT_INDEX_FILE pointing to its temp staging -# area. Unset them so git commands below resolve against the real repo and index. -# See: https://github.com/prowler-cloud/prowler/pull/10574 -unset GIT_WORK_TREE GIT_DIR GIT_INDEX_FILE GIT_PREFIX GIT_COMMON_DIR GIT_OBJECT_DIRECTORY - -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -echo "" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -echo "🚀 Prowler UI - Pre-Commit Hook" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -echo "" - -# Load .env file (look in git root directory) -GIT_ROOT=$(git rev-parse --show-toplevel) -if [ -f "$GIT_ROOT/ui/.env" ]; then - CODE_REVIEW_ENABLED=$(grep "^CODE_REVIEW_ENABLED" "$GIT_ROOT/ui/.env" | cut -d'=' -f2 | tr -d ' ') -elif [ -f "$GIT_ROOT/.env" ]; then - CODE_REVIEW_ENABLED=$(grep "^CODE_REVIEW_ENABLED" "$GIT_ROOT/.env" | cut -d'=' -f2 | tr -d ' ') -elif [ -f ".env" ]; then - CODE_REVIEW_ENABLED=$(grep "^CODE_REVIEW_ENABLED" .env | cut -d'=' -f2 | tr -d ' ') -else - CODE_REVIEW_ENABLED="false" -fi - -# Normalize the value to lowercase -CODE_REVIEW_ENABLED=$(echo "$CODE_REVIEW_ENABLED" | tr '[:upper:]' '[:lower:]') - -echo -e "${BLUE}ℹ️ Code Review Status: ${CODE_REVIEW_ENABLED}${NC}" -echo "" - -# Get staged files in the UI folder only (what will be committed) -# Always use GIT_ROOT-relative pathspecs so detection works regardless of cwd -STAGED_FILES=$(git -C "$GIT_ROOT" diff --cached --name-only --diff-filter=ACM -- 'ui/' | grep -E '\.(tsx?|jsx?)$' || true) - -if [ "$CODE_REVIEW_ENABLED" = "true" ]; then - if [ -z "$STAGED_FILES" ]; then - echo -e "${YELLOW}⚠️ No TypeScript/JavaScript files staged to validate${NC}" - echo "" - else - echo -e "${YELLOW}🔍 Running Claude Code standards validation...${NC}" - echo "" - echo -e "${BLUE}📋 Files to validate:${NC}" - echo "$STAGED_FILES" | while IFS= read -r file; do echo " - $file"; done - echo "" - - echo -e "${BLUE}📤 Sending to Claude Code for validation...${NC}" - echo "" - - # Build prompt with full file contents - VALIDATION_PROMPT=$( - cat <<'PROMPT_EOF' -You are a code reviewer for the Prowler UI project. Analyze the full file contents of changed files below and validate they comply with AGENTS.md standards. - -**RULES TO CHECK:** -1. React Imports: NO `import * as React` or `import React, {` → Use `import { useState }` -2. TypeScript: NO union types like `type X = "a" | "b"` → Use const-based: `const X = {...} as const` -3. Tailwind: NO `var()` or hex colors in className → Use Tailwind utilities and semantic color classes. Exception: `var()` is allowed when passing colors to chart/graph components that require CSS color strings (not Tailwind classes) for their APIs. -4. cn(): Use for merging multiple classes or for conditionals (handles Tailwind conflicts with twMerge) → `cn(BUTTON_STYLES.base, BUTTON_STYLES.active, isLoading && "opacity-50")` -5. React 19: NO `useMemo`/`useCallback` without reason -6. Zod v4: Use `.min(1)` not `.nonempty()`, `z.email()` not `z.string().email()`. All inputs must be validated with Zod. -7. File Org: 1 feature = local, 2+ features = shared -8. Directives: Server Actions need "use server", clients need "use client" -9. Implement DRY, KISS principles. (example: reusable components, avoid repetition) -10. Layout must work for all the responsive breakpoints (mobile, tablet, desktop) -11. ANY types cannot be used - CRITICAL: Check for `: any` in all visible lines -12. Use the components inside components/shadcn if possible -13. Check Accessibility best practices (like alt tags in images, semantic HTML, Aria labels, etc.) - -=== FILES TO REVIEW === -PROMPT_EOF - ) - - # Add full file contents for each staged file - for file in $STAGED_FILES; do - VALIDATION_PROMPT="$VALIDATION_PROMPT - -=== FILE: $file === -$(cat "$file" 2>/dev/null || echo "Error reading file")" - done - - VALIDATION_PROMPT="$VALIDATION_PROMPT - -=== END FILES === - -**IMPORTANT: Your response MUST start with exactly one of these lines:** -STATUS: PASSED -STATUS: FAILED - -**If FAILED:** List each violation with File, Line Number, Rule Number, and Issue. -**If PASSED:** Confirm all files comply with AGENTS.md standards. - -**Start your response now with STATUS:**" - - # Send to Claude Code - if VALIDATION_OUTPUT=$(echo "$VALIDATION_PROMPT" | claude 2>&1); then - echo "$VALIDATION_OUTPUT" - echo "" - - # Check result - STRICT MODE: fail if status unclear - if echo "$VALIDATION_OUTPUT" | grep -q "^STATUS: PASSED"; then - echo "" - echo -e "${GREEN}✅ VALIDATION PASSED${NC}" - echo "" - elif echo "$VALIDATION_OUTPUT" | grep -q "^STATUS: FAILED"; then - echo "" - echo -e "${RED}❌ VALIDATION FAILED${NC}" - echo -e "${RED}Fix violations before committing${NC}" - echo "" - exit 1 - else - echo "" - echo -e "${RED}❌ VALIDATION ERROR${NC}" - echo -e "${RED}Could not determine validation status from Claude Code response${NC}" - echo -e "${YELLOW}Response must start with 'STATUS: PASSED' or 'STATUS: FAILED'${NC}" - echo "" - echo -e "${YELLOW}To bypass validation temporarily, set CODE_REVIEW_ENABLED=false in .env${NC}" - echo "" - exit 1 - fi - else - echo -e "${YELLOW}⚠️ Claude Code not available${NC}" - fi - echo "" - fi -else - echo -e "${YELLOW}⏭️ Code review disabled (CODE_REVIEW_ENABLED=false)${NC}" - echo "" -fi - -# Run healthcheck (typecheck and lint check) only if there are UI changes -if [ -z "$STAGED_FILES" ]; then - echo -e "${YELLOW}⏭️ No UI files staged, skipping healthcheck/tests/build${NC}" - echo "" - exit 0 -fi - -echo -e "${BLUE}🏥 Running healthcheck...${NC}" -echo "" - -cd "$GIT_ROOT/ui" -if pnpm run healthcheck; then - echo "" - echo -e "${GREEN}✅ Healthcheck passed${NC}" - echo "" -else - echo "" - echo -e "${RED}❌ Healthcheck failed${NC}" - echo -e "${RED}Fix type errors and linting issues before committing${NC}" - echo "" - exit 1 -fi - -# Run unit tests (targeted based on staged files) -echo -e "${BLUE}🧪 Running unit tests...${NC}" -echo "" - -# Get staged source files (exclude test files) -# Use GIT_ROOT so pathspecs are always correct regardless of cwd -STAGED_SOURCE_FILES=$(git -C "$GIT_ROOT" diff --cached --name-only --diff-filter=ACM -- 'ui/*.ts' 'ui/*.tsx' | sed 's|^ui/||' | grep -v '\.test\.\|\.spec\.\|vitest\.config\|vitest\.setup' || true) - -# Check if critical paths changed (lib/, types/, config/) -CRITICAL_PATHS_CHANGED=$(git -C "$GIT_ROOT" diff --cached --name-only -- 'ui/lib/' 'ui/types/' 'ui/config/' 'ui/middleware.ts' 'ui/vitest.config.ts' 'ui/vitest.setup.ts' || true) - -if [ -n "$CRITICAL_PATHS_CHANGED" ]; then - echo -e "${YELLOW}Critical paths changed - running ALL unit tests${NC}" - if pnpm run test:run; then - echo "" - echo -e "${GREEN}✅ Unit tests passed${NC}" - echo "" - else - echo "" - echo -e "${RED}❌ Unit tests failed${NC}" - echo -e "${RED}Fix failing tests before committing${NC}" - echo "" - exit 1 - fi -elif [ -n "$STAGED_SOURCE_FILES" ]; then - echo -e "${YELLOW}Running tests related to changed files:${NC}" - echo "$STAGED_SOURCE_FILES" | while IFS= read -r file; do [ -n "$file" ] && echo " - $file"; done - echo "" - # shellcheck disable=SC2086 # Word splitting is intentional - vitest needs each file as separate arg - if pnpm exec vitest related $STAGED_SOURCE_FILES --run; then - echo "" - echo -e "${GREEN}✅ Unit tests passed${NC}" - echo "" - else - echo "" - echo -e "${RED}❌ Unit tests failed${NC}" - echo -e "${RED}Fix failing tests before committing${NC}" - echo "" - exit 1 - fi -else - echo -e "${YELLOW}No source files changed - running ALL unit tests${NC}" - if pnpm run test:run; then - echo "" - echo -e "${GREEN}✅ Unit tests passed${NC}" - echo "" - else - echo "" - echo -e "${RED}❌ Unit tests failed${NC}" - echo -e "${RED}Fix failing tests before committing${NC}" - echo "" - exit 1 - fi -fi - -# Run build -echo -e "${BLUE}🔨 Running build...${NC}" -echo "" - -if pnpm run build; then - echo "" - echo -e "${GREEN}✅ Build passed${NC}" - echo "" -else - echo "" - echo -e "${RED}❌ Build failed${NC}" - echo -e "${RED}Fix build errors before committing${NC}" - echo "" - exit 1 -fi diff --git a/ui/.pre-commit-config.yaml b/ui/.pre-commit-config.yaml index c92ea6cfd3..eab333ceea 100644 --- a/ui/.pre-commit-config.yaml +++ b/ui/.pre-commit-config.yaml @@ -3,21 +3,30 @@ orphan: true repos: - repo: local hooks: + # P0 - Formatters: write fixes on staged files; prek re-stages. + - id: ui-prettier + name: UI - Prettier (write, staged) + entry: pnpm exec prettier --write --ignore-unknown + language: system + pass_filenames: true + priority: 0 + + - id: ui-lint + name: UI - ESLint (fix, staged) + entry: pnpm exec eslint --fix --max-warnings 40 --no-warn-ignored + language: system + files: '\.(ts|tsx|js|jsx)$' + pass_filenames: true + priority: 1 + + # P10 - Project-wide validators (TypeScript is fundamentally project-wide). - id: ui-typecheck name: UI - TypeScript Check entry: pnpm run typecheck language: system files: '\.(ts|tsx|js|jsx)$' pass_filenames: false - priority: 0 - - - id: ui-lint - name: UI - ESLint - entry: pnpm run lint:check - language: system - files: '\.(ts|tsx|js|jsx)$' - pass_filenames: false - priority: 0 + priority: 10 - id: ui-tests name: UI - Unit Tests @@ -26,12 +35,4 @@ repos: files: '\.(ts|tsx|js|jsx)$' exclude: '\.test\.|\.spec\.|vitest\.config|vitest\.setup' pass_filenames: true - priority: 1 - - - id: ui-build - name: UI - Build - entry: pnpm run build - language: system - files: '\.(ts|tsx|js|jsx|json|css)$' - pass_filenames: false - priority: 2 + priority: 10 diff --git a/ui/.prettierignore b/ui/.prettierignore index 40b878db5b..5377d96f2d 100644 --- a/ui/.prettierignore +++ b/ui/.prettierignore @@ -1 +1,19 @@ -node_modules/ \ No newline at end of file +# Dependencies +node_modules/ + +# Build outputs +.next/ +.now/ +build/ +coverage/ +dist/ +esm/ + +# Generated files +next-env.d.ts +public/mockServiceWorker.js + +# Lockfiles +package-lock.json +pnpm-lock.yaml +yarn.lock diff --git a/ui/.prettierrc.json b/ui/.prettierrc.json index 0cbbafa6cc..643c55095f 100644 --- a/ui/.prettierrc.json +++ b/ui/.prettierrc.json @@ -6,5 +6,5 @@ "useTabs": false, "semi": true, "printWidth": 80, - "plugins": ["prettier-plugin-tailwindcss"] + "plugins": ["prettier-plugin-packagejson", "prettier-plugin-tailwindcss"] } diff --git a/ui/AGENTS.md b/ui/AGENTS.md index 40cf77dc1a..55b62045ac 100644 --- a/ui/AGENTS.md +++ b/ui/AGENTS.md @@ -1,11 +1,12 @@ # Prowler UI - AI Agent Ruleset > **Skills Reference**: For detailed patterns, use these skills: +> > - [`prowler-ui`](../skills/prowler-ui/SKILL.md) - Prowler-specific UI patterns > - [`prowler-test-ui`](../skills/prowler-test-ui/SKILL.md) - Playwright E2E testing (comprehensive) > - [`typescript`](../skills/typescript/SKILL.md) - Const types, flat interfaces > - [`react-19`](../skills/react-19/SKILL.md) - No useMemo/useCallback, compiler -> - [`nextjs-15`](../skills/nextjs-15/SKILL.md) - App Router, Server Actions +> - [`nextjs-16`](../skills/nextjs-16/SKILL.md) - App Router, Server Actions > - [`tailwind-4`](../skills/tailwind-4/SKILL.md) - cn() utility, no var() in className > - [`zod-4`](../skills/zod-4/SKILL.md) - New API (z.email(), z.uuid()) > - [`zustand-5`](../skills/zustand-5/SKILL.md) - Selectors, persist middleware @@ -14,39 +15,39 @@ > - [`vitest`](../skills/vitest/SKILL.md) - Unit testing with React Testing Library > - [`tdd`](../skills/tdd/SKILL.md) - TDD workflow (MANDATORY for UI tasks) -### Auto-invoke Skills +## Auto-invoke Skills When performing these actions, ALWAYS invoke the corresponding skill FIRST: -| Action | Skill | -|--------|-------| -| Add changelog entry for a PR or feature | `prowler-changelog` | -| App Router / Server Actions | `nextjs-15` | -| Building AI chat features | `ai-sdk-5` | -| Committing changes | `prowler-commit` | -| Create PR that requires changelog entry | `prowler-changelog` | -| Creating Zod schemas | `zod-4` | -| Creating a git commit | `prowler-commit` | -| Creating/modifying Prowler UI components | `prowler-ui` | -| Fixing bug | `tdd` | -| Implementing feature | `tdd` | -| Modifying component | `tdd` | -| Refactoring code | `tdd` | -| Review changelog format and conventions | `prowler-changelog` | -| Testing hooks or utilities | `vitest` | -| Update CHANGELOG.md in any component | `prowler-changelog` | -| Using Zustand stores | `zustand-5` | -| Working on Prowler UI structure (actions/adapters/types/hooks) | `prowler-ui` | -| Working on task | `tdd` | -| Working with Prowler UI test helpers/pages | `prowler-test-ui` | -| Working with Tailwind classes | `tailwind-4` | -| Writing Playwright E2E tests | `playwright` | -| Writing Prowler UI E2E tests | `prowler-test-ui` | -| Writing React component tests | `vitest` | -| Writing React components | `react-19` | -| Writing TypeScript types/interfaces | `typescript` | -| Writing Vitest tests | `vitest` | -| Writing unit tests for UI | `vitest` | +| Action | Skill | +| -------------------------------------------------------------- | ------------------- | +| Add changelog entry for a PR or feature | `prowler-changelog` | +| App Router / Server Actions | `nextjs-16` | +| Building AI chat features | `ai-sdk-5` | +| Committing changes | `prowler-commit` | +| Create PR that requires changelog entry | `prowler-changelog` | +| Creating Zod schemas | `zod-4` | +| Creating a git commit | `prowler-commit` | +| Creating/modifying Prowler UI components | `prowler-ui` | +| Fixing bug | `tdd` | +| Implementing feature | `tdd` | +| Modifying component | `tdd` | +| Refactoring code | `tdd` | +| Review changelog format and conventions | `prowler-changelog` | +| Testing hooks or utilities | `vitest` | +| Update CHANGELOG.md in any component | `prowler-changelog` | +| Using Zustand stores | `zustand-5` | +| Working on Prowler UI structure (actions/adapters/types/hooks) | `prowler-ui` | +| Working on task | `tdd` | +| Working with Prowler UI test helpers/pages | `prowler-test-ui` | +| Working with Tailwind classes | `tailwind-4` | +| Writing Playwright E2E tests | `playwright` | +| Writing Prowler UI E2E tests | `prowler-test-ui` | +| Writing React component tests | `vitest` | +| Writing React components | `react-19` | +| Writing TypeScript types/interfaces | `typescript` | +| Writing Vitest tests | `vitest` | +| Writing unit tests for UI | `vitest` | --- @@ -88,7 +89,7 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST: ### Component Placement -``` +```text New/Existing UI? → shadcn/ui + Tailwind (NEVER HeroUI for new code) Used 1 feature? → features/{feature}/components | Used 2+? → components/{domain}/ Needs state/hooks? → "use client" | Server component? → No directive @@ -96,7 +97,7 @@ Needs state/hooks? → "use client" | Server component? → No directive ### Code Location -``` +```text Server action → actions/{feature}/{feature}.ts Data transform → actions/{feature}/{feature}.adapter.ts Types (shared 2+) → types/{domain}.ts | Types (local 1) → {feature}/types.ts @@ -137,8 +138,8 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; const schema = z.object({ - email: z.email(), // Zod 4: z.email() not z.string().email() - id: z.uuid(), // Zod 4: z.uuid() not z.string().uuid() + email: z.email(), // Zod 4: z.email() not z.string().email() + id: z.uuid(), // Zod 4: z.uuid() not z.string().uuid() }); const form = useForm({ resolver: zodResolver(schema) }); @@ -163,8 +164,12 @@ const useStore = create( ```typescript export class FeaturePage extends BasePage { readonly submitBtn = this.page.getByRole("button", { name: "Submit" }); - async goto() { await super.goto("/path"); } - async submit() { await this.submitBtn.click(); } + async goto() { + await super.goto("/path"); + } + async submit() { + await this.submitBtn.click(); + } } test("action works", { tag: ["@critical", "@feature"] }, async ({ page }) => { @@ -179,7 +184,7 @@ test("action works", { tag: ["@critical", "@feature"] }, async ({ page }) => { ## TECH STACK -Next.js 15.5.9 | React 19.2.2 | Tailwind 4.1.13 | shadcn/ui +Next.js 16.2.3 | React 19.2.5 | Tailwind 4.1.18 | shadcn/ui Zod 4.1.11 | React Hook Form 7.62.0 | Zustand 5.0.8 | NextAuth 5.0.0-beta.30 | Recharts 2.15.4 > **Note**: HeroUI exists in `components/ui/` as legacy code. Do NOT add new components there. @@ -188,7 +193,7 @@ Zod 4.1.11 | React Hook Form 7.62.0 | Zustand 5.0.8 | NextAuth 5.0.0-beta.30 | R ## PROJECT STRUCTURE -``` +```text ui/ ├── app/(auth)/ # Auth pages ├── app/(prowler)/ # Main app: compliance, findings, providers, scans @@ -226,5 +231,6 @@ pnpm run test:e2e:ui - [ ] Relevant E2E tests pass - [ ] All UI states handled (loading, error, empty) - [ ] No secrets in code (use `.env.local`) +- [ ] New npm dependencies include package-health evidence (maintenance, popularity, known vulnerabilities, license, release age) and a rationale for not using existing/native alternatives. - [ ] Error messages sanitized - [ ] Server-side validation present diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index 8b0f8d17e6..d0bbe038df 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -2,6 +2,102 @@ All notable changes to the **Prowler UI** are documented in this file. +## [1.29.0] (Prowler UNRELEASED) + +### 🔄 Changed + +- Dark mode: pure-black canvas, pure-white primary text, and brighter border / input tokens for clearer separation between cards, tables, and inputs [(#11073)](https://github.com/prowler-cloud/prowler/pull/11073) + +### 🐞 Fixed + +- Compliance page now loads the most recent scan when opened from the sidebar instead of showing the "no compliance data available" alert [(#11374)](https://github.com/prowler-cloud/prowler/pull/11374) + +--- + +## [1.28.1] (Prowler v5.28.1) + +### 🐞 Fixed + +- Large scan report ZIP downloads now stream through a Next.js Route Handler instead of buffering the full file in a Server Action [(#11330)](https://github.com/prowler-cloud/prowler/pull/11330) +- Compliance requirement findings table now respects the page size selector [(#11365)](https://github.com/prowler-cloud/prowler/pull/11365) + +--- + +## [1.28.0] (Prowler v5.28.0) + +### 🚀 Added + +- `okta` provider support with OAuth 2.0 private-key JWT credentials form (client ID + PEM private key) [(#11213)](https://github.com/prowler-cloud/prowler/pull/11213) +- "Resource Metadata / Evidence" tab in the finding detail drawer [(#11187)](https://github.com/prowler-cloud/prowler/pull/11187) + +### 🐞 Fixed + +- Resource detail panels: metadata editor now scrolls internally with the minimal scrollbar across the finding drawer and `/resources/:id`, tab labels truncate with tooltips on narrow widths, and "View in AWS Console" moved from the resource UID row to the resource actions menu [(#11325)](https://github.com/prowler-cloud/prowler/pull/11325) + +--- + +## [1.27.0] (Prowler v5.27.0) + +### 🚀 Added + +- Health endpoint at `GET /api/health` for Docker Compose liveness checks [(#11145)](https://github.com/prowler-cloud/prowler/pull/11145) +- AWS findings and resource details now expose a "View in AWS Console" link that opens the resource directly in the AWS Console via the universal `/go/view` ARN resolver [(#9172)](https://github.com/prowler-cloud/prowler/pull/9172) +- Lighthouse AI: Prowler App Finding Groups MCP tools [(#11140)](https://github.com/prowler-cloud/prowler/pull/11140) + +### 🔄 Changed + +- Trimmed unused `npm` dependencies [(#11115)](https://github.com/prowler-cloud/prowler/pull/11115) +- Faster, stricter pre-commit: prek lints and formats only staged UI files (husky removed), with Prettier and ESLint (`--max-warnings 40`, stale-disable detection) now covering the full UI workspace, including `public/` assets [(#11118)](https://github.com/prowler-cloud/prowler/pull/11118) +- Attack Paths graph now uses React Flow with improved layout, interactions, export, minimap, and browser test coverage [(#10686)](https://github.com/prowler-cloud/prowler/pull/10686) +- SAML ACS URL is only shown if the email domain is configured [(#11144)](https://github.com/prowler-cloud/prowler/pull/11144) +- "View Resource" action in the finding resource detail drawer is now an icon-only link rendered next to the resource name (instead of a text button in the UID row), keeping the "View in AWS Console" link unchanged [(#11193)](https://github.com/prowler-cloud/prowler/pull/11193) + +### 🐞 Fixed + +- Mute Findings modal now enforces the 100-character limit on the rule name input with a live counter and inline error, matching the existing reason field behaviour [(#11158)](https://github.com/prowler-cloud/prowler/pull/11158) +- Finding drawer no longer renders literal backticks around inline code in Risk, Description and Remediation sections [(#11142)](https://github.com/prowler-cloud/prowler/pull/11142) +- Launch Scan first-provider wizard continues after provider creation instead of resetting the Scans page [(#11136)](https://github.com/prowler-cloud/prowler/pull/11136) +- Attack Paths graph nodes now wrap long resource and finding labels, indicate truncated values with `…`, and show the full value in an immediate tooltip [(#11197)](https://github.com/prowler-cloud/prowler/pull/11197) + +### 🔐 Security + +- `npm` dependencies updated to patched versions for Next.js, Vite, LangChain, XML parsing, lodash, and related transitive packages [(#11173)](https://github.com/prowler-cloud/prowler/pull/11173) +- Hardened `npm` supply chain controls [(#11157)](https://github.com/prowler-cloud/prowler/pull/11157) + +--- + +## [1.26.1] (Prowler 5.26.1) + +### 🐞 Fixed + +- Role form Cancel buttons now return to Roles [(#11125)](https://github.com/prowler-cloud/prowler/pull/11125) +- Shared select dropdowns stay constrained and scrollable inside modals [(#11125)](https://github.com/prowler-cloud/prowler/pull/11125) + +--- + +## [1.26.0] (Prowler v5.26.0) + +### 🚀 Added + +- ASD Essential Eight compliance framework support [(#11071)](https://github.com/prowler-cloud/prowler/pull/11071) + +### 🔄 Changed + +- Standardized "Providers" wording across UI and documentation, replacing legacy "Cloud Providers" / "Accounts" / "Account Groups" copy [(#10971)](https://github.com/prowler-cloud/prowler/pull/10971) +- Finding detail drawer now labels remediation actions from finding-level recommendation URLs by destination: "View CVE", "View in Prowler Hub", "View Advisory", or "View Reference", while keeping URL-only remediation cards labeled [(#10853)](https://github.com/prowler-cloud/prowler/pull/10853) +- Finding detail drawer reorganized: status-colored banner below the resource info, dedicated Remediation tab, renamed "Findings for this resource" tab, and inline View Resource link next to the resource UID [(#11091)](https://github.com/prowler-cloud/prowler/pull/11091) +- ThreatScore compliance views: canonical pillar order across all charts and the accordion, clickable pillars on `/compliance` that anchor the detail page, Top Failed Sections always shows the full pillar set, and donut tooltip now triggers on every segment [(#10975)](https://github.com/prowler-cloud/prowler/pull/10975) + +--- + +## [1.25.3] (Prowler v5.25.3) + +### 🐞 Fixed + +- CLI command in the finding drawer no longer renders the line-number gutter, matching the original styled block while removing the leading `1` [(#11059)](https://github.com/prowler-cloud/prowler/pull/11059) + +--- + ## [1.25.2] (Prowler v5.25.2) ### 🔄 Changed @@ -30,7 +126,7 @@ All notable changes to the **Prowler UI** are documented in this file. ### 🔄 Changed - Redesign compliance page, client-side search for compliance frameworks, compact scan selector trigger, enhanced compliance cards [(#10767)](https://github.com/prowler-cloud/prowler/pull/10767) -- Allows tenant owners to expel users from their organizations [(#10787)](https://github.com/prowler-cloud/prowler/pull/10787) +- Allows tenant owners to expel users from their organizations [(#10787)](https://github.com/prowler-cloud/prowler/pull/10787) - Shared filter dropdowns now support local option search and auto-scroll to the first visible match across table and provider filters [(#10859)](https://github.com/prowler-cloud/prowler/pull/10859) - Backward-compatibility middleware redirect from `/sign-up?invitation_token=…` to `/invitation/accept?invitation_token=…`; new invitation emails use `/invitation/accept` directly [(#10797)](https://github.com/prowler-cloud/prowler/pull/10797) - Mutelist improvements: table now supports name/reason search and visual count badges for finding targets [(#10846)](https://github.com/prowler-cloud/prowler/pull/10846) diff --git a/ui/README.md b/ui/README.md index b1af73ffe3..998dc84a37 100644 --- a/ui/README.md +++ b/ui/README.md @@ -2,10 +2,12 @@ This repository hosts the UI component for Prowler, providing a user-friendly web interface to interact seamlessly with Prowler's features. - ## 🚀 Production deployment + ### Docker deployment + #### Clone the repository + ```console # HTTPS git clone https://github.com/prowler-cloud/ui.git @@ -14,16 +16,21 @@ git clone https://github.com/prowler-cloud/ui.git git clone git@github.com:prowler-cloud/ui.git ``` + #### Build the Docker image + ```bash docker build -t prowler-cloud/ui . --target prod ``` + #### Run the Docker container + ```bash docker run -p 3000:3000 prowler-cloud/ui ``` ### Local deployment + #### Clone the repository ```console @@ -48,8 +55,11 @@ pnpm start ``` ## 🧪 Development deployment + ### Docker deployment + #### Clone the repository + ```console # HTTPS git clone https://github.com/prowler-cloud/ui.git @@ -58,16 +68,21 @@ git clone https://github.com/prowler-cloud/ui.git git clone git@github.com:prowler-cloud/ui.git ``` + #### Build the Docker image + ```bash docker build -t prowler-cloud/ui . --target dev ``` + #### Run the Docker container + ```bash docker run -p 3000:3000 prowler-cloud/ui ``` ### Local deployment + #### Clone the repository ```console @@ -107,47 +122,12 @@ pnpm run dev - [Framer Motion](https://www.framer.com/motion/) - [next-themes](https://github.com/pacocoursey/next-themes) -## Git Hooks & Code Review +## Git Hooks -This project uses Git hooks to maintain code quality. When you commit changes to TypeScript/JavaScript files, the pre-commit hook can optionally validate them against our coding standards using Claude Code. - -### Enabling Code Review - -To enable automatic code review on commits, add this to your `.env` file in the project root: +The UI uses [prek](https://github.com/j178/prek) for pre-commit checks, configured in [`.pre-commit-config.yaml`](.pre-commit-config.yaml). `pnpm install` runs the postinstall script that installs hooks automatically. To re-install manually: ```bash -CODE_REVIEW_ENABLED=true -``` - -When enabled, the hook will: -- ✅ Validate your staged changes against `AGENTS.md` standards -- ✅ Check for common issues (any types, incorrect imports, styling violations, etc.) -- ✅ Block commits that don't comply with the standards -- ✅ Provide helpful feedback on how to fix issues - -### Disabling Code Review - -To disable code review (faster commits, useful for quick iterations): - -```bash -CODE_REVIEW_ENABLED=false -``` - -Or remove the variable from your `.env` file. - -### Requirements - -- [Claude Code CLI](https://github.com/anthropics/claude-code) installed and authenticated -- `.env` file in the project root with `CODE_REVIEW_ENABLED` set - -### Troubleshooting - -If hooks aren't running after commits, verify prek is installed and hooks are set up: - -```bash -# Check prek is available -prek --version - -# Re-install hooks if needed prek install --overwrite ``` + +On each commit, prek runs Prettier and ESLint against the staged files, plus a project-wide TypeScript check and the unit tests related to the staged changes. The full Next.js build runs in CI, not on commit. diff --git a/ui/__tests__/mockServiceWorker.test.ts b/ui/__tests__/mockServiceWorker.test.ts new file mode 100644 index 0000000000..e89265c8ac --- /dev/null +++ b/ui/__tests__/mockServiceWorker.test.ts @@ -0,0 +1,18 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +import { describe, expect, it } from "vitest"; + +describe("mock service worker message hardening", () => { + it("rejects messages from unexpected origins before handling client messages", () => { + const workerSource = readFileSync( + join(process.cwd(), "public/mockServiceWorker.js"), + "utf8", + ); + + expect(workerSource).toContain("event.origin !== self.location.origin"); + expect( + workerSource.indexOf("event.origin !== self.location.origin"), + ).toBeLessThan(workerSource.indexOf("const clientId = Reflect.get")); + }); +}); diff --git a/ui/__tests__/msw/handlers/attack-paths.ts b/ui/__tests__/msw/handlers/attack-paths.ts new file mode 100644 index 0000000000..b17c6c0596 --- /dev/null +++ b/ui/__tests__/msw/handlers/attack-paths.ts @@ -0,0 +1,231 @@ +import { http, HttpResponse } from "msw"; + +import type { PageFixture } from "@/app/(prowler)/attack-paths/(workflow)/query-builder/attack-paths-page.fixtures"; +import type { + AttackPathQueriesResponse, + AttackPathQuery, + AttackPathQueryResult, + AttackPathScan, + AttackPathScansResponse, + QueryResultAttributes, +} from "@/types/attack-paths"; + +const API = process.env.NEXT_PUBLIC_API_BASE_URL!; + +type JsonApiErrorBody = { + errors: Array<{ detail: string; status: string }>; +}; + +const toScansApiResponse = ( + scans: AttackPathScan[], +): AttackPathScansResponse => ({ + data: scans, + links: { + first: `${API}/attack-paths-scans?page=1`, + last: `${API}/attack-paths-scans?page=1`, + next: null, + prev: null, + }, +}); + +const toQueriesApiResponse = ( + queries: AttackPathQuery[], +): AttackPathQueriesResponse => ({ + data: queries, +}); + +const toQueryResultApiResponse = ( + attrs: QueryResultAttributes, + queryId: string, +): AttackPathQueryResult => ({ + data: { + type: "attack-paths-query-run-requests", + id: queryId, + attributes: attrs, + }, +}); + +const toErrorBody = (detail: string, status: number): JsonApiErrorBody => ({ + errors: [{ detail, status: String(status) }], +}); + +const toFindingApiResponse = (fx: PageFixture, findingId: string) => { + const findingNode = fx.queryResult?.nodes.find( + (node) => node.id === findingId, + ); + const resourceNode = fx.queryResult?.nodes.find((node) => + fx.queryResult?.relationships?.some( + (rel) => + (rel.source === node.id && rel.target === findingId) || + (rel.target === node.id && rel.source === findingId), + ), + ); + const scan = fx.scans[0]; + const providerId = scan?.relationships?.provider?.data?.id ?? "provider-1"; + const resourceId = resourceNode?.id ?? "resource-1"; + + return { + data: { + type: "findings", + id: findingId, + attributes: { + uid: String(findingNode?.properties.id ?? findingId), + delta: null, + status: String(findingNode?.properties.status ?? "FAIL"), + status_extended: "Status extended", + severity: String(findingNode?.properties.severity ?? "critical"), + check_id: "attack_path_check", + muted: false, + muted_reason: null, + check_metadata: { + risk: "High", + notes: "", + checkid: "attack_path_check", + provider: "aws", + severity: String(findingNode?.properties.severity ?? "critical"), + checktype: [], + dependson: [], + relatedto: [], + categories: ["security"], + checktitle: String( + findingNode?.properties.check_title ?? "Attack path finding", + ), + compliance: null, + relatedurl: "", + description: "Attack path finding description", + remediation: { + code: { cli: "", other: "", nativeiac: "", terraform: "" }, + recommendation: { url: "", text: "Fix the finding" }, + }, + additionalurls: [], + servicename: String(resourceNode?.properties.service ?? "s3"), + checkaliases: [], + resourcetype: String(resourceNode?.labels[0] ?? "Resource"), + subservicename: "", + resourceidtemplate: "", + }, + raw_result: null, + inserted_at: "2026-04-21T10:00:00Z", + updated_at: "2026-04-21T10:05:00Z", + first_seen_at: null, + }, + relationships: { + resources: { data: [{ type: "resources", id: resourceId }] }, + scan: { data: { type: "scans", id: scan?.id ?? "scan-1" } }, + }, + }, + included: [ + { + type: "resources", + id: resourceId, + attributes: { + uid: String(resourceNode?.properties.arn ?? resourceId), + name: String(resourceNode?.properties.name ?? resourceId), + region: "us-east-1", + service: String(resourceNode?.properties.service ?? "s3"), + tags: {}, + type: String(resourceNode?.labels[0] ?? "Resource"), + inserted_at: "2026-04-21T10:00:00Z", + updated_at: "2026-04-21T10:05:00Z", + details: null, + partition: null, + }, + }, + { + type: "scans", + id: scan?.id ?? "scan-1", + attributes: { + name: "Attack path scan", + trigger: "manual", + state: scan?.attributes.state ?? "completed", + unique_resource_count: 1, + progress: scan?.attributes.progress ?? 100, + duration: scan?.attributes.duration ?? 0, + started_at: scan?.attributes.started_at ?? "2026-04-21T10:00:00Z", + inserted_at: scan?.attributes.inserted_at ?? "2026-04-21T10:00:00Z", + completed_at: scan?.attributes.completed_at ?? "2026-04-21T10:05:00Z", + scheduled_at: null, + next_scan_at: "", + }, + relationships: { + provider: { data: { type: "providers", id: providerId } }, + }, + }, + { + type: "providers", + id: providerId, + attributes: { + provider: scan?.attributes.provider_type ?? "aws", + uid: scan?.attributes.provider_uid ?? "123456789", + alias: scan?.attributes.provider_alias ?? "Provider", + connection: { + connected: true, + last_checked_at: "2026-04-21T10:00:00Z", + }, + inserted_at: "2026-04-21T10:00:00Z", + updated_at: "2026-04-21T10:05:00Z", + }, + }, + ], + }; +}; + +export const handlersForFixture = (fx: PageFixture) => [ + http.get(`${API}/attack-paths-scans`, () => + HttpResponse.json(toScansApiResponse(fx.scans)), + ), + + http.get<{ scanId: string }>( + `${API}/attack-paths-scans/:scanId/queries`, + () => + HttpResponse.json( + toQueriesApiResponse(fx.queries), + ), + ), + + http.post<{ scanId: string }>( + `${API}/attack-paths-scans/:scanId/queries/run`, + () => { + if (fx.queryError) { + return HttpResponse.json( + toErrorBody(fx.queryError.error, fx.queryError.status), + { status: fx.queryError.status }, + ); + } + if (!fx.queryResult) { + return HttpResponse.json( + toErrorBody("No data found", 404), + { status: 404 }, + ); + } + return HttpResponse.json( + toQueryResultApiResponse(fx.queryResult, fx.queryId), + ); + }, + ), + + http.post<{ scanId: string }>( + `${API}/attack-paths-scans/:scanId/queries/custom`, + () => { + if (fx.queryError) { + return HttpResponse.json( + toErrorBody(fx.queryError.error, fx.queryError.status), + { status: fx.queryError.status }, + ); + } + if (!fx.queryResult) { + return HttpResponse.json( + toErrorBody("No data found", 404), + { status: 404 }, + ); + } + return HttpResponse.json( + toQueryResultApiResponse(fx.queryResult, fx.queryId), + ); + }, + ), + + http.get<{ findingId: string }>(`${API}/findings/:findingId`, ({ params }) => + HttpResponse.json(toFindingApiResponse(fx, params.findingId)), + ), +]; diff --git a/ui/__tests__/msw/handlers/index.ts b/ui/__tests__/msw/handlers/index.ts new file mode 100644 index 0000000000..5859c31b8d --- /dev/null +++ b/ui/__tests__/msw/handlers/index.ts @@ -0,0 +1,13 @@ +import type { HttpHandler } from "msw"; + +/** + * Static handlers shared by every browser test — registered as defaults on + * the worker. Use this list for endpoints whose response doesn't change + * across tests (e.g. `/users/me`, `/tenants/current`, health checks). + * + * Per-domain dynamic handlers that depend on fixture data live in their own + * files alongside this index (e.g. `./attack-paths.ts`) and are imported + * directly by the tests that need them, then wired via + * `worker.use(...handlersForFixture(fx))`. + */ +export const handlers: HttpHandler[] = []; diff --git a/ui/__tests__/msw/worker.ts b/ui/__tests__/msw/worker.ts new file mode 100644 index 0000000000..318d6f4a03 --- /dev/null +++ b/ui/__tests__/msw/worker.ts @@ -0,0 +1,5 @@ +import { setupWorker } from "msw/browser"; + +import { handlers } from "./handlers"; + +export const worker = setupWorker(...handlers); diff --git a/ui/__tests__/render-browser.tsx b/ui/__tests__/render-browser.tsx new file mode 100644 index 0000000000..5ae56d4fec --- /dev/null +++ b/ui/__tests__/render-browser.tsx @@ -0,0 +1,25 @@ +import type { ComponentType, PropsWithChildren, ReactElement } from "react"; +import { render as vitestRender } from "vitest-browser-react"; + +const TestProviders = ({ children }: PropsWithChildren) => <>{children}; + +type RenderOptions = Parameters[1]; + +export function render(ui: ReactElement, options?: RenderOptions) { + const userWrapper = options?.wrapper as + | ComponentType + | undefined; + + const Wrapper = userWrapper + ? ({ children }: PropsWithChildren) => { + const Inner = userWrapper; + return ( + + {children} + + ); + } + : TestProviders; + + return vitestRender(ui, { ...options, wrapper: Wrapper }); +} diff --git a/ui/actions/attack-paths/query-result.adapter.ts b/ui/actions/attack-paths/query-result.adapter.ts index 65b33843af..93bf38aca0 100644 --- a/ui/actions/attack-paths/query-result.adapter.ts +++ b/ui/actions/attack-paths/query-result.adapter.ts @@ -131,27 +131,16 @@ export function adaptQueryResultToGraphData( // Populate findings and resources based on HAS_FINDING edges edges.forEach((edge) => { if (edge.type === "HAS_FINDING") { - const sourceId = - typeof edge.source === "string" - ? edge.source - : (edge.source as { id?: string })?.id; - const targetId = - typeof edge.target === "string" - ? edge.target - : (edge.target as { id?: string })?.id; + // Add finding to source node (resource -> finding) + const sourceNode = normalizedNodes.find((n) => n.id === edge.source); + if (sourceNode) { + sourceNode.findings.push(edge.target); + } - if (sourceId && targetId) { - // Add finding to source node (resource -> finding) - const sourceNode = normalizedNodes.find((n) => n.id === sourceId); - if (sourceNode) { - sourceNode.findings.push(targetId); - } - - // Add resource to target node (finding <- resource) - const targetNode = normalizedNodes.find((n) => n.id === targetId); - if (targetNode) { - targetNode.resources.push(sourceId); - } + // Add resource to target node (finding <- resource) + const targetNode = normalizedNodes.find((n) => n.id === edge.target); + if (targetNode) { + targetNode.resources.push(edge.source); } } }); diff --git a/ui/actions/auth/auth.ts b/ui/actions/auth/auth.ts index 8b777cad10..ca6fd2d099 100644 --- a/ui/actions/auth/auth.ts +++ b/ui/actions/auth/auth.ts @@ -172,6 +172,7 @@ export const getUserByMe = async (accessToken: string) => { manage_scans: userRole.attributes.manage_scans || false, manage_integrations: userRole.attributes.manage_integrations || false, manage_billing: userRole.attributes.manage_billing || false, + manage_alerts: userRole.attributes.manage_alerts || false, unlimited_visibility: userRole.attributes.unlimited_visibility || false, }; diff --git a/ui/actions/finding-groups/finding-groups.adapter.ts b/ui/actions/finding-groups/finding-groups.adapter.ts index 7f260c8aa6..6b78bc189a 100644 --- a/ui/actions/finding-groups/finding-groups.adapter.ts +++ b/ui/actions/finding-groups/finding-groups.adapter.ts @@ -139,6 +139,7 @@ interface FindingGroupResourceAttributes { resource: ResourceInfo; provider: ProviderInfo; status: string; + status_extended?: string; muted?: boolean; delta?: string | null; severity: string; @@ -187,6 +188,7 @@ export function adaptFindingGroupResourcesResponse( region: item.attributes.resource?.region || "-", severity: (item.attributes.severity || "informational") as Severity, status: item.attributes.status, + statusExtended: item.attributes.status_extended, delta: item.attributes.delta || null, isMuted: item.attributes.muted ?? item.attributes.status === "MUTED", mutedReason: item.attributes.muted_reason || undefined, diff --git a/ui/actions/findings/findings-by-resource.adapter.test.ts b/ui/actions/findings/findings-by-resource.adapter.test.ts index 87d37c192c..a94f34a9c4 100644 --- a/ui/actions/findings/findings-by-resource.adapter.test.ts +++ b/ui/actions/findings/findings-by-resource.adapter.test.ts @@ -116,6 +116,84 @@ describe("adaptFindingsByResourceResponse — malformed input", () => { expect(result[0].checkId).toBe("s3_check"); }); + it("should extract resource metadata and details from the included resource", () => { + // Given — finding with an included resource exposing metadata + details + createDictMock.mockImplementation((type: string) => + type === "resources" + ? { + "resource-1": { + id: "resource-1", + attributes: { + uid: "image:python:3.12", + name: "python", + type: "Python", + details: "Python 3.12 base image", + metadata: '{"PkgName":"requests","Versions":["2.0"]}', + }, + }, + } + : {}, + ); + + const input = { + data: { + id: "finding-1", + attributes: { + uid: "uid-1", + check_id: "image_vulnerability", + status: "FAIL", + severity: "critical", + check_metadata: { checktitle: "Image Vulnerability" }, + }, + relationships: { + resources: { data: [{ id: "resource-1" }] }, + scan: { data: null }, + }, + }, + included: [], + }; + + // When + const result = adaptFindingsByResourceResponse(input); + + // Then + expect(result).toHaveLength(1); + expect(result[0].resourceDetails).toBe("Python 3.12 base image"); + expect(result[0].resourceMetadata).toBe( + '{"PkgName":"requests","Versions":["2.0"]}', + ); + }); + + it("should default resource metadata and details to null when absent", () => { + // Given — valid finding without an included resource + const input = { + data: [ + { + id: "finding-1", + attributes: { + uid: "uid-1", + check_id: "s3_check", + status: "FAIL", + severity: "critical", + check_metadata: { checktitle: "S3 Check" }, + }, + relationships: { + resources: { data: [] }, + scan: { data: null }, + }, + }, + ], + included: [], + }; + + // When + const result = adaptFindingsByResourceResponse(input); + + // Then + expect(result[0].resourceDetails).toBeNull(); + expect(result[0].resourceMetadata).toBeNull(); + }); + it("should normalize a single finding response into a one-item drawer array", () => { // Given — getFindingById returns a single JSON:API resource object const input = { diff --git a/ui/actions/findings/findings-by-resource.adapter.ts b/ui/actions/findings/findings-by-resource.adapter.ts index d1685eaa9f..0312df00da 100644 --- a/ui/actions/findings/findings-by-resource.adapter.ts +++ b/ui/actions/findings/findings-by-resource.adapter.ts @@ -57,6 +57,8 @@ export interface ResourceDrawerFinding { resourceRegion: string; resourceType: string; resourceGroup: string; + resourceDetails: string | null; + resourceMetadata: Record | string | null; // Provider providerType: ProviderType; providerAlias: string; @@ -260,6 +262,14 @@ export function adaptFindingsByResourceResponse( resourceRegion: (resourceAttrs.region as string | undefined) || "-", resourceType: (resourceAttrs.type as string | undefined) || "-", resourceGroup: (meta.resourcegroup as string | undefined) || "-", + resourceDetails: + (resourceAttrs.details as string | null | undefined) ?? null, + resourceMetadata: + (resourceAttrs.metadata as + | Record + | string + | null + | undefined) ?? null, // Provider providerType: ((providerAttrs.provider as string | undefined) || "aws") as ProviderType, diff --git a/ui/actions/manage-groups/manage-groups.ts b/ui/actions/manage-groups/manage-groups.ts index 8758b66f68..933dabbdbc 100644 --- a/ui/actions/manage-groups/manage-groups.ts +++ b/ui/actions/manage-groups/manage-groups.ts @@ -23,7 +23,7 @@ export const getProviderGroups = async ({ const headers = await getAuthHeaders({ contentType: false }); if (isNaN(Number(page)) || page < 1) - redirect("/providers?tab=account-groups"); + redirect("/providers?tab=provider-groups"); const url = new URL(`${apiBaseUrl}/provider-groups`); @@ -112,7 +112,7 @@ export const createProviderGroup = async (formData: FormData) => { body, }); - return await handleApiResponse(response, "/providers?tab=account-groups"); + return await handleApiResponse(response, "/providers?tab=provider-groups"); } catch (error) { handleApiError(error); } @@ -169,7 +169,7 @@ export const deleteProviderGroup = async (formData: FormData) => { if (!providerGroupId) { return { - errors: [{ detail: "Account Group ID is required." }], + errors: [{ detail: "Provider Group ID is required." }], }; } diff --git a/ui/actions/providers/providers.test.ts b/ui/actions/providers/providers.test.ts index ab3037db8b..4d56e45b4b 100644 --- a/ui/actions/providers/providers.test.ts +++ b/ui/actions/providers/providers.test.ts @@ -29,7 +29,7 @@ vi.mock("@/lib", () => ({ wait: vi.fn(), })); -vi.mock("@/lib/provider-credentials/build-crendentials", () => ({ +vi.mock("@/lib/provider-credentials/build-credentials", () => ({ buildSecretConfig: vi.fn(() => ({ secretType: "access-secret-key", secret: { key: "value" }, diff --git a/ui/actions/providers/providers.ts b/ui/actions/providers/providers.ts index 63c01332b1..010646dc34 100644 --- a/ui/actions/providers/providers.ts +++ b/ui/actions/providers/providers.ts @@ -4,7 +4,7 @@ import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; import { apiBaseUrl, getAuthHeaders, getFormValue, wait } from "@/lib"; -import { buildSecretConfig } from "@/lib/provider-credentials/build-crendentials"; +import { buildSecretConfig } from "@/lib/provider-credentials/build-credentials"; import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields"; import { appendSanitizedProviderInFilters } from "@/lib/provider-filters"; import { handleApiError, handleApiResponse } from "@/lib/server-actions-helper"; diff --git a/ui/actions/roles/roles.test.ts b/ui/actions/roles/roles.test.ts new file mode 100644 index 0000000000..1e5dfaf189 --- /dev/null +++ b/ui/actions/roles/roles.test.ts @@ -0,0 +1,109 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const { + fetchMock, + getAuthHeadersMock, + handleApiErrorMock, + handleApiResponseMock, +} = vi.hoisted(() => ({ + fetchMock: vi.fn(), + getAuthHeadersMock: vi.fn(), + handleApiErrorMock: vi.fn(), + handleApiResponseMock: vi.fn(), +})); + +vi.mock("next/cache", () => ({ + revalidatePath: vi.fn(), +})); + +vi.mock("next/navigation", () => ({ + redirect: vi.fn(), +})); + +vi.mock("@/lib", () => ({ + apiBaseUrl: "https://api.example.com/api/v1", + getAuthHeaders: getAuthHeadersMock, +})); + +vi.mock("@/lib/server-actions-helper", () => ({ + handleApiError: handleApiErrorMock, + handleApiResponse: handleApiResponseMock, +})); + +import { addRole, updateRole } from "./roles"; + +const lastRequestBody = () => { + const call = fetchMock.mock.calls.at(-1); + if (!call) throw new Error("fetch was not called"); + const [, init] = call; + return JSON.parse(String((init as RequestInit).body)); +}; + +const makeRoleFormData = () => { + const formData = new FormData(); + formData.set("name", "Alert manager"); + formData.set("manage_users", "false"); + formData.set("manage_account", "false"); + formData.set("manage_billing", "false"); + formData.set("manage_providers", "false"); + formData.set("manage_integrations", "false"); + formData.set("manage_scans", "false"); + formData.set("manage_alerts", "true"); + formData.set("unlimited_visibility", "false"); + return formData; +}; + +describe("role actions", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.stubGlobal("fetch", fetchMock); + getAuthHeadersMock.mockResolvedValue({ Authorization: "Bearer token" }); + handleApiResponseMock.mockResolvedValue({ data: { id: "role-1" } }); + handleApiErrorMock.mockReturnValue({ error: "Unexpected error" }); + fetchMock.mockResolvedValue( + new Response(JSON.stringify({ data: { id: "role-1" } }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it("includes manage_alerts when creating a role in Prowler Cloud", async () => { + // Given + vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true"); + + // When + await addRole(makeRoleFormData()); + + // Then + expect(lastRequestBody().data.attributes.manage_alerts).toBe(true); + }); + + it("omits manage_alerts when creating a role outside Prowler Cloud", async () => { + // Given + vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false"); + + // When + await addRole(makeRoleFormData()); + + // Then + expect(lastRequestBody().data.attributes).not.toHaveProperty( + "manage_alerts", + ); + }); + + it("includes manage_alerts when updating a role in Prowler Cloud", async () => { + // Given + vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true"); + + // When + await updateRole(makeRoleFormData(), "role-1"); + + // Then + expect(lastRequestBody().data.attributes.manage_alerts).toBe(true); + }); +}); diff --git a/ui/actions/roles/roles.ts b/ui/actions/roles/roles.ts index 24895e4369..645db72951 100644 --- a/ui/actions/roles/roles.ts +++ b/ui/actions/roles/roles.ts @@ -107,10 +107,12 @@ export const addRole = async (formData: FormData) => { }, }; - // Conditionally include manage_billing for cloud environment + // Conditionally include Prowler Cloud permissions. if (process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true") { payload.data.attributes.manage_billing = formData.get("manage_billing") === "true"; + payload.data.attributes.manage_alerts = + formData.get("manage_alerts") === "true"; } // Add provider groups relationships only if there are items @@ -162,10 +164,12 @@ export const updateRole = async (formData: FormData, roleId: string) => { }, }; - // Conditionally include manage_billing for cloud environments + // Conditionally include Prowler Cloud permissions. if (process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true") { payload.data.attributes.manage_billing = formData.get("manage_billing") === "true"; + payload.data.attributes.manage_alerts = + formData.get("manage_alerts") === "true"; } // Add provider groups relationships only if there are items diff --git a/ui/app/(auth)/alerts/confirm/confirm-alert-recipient.test.ts b/ui/app/(auth)/alerts/confirm/confirm-alert-recipient.test.ts new file mode 100644 index 0000000000..d95966e680 --- /dev/null +++ b/ui/app/(auth)/alerts/confirm/confirm-alert-recipient.test.ts @@ -0,0 +1,138 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { confirmAlertRecipient } from "./confirm-alert-recipient"; + +const fetchMock = vi.fn(); + +const lastFetchCall = (): { url: string; init: RequestInit } => { + const call = fetchMock.mock.calls.at(-1); + if (!call) throw new Error("fetch was not called"); + const [url, init] = call; + return { url: String(url), init: (init ?? {}) as RequestInit }; +}; + +describe("confirmAlertRecipient", () => { + beforeEach(() => { + vi.stubGlobal("fetch", fetchMock); + vi.stubEnv("NEXT_PUBLIC_API_BASE_URL", "https://api.example.com/api/v1"); + fetchMock.mockResolvedValue( + new Response( + JSON.stringify({ + state: "confirmed", + message: + "Your subscription has been confirmed. You will receive alert digests at this address.", + }), + { status: 200, headers: { "Content-Type": "application/json" } }, + ), + ); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + vi.unstubAllGlobals(); + vi.clearAllMocks(); + }); + + it("calls the public confirmation endpoint without auth headers", async () => { + // When + const result = await confirmAlertRecipient("token-1"); + + // Then + expect(result).toEqual({ + ok: true, + state: "confirmed", + message: + "Your subscription has been confirmed. You will receive alert digests at this address.", + }); + const { url, init } = lastFetchCall(); + expect(url).toBe( + "https://api.example.com/api/v1/alerts/recipients/confirm?token=token-1", + ); + expect(init).toEqual({ + headers: { Accept: "application/json" }, + cache: "no-store", + }); + }); + + it("returns the API message for invalid tokens", async () => { + // Given + fetchMock.mockResolvedValueOnce( + new Response( + JSON.stringify({ + state: "invalid_token", + message: "This link is invalid or has expired.", + }), + { status: 400, headers: { "Content-Type": "application/json" } }, + ), + ); + + // When + const result = await confirmAlertRecipient("expired-token"); + + // Then + expect(result).toEqual({ + ok: false, + state: "invalid_token", + message: "This link is invalid or has expired.", + }); + }); + + it("returns the API message for missing tokens", async () => { + // Given + fetchMock.mockResolvedValueOnce( + new Response( + JSON.stringify({ + state: "missing_token", + message: "This link is missing a token.", + }), + { status: 400, headers: { "Content-Type": "application/json" } }, + ), + ); + + // When + const result = await confirmAlertRecipient(); + + // Then + expect(result).toEqual({ + ok: false, + state: "missing_token", + message: "This link is missing a token.", + }); + expect(lastFetchCall().url).toBe( + "https://api.example.com/api/v1/alerts/recipients/confirm", + ); + }); + + it("returns the fallback message when the API base URL is missing", async () => { + // Given + vi.stubEnv("NEXT_PUBLIC_API_BASE_URL", ""); + + // When + const result = await confirmAlertRecipient("token-1"); + + // Then + expect(result).toEqual({ + ok: false, + state: "missing_api_base_url", + message: + "We could not process this confirmation link. Please try again later.", + }); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it("returns the fallback message when the request fails", async () => { + // Given + fetchMock.mockRejectedValueOnce(new Error("network down")); + + // When + const result = await confirmAlertRecipient("token-1"); + + // Then + expect(result).toEqual({ + ok: false, + state: "network_error", + message: + "We could not process this confirmation link. Please try again later.", + }); + }); +}); diff --git a/ui/app/(auth)/alerts/confirm/confirm-alert-recipient.ts b/ui/app/(auth)/alerts/confirm/confirm-alert-recipient.ts new file mode 100644 index 0000000000..b567bb4124 --- /dev/null +++ b/ui/app/(auth)/alerts/confirm/confirm-alert-recipient.ts @@ -0,0 +1,79 @@ +interface AlertConfirmApiResponse { + state?: string; + message?: string; +} + +interface AlertConfirmResult { + ok: boolean; + state: string; + message: string; +} + +const FALLBACK_CONFIRM_ERROR = + "We could not process this confirmation link. Please try again later."; + +const toMessage = (payload: unknown): string | null => { + if ( + typeof payload === "object" && + payload !== null && + "message" in payload && + typeof payload.message === "string" + ) { + return payload.message; + } + + return null; +}; + +const toState = (payload: unknown): string => { + if ( + typeof payload === "object" && + payload !== null && + "state" in payload && + typeof payload.state === "string" + ) { + return payload.state; + } + + return "unknown"; +}; + +export const confirmAlertRecipient = async ( + token?: string, +): Promise => { + const apiBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL; + if (!apiBaseUrl) { + return { + ok: false, + state: "missing_api_base_url", + message: FALLBACK_CONFIRM_ERROR, + }; + } + + const url = new URL(`${apiBaseUrl}/alerts/recipients/confirm`); + if (token) { + url.searchParams.set("token", token); + } + + try { + const response = await fetch(url.toString(), { + headers: { + Accept: "application/json", + }, + cache: "no-store", + }); + const payload = (await response.json()) as AlertConfirmApiResponse; + + return { + ok: response.ok, + state: toState(payload), + message: toMessage(payload) ?? FALLBACK_CONFIRM_ERROR, + }; + } catch { + return { + ok: false, + state: "network_error", + message: FALLBACK_CONFIRM_ERROR, + }; + } +}; diff --git a/ui/app/(auth)/alerts/confirm/page.test.tsx b/ui/app/(auth)/alerts/confirm/page.test.tsx new file mode 100644 index 0000000000..8b6203c109 --- /dev/null +++ b/ui/app/(auth)/alerts/confirm/page.test.tsx @@ -0,0 +1,81 @@ +import { render, screen } from "@testing-library/react"; +import { ReactNode } from "react"; +import { describe, expect, it, vi } from "vitest"; + +import AlertsConfirmPage from "./page"; + +const confirmAlertRecipientMock = vi.hoisted(() => vi.fn()); + +vi.mock("./confirm-alert-recipient", () => ({ + confirmAlertRecipient: confirmAlertRecipientMock, +})); + +vi.mock("@/components/auth/oss/auth-layout", () => ({ + AuthLayout: ({ title, children }: { title: string; children: ReactNode }) => ( +
    {children}
    + ), +})); + +vi.mock("@/components/shadcn", () => ({ + Button: ({ children }: { children: ReactNode }) =>
    {children}
    , +})); + +vi.mock("next/link", () => ({ + default: ({ children, href }: { children: ReactNode; href: string }) => ( + {children} + ), +})); + +describe("AlertsConfirmPage", () => { + it("shows the API message after confirming the alert recipient", async () => { + // Given + confirmAlertRecipientMock.mockResolvedValueOnce({ + ok: true, + state: "confirmed", + message: + "Your subscription has been confirmed. You will receive alert digests at this address.", + }); + + // When + render( + await AlertsConfirmPage({ + searchParams: Promise.resolve({ token: "token-1" }), + }), + ); + + // Then + expect(confirmAlertRecipientMock).toHaveBeenCalledWith("token-1"); + expect(screen.getByLabelText("Subscription confirmed")).toBeInTheDocument(); + expect( + screen.getByText( + "Your subscription has been confirmed. You will receive alert digests at this address.", + ), + ).toBeVisible(); + expect( + screen.getByRole("link", { name: "Continue to Prowler" }), + ).toHaveAttribute("href", "/"); + }); + + it("shows the subscription link title when confirmation fails", async () => { + // Given + confirmAlertRecipientMock.mockResolvedValueOnce({ + ok: false, + state: "invalid_token", + message: "This link is invalid or has expired.", + }); + + // When + render( + await AlertsConfirmPage({ + searchParams: Promise.resolve({ token: ["expired-token"] }), + }), + ); + + // Then + expect(confirmAlertRecipientMock).toHaveBeenCalledWith("expired-token"); + expect(screen.getByLabelText("Subscription link")).toBeInTheDocument(); + expect( + screen.getByText("This link is invalid or has expired."), + ).toBeVisible(); + }); +}); diff --git a/ui/app/(auth)/alerts/confirm/page.tsx b/ui/app/(auth)/alerts/confirm/page.tsx new file mode 100644 index 0000000000..3791166d60 --- /dev/null +++ b/ui/app/(auth)/alerts/confirm/page.tsx @@ -0,0 +1,40 @@ +import Link from "next/link"; + +import { AuthLayout } from "@/components/auth/oss/auth-layout"; +import { Button } from "@/components/shadcn"; + +import { confirmAlertRecipient } from "./confirm-alert-recipient"; + +interface AlertsConfirmPageProps { + searchParams: Promise<{ [key: string]: string | string[] | undefined }>; +} + +const getParamValue = ( + params: Awaited, + key: string, +): string | undefined => { + const value = params[key]; + return Array.isArray(value) ? value[0] : value; +}; + +export default async function AlertsConfirmPage({ + searchParams, +}: AlertsConfirmPageProps) { + const resolvedSearchParams = await searchParams; + const token = getParamValue(resolvedSearchParams, "token"); + const result = await confirmAlertRecipient(token); + const title = result.ok ? "Subscription confirmed" : "Subscription link"; + + return ( + +
    +

    + {result.message} +

    + +
    +
    + ); +} diff --git a/ui/app/(auth)/alerts/unsubscribe/page.test.tsx b/ui/app/(auth)/alerts/unsubscribe/page.test.tsx new file mode 100644 index 0000000000..4ffeb548b0 --- /dev/null +++ b/ui/app/(auth)/alerts/unsubscribe/page.test.tsx @@ -0,0 +1,58 @@ +import { render, screen } from "@testing-library/react"; +import { ReactNode } from "react"; +import { describe, expect, it, vi } from "vitest"; + +import AlertsUnsubscribePage from "./page"; + +const unsubscribeAlertRecipientMock = vi.hoisted(() => vi.fn()); + +vi.mock("./unsubscribe-alert-recipient", () => ({ + unsubscribeAlertRecipient: unsubscribeAlertRecipientMock, +})); + +vi.mock("@/components/auth/oss/auth-layout", () => ({ + AuthLayout: ({ title, children }: { title: string; children: ReactNode }) => ( +
    {children}
    + ), +})); + +vi.mock("@/components/shadcn", () => ({ + Button: ({ children }: { children: ReactNode }) =>
    {children}
    , +})); + +vi.mock("next/link", () => ({ + default: ({ children, href }: { children: ReactNode; href: string }) => ( + {children} + ), +})); + +describe("AlertsUnsubscribePage", () => { + it("shows a neutral link back to the app after unsubscribing", async () => { + // Given + unsubscribeAlertRecipientMock.mockResolvedValueOnce({ + ok: true, + state: "unsubscribed", + message: + "You have been unsubscribed. You will not receive further alerts at this address.", + }); + + // When + render( + await AlertsUnsubscribePage({ + searchParams: Promise.resolve({ token: "token-1" }), + }), + ); + + // Then + expect(unsubscribeAlertRecipientMock).toHaveBeenCalledWith("token-1"); + expect(screen.getByLabelText("Unsubscribed")).toBeInTheDocument(); + expect( + screen.getByText( + "You have been unsubscribed. You will not receive further alerts at this address.", + ), + ).toBeVisible(); + expect( + screen.getByRole("link", { name: "Continue to Prowler" }), + ).toHaveAttribute("href", "/"); + }); +}); diff --git a/ui/app/(auth)/alerts/unsubscribe/page.tsx b/ui/app/(auth)/alerts/unsubscribe/page.tsx new file mode 100644 index 0000000000..e54798a6e9 --- /dev/null +++ b/ui/app/(auth)/alerts/unsubscribe/page.tsx @@ -0,0 +1,40 @@ +import Link from "next/link"; + +import { AuthLayout } from "@/components/auth/oss/auth-layout"; +import { Button } from "@/components/shadcn"; + +import { unsubscribeAlertRecipient } from "./unsubscribe-alert-recipient"; + +interface AlertsUnsubscribePageProps { + searchParams: Promise<{ [key: string]: string | string[] | undefined }>; +} + +const getParamValue = ( + params: Awaited, + key: string, +): string | undefined => { + const value = params[key]; + return Array.isArray(value) ? value[0] : value; +}; + +export default async function AlertsUnsubscribePage({ + searchParams, +}: AlertsUnsubscribePageProps) { + const resolvedSearchParams = await searchParams; + const token = getParamValue(resolvedSearchParams, "token"); + const result = await unsubscribeAlertRecipient(token); + const title = result.ok ? "Unsubscribed" : "Subscription link"; + + return ( + +
    +

    + {result.message} +

    + +
    +
    + ); +} diff --git a/ui/app/(auth)/alerts/unsubscribe/unsubscribe-alert-recipient.test.ts b/ui/app/(auth)/alerts/unsubscribe/unsubscribe-alert-recipient.test.ts new file mode 100644 index 0000000000..8a595fa63e --- /dev/null +++ b/ui/app/(auth)/alerts/unsubscribe/unsubscribe-alert-recipient.test.ts @@ -0,0 +1,105 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { unsubscribeAlertRecipient } from "./unsubscribe-alert-recipient"; + +const fetchMock = vi.fn(); + +const lastFetchCall = (): { url: string; init: RequestInit } => { + const call = fetchMock.mock.calls.at(-1); + if (!call) throw new Error("fetch was not called"); + const [url, init] = call; + return { url: String(url), init: (init ?? {}) as RequestInit }; +}; + +describe("unsubscribeAlertRecipient", () => { + beforeEach(() => { + vi.stubGlobal("fetch", fetchMock); + vi.stubEnv("NEXT_PUBLIC_API_BASE_URL", "https://api.example.com/api/v1"); + fetchMock.mockResolvedValue( + new Response( + JSON.stringify({ + state: "unsubscribed", + message: + "You have been unsubscribed. You will not receive further alerts at this address.", + }), + { status: 200, headers: { "Content-Type": "application/json" } }, + ), + ); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + vi.unstubAllGlobals(); + vi.clearAllMocks(); + }); + + it("calls the public unsubscribe endpoint without auth headers", async () => { + // When + const result = await unsubscribeAlertRecipient("token-1"); + + // Then + expect(result).toEqual({ + ok: true, + state: "unsubscribed", + message: + "You have been unsubscribed. You will not receive further alerts at this address.", + }); + const { url, init } = lastFetchCall(); + expect(url).toBe( + "https://api.example.com/api/v1/alerts/recipients/unsubscribe?token=token-1", + ); + expect(init).toEqual({ + headers: { Accept: "application/json" }, + cache: "no-store", + }); + }); + + it("returns the API message for invalid tokens", async () => { + // Given + fetchMock.mockResolvedValueOnce( + new Response( + JSON.stringify({ + state: "invalid_token", + message: "This link is invalid or has expired.", + }), + { status: 400, headers: { "Content-Type": "application/json" } }, + ), + ); + + // When + const result = await unsubscribeAlertRecipient("expired-token"); + + // Then + expect(result).toEqual({ + ok: false, + state: "invalid_token", + message: "This link is invalid or has expired.", + }); + }); + + it("returns the API message for missing tokens", async () => { + // Given + fetchMock.mockResolvedValueOnce( + new Response( + JSON.stringify({ + state: "missing_token", + message: "This link is missing a token.", + }), + { status: 400, headers: { "Content-Type": "application/json" } }, + ), + ); + + // When + const result = await unsubscribeAlertRecipient(); + + // Then + expect(result).toEqual({ + ok: false, + state: "missing_token", + message: "This link is missing a token.", + }); + expect(lastFetchCall().url).toBe( + "https://api.example.com/api/v1/alerts/recipients/unsubscribe", + ); + }); +}); diff --git a/ui/app/(auth)/alerts/unsubscribe/unsubscribe-alert-recipient.ts b/ui/app/(auth)/alerts/unsubscribe/unsubscribe-alert-recipient.ts new file mode 100644 index 0000000000..af64165a1e --- /dev/null +++ b/ui/app/(auth)/alerts/unsubscribe/unsubscribe-alert-recipient.ts @@ -0,0 +1,79 @@ +interface AlertUnsubscribeApiResponse { + state?: string; + message?: string; +} + +interface AlertUnsubscribeResult { + ok: boolean; + state: string; + message: string; +} + +const FALLBACK_UNSUBSCRIBE_ERROR = + "We could not process this unsubscribe link. Please try again later."; + +const toMessage = (payload: unknown): string | null => { + if ( + typeof payload === "object" && + payload !== null && + "message" in payload && + typeof payload.message === "string" + ) { + return payload.message; + } + + return null; +}; + +const toState = (payload: unknown): string => { + if ( + typeof payload === "object" && + payload !== null && + "state" in payload && + typeof payload.state === "string" + ) { + return payload.state; + } + + return "unknown"; +}; + +export const unsubscribeAlertRecipient = async ( + token?: string, +): Promise => { + const apiBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL; + if (!apiBaseUrl) { + return { + ok: false, + state: "missing_api_base_url", + message: FALLBACK_UNSUBSCRIBE_ERROR, + }; + } + + const url = new URL(`${apiBaseUrl}/alerts/recipients/unsubscribe`); + if (token) { + url.searchParams.set("token", token); + } + + try { + const response = await fetch(url.toString(), { + headers: { + Accept: "application/json", + }, + cache: "no-store", + }); + const payload = (await response.json()) as AlertUnsubscribeApiResponse; + + return { + ok: response.ok, + state: toState(payload), + message: toMessage(payload) ?? FALLBACK_UNSUBSCRIBE_ERROR, + }; + } catch { + return { + ok: false, + state: "network_error", + message: FALLBACK_UNSUBSCRIBE_ERROR, + }; + } +}; diff --git a/ui/app/(prowler)/_overview/_components/accounts-selector.test.tsx b/ui/app/(prowler)/_overview/_components/accounts-selector.test.tsx index 8759e86fc0..62154f1042 100644 --- a/ui/app/(prowler)/_overview/_components/accounts-selector.test.tsx +++ b/ui/app/(prowler)/_overview/_components/accounts-selector.test.tsx @@ -31,6 +31,7 @@ vi.mock("@/components/icons/providers-badge", () => ({ OracleCloudProviderBadge: () => Oracle Cloud, AlibabaCloudProviderBadge: () => Alibaba Cloud, VercelProviderBadge: () => Vercel, + OktaProviderBadge: () => Okta, })); vi.mock("@/components/shadcn/select/multiselect", () => ({ @@ -138,4 +139,13 @@ describe("AccountsSelector", () => { screen.getByText("Production AWS").closest("[data-value]"), ).toHaveAttribute("data-keywords", expect.stringContaining("123456789012")); }); + + it("disables select all when every account is already shown", () => { + render(); + + expect( + screen.getByRole("option", { name: /select all accounts/i }), + ).toHaveAttribute("aria-disabled", "true"); + expect(screen.getByText("All selected")).toBeInTheDocument(); + }); }); diff --git a/ui/app/(prowler)/_overview/_components/accounts-selector.tsx b/ui/app/(prowler)/_overview/_components/accounts-selector.tsx index e134625d98..4e7ca5bf79 100644 --- a/ui/app/(prowler)/_overview/_components/accounts-selector.tsx +++ b/ui/app/(prowler)/_overview/_components/accounts-selector.tsx @@ -16,6 +16,7 @@ import { KS8ProviderBadge, M365ProviderBadge, MongoDBAtlasProviderBadge, + OktaProviderBadge, OpenStackProviderBadge, OracleCloudProviderBadge, VercelProviderBadge, @@ -51,6 +52,7 @@ const PROVIDER_ICON: Record = { cloudflare: , openstack: , vercel: , + okta: , }; /** Common props shared by both batch and instant modes. */ @@ -146,7 +148,7 @@ export function AccountsSelector({ const filterDescription = selectedProviderTypes && selectedProviderTypes.length > 0 ? `Accounts for ${selectedProviderTypes.map(getProviderDisplayName).join(", ")}` - : "All connected cloud provider accounts"; + : "All connected provider accounts"; return (
    @@ -155,8 +157,8 @@ export function AccountsSelector({ className="sr-only" id="accounts-label" > - Filter by cloud provider account. {filterDescription}. Select one or - more accounts to view findings. + Filter by provider account. {filterDescription}. Select one or more + accounts to view findings. handleMultiValueChange([])} + className="text-text-neutral-secondary flex w-full cursor-pointer items-center gap-3 rounded-lg px-4 py-3 text-sm font-semibold hover:bg-slate-200 aria-disabled:cursor-not-allowed aria-disabled:opacity-50 dark:hover:bg-slate-700/50" + onClick={() => { + if (selectedIds.length === 0) return; + handleMultiValueChange([]); + }} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); + if (selectedIds.length === 0) return; handleMultiValueChange([]); } }} > - Select All + {selectedIds.length === 0 ? "All selected" : "Select All"}
    {visibleProviders.map((p) => { const id = p.id; diff --git a/ui/app/(prowler)/_overview/_components/provider-type-selector.test.tsx b/ui/app/(prowler)/_overview/_components/provider-type-selector.test.tsx index a60766394f..d7e027cae2 100644 --- a/ui/app/(prowler)/_overview/_components/provider-type-selector.test.tsx +++ b/ui/app/(prowler)/_overview/_components/provider-type-selector.test.tsx @@ -31,6 +31,7 @@ vi.mock("@/components/icons/providers-badge", () => ({ CloudflareProviderBadge: () => Cloudflare, OpenStackProviderBadge: () => OpenStack, VercelProviderBadge: () => Vercel, + OktaProviderBadge: () => Okta, })); vi.mock("@/components/shadcn/select/multiselect", () => ({ @@ -135,4 +136,13 @@ describe("ProviderTypeSelector", () => { expect.stringContaining("Amazon Web Services"), ); }); + + it("disables select all when every provider is already shown", () => { + render(); + + expect( + screen.getByRole("option", { name: /select all providers/i }), + ).toHaveAttribute("aria-disabled", "true"); + expect(screen.getByText("All selected")).toBeInTheDocument(); + }); }); diff --git a/ui/app/(prowler)/_overview/_components/provider-type-selector.tsx b/ui/app/(prowler)/_overview/_components/provider-type-selector.tsx index 819bc49000..9d0b0de3b5 100644 --- a/ui/app/(prowler)/_overview/_components/provider-type-selector.tsx +++ b/ui/app/(prowler)/_overview/_components/provider-type-selector.tsx @@ -89,6 +89,11 @@ const VercelProviderBadge = lazy(() => default: m.VercelProviderBadge, })), ); +const OktaProviderBadge = lazy(() => + import("@/components/icons/providers-badge").then((m) => ({ + default: m.OktaProviderBadge, + })), +); type IconProps = { width: number; height: number }; @@ -160,6 +165,10 @@ const PROVIDER_DATA: Record< label: "Vercel", icon: VercelProviderBadge, }, + okta: { + label: "Okta", + icon: OktaProviderBadge, + }, }; /** Common props shared by both batch and instant modes. */ @@ -277,8 +286,7 @@ export const ProviderTypeSelector = ({ className="sr-only" id="provider-type-label" > - Filter by cloud provider type. Select one or more providers to view - findings. + Filter by provider type. Select one or more providers to view findings. handleMultiValueChange([])} + className="text-text-neutral-secondary flex w-full cursor-pointer items-center gap-3 rounded-lg px-4 py-3 text-sm font-semibold hover:bg-slate-200 aria-disabled:cursor-not-allowed aria-disabled:opacity-50 dark:hover:bg-slate-700/50" + onClick={() => { + if (selectedTypes.length === 0) return; + handleMultiValueChange([]); + }} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); + if (selectedTypes.length === 0) return; handleMultiValueChange([]); } }} > - Select All + {selectedTypes.length === 0 ? "All selected" : "Select All"} {availableTypes.map((providerType) => ( ({ + fetchMock: vi.fn(), + getAuthHeadersMock: vi.fn(), + handleApiErrorMock: vi.fn(), + handleApiResponseMock: vi.fn(), +})); + +vi.mock("@/lib", () => ({ + apiBaseUrl: "https://api.test/api/v1", + getAuthHeaders: getAuthHeadersMock, + getErrorMessage: (error: unknown) => + error instanceof Error ? error.message : String(error), +})); + +vi.mock("@/lib/server-actions-helper", () => ({ + handleApiError: handleApiErrorMock, + handleApiResponse: handleApiResponseMock, +})); + +import { ALERT_AGGREGATE_OPS, ALERT_TRIGGER_KINDS } from "../_types"; +import { + createAlert, + deleteAlert, + disableAlert, + enableAlert, + listAlerts, + previewAlertCondition, + seedAlertRule, + updateAlert, +} from "./alerts"; + +const lastFetchCall = (): { url: string; init: RequestInit } => { + const call = fetchMock.mock.calls.at(-1); + if (!call) throw new Error("fetch was not called"); + const [url, init] = call; + return { url: String(url), init: (init ?? {}) as RequestInit }; +}; + +beforeEach(() => { + vi.clearAllMocks(); + vi.stubGlobal("fetch", fetchMock); + fetchMock.mockResolvedValue( + new Response(JSON.stringify({ data: [] }), { + status: 200, + headers: { "Content-Type": "application/vnd.api+json" }, + }), + ); + getAuthHeadersMock.mockResolvedValue({ + Accept: "application/vnd.api+json", + Authorization: "Bearer test-token", + "Content-Type": "application/vnd.api+json", + }); + handleApiResponseMock.mockResolvedValue({ data: [] }); + handleApiErrorMock.mockReturnValue({ error: "Unexpected error." }); +}); + +describe("listAlerts", () => { + it("returns whatever handleApiResponse returns", async () => { + handleApiResponseMock.mockResolvedValue({ + data: [], + meta: { pagination: { count: 0 } }, + }); + const result = await listAlerts({ "filter[enabled]": "true" }); + expect(result).toEqual({ data: [], meta: { pagination: { count: 0 } } }); + }); + + it("forwards searchParams as query string", async () => { + await listAlerts({ "filter[trigger]": "daily" }); + expect(lastFetchCall().url).toContain("filter%5Btrigger%5D=daily"); + }); + + it("delegates network errors to handleApiError", async () => { + fetchMock.mockRejectedValueOnce(new Error("boom")); + handleApiErrorMock.mockReturnValueOnce({ error: "boom" }); + const result = await listAlerts(); + expect(handleApiErrorMock).toHaveBeenCalled(); + expect(result).toEqual({ error: "boom" }); + }); +}); + +describe("createAlert", () => { + it("posts a JSON:API envelope with schema_version", async () => { + handleApiResponseMock.mockResolvedValue({ + data: { + id: "alert-1", + type: "alert-rules", + attributes: { name: "n", trigger: "after_scan" }, + }, + }); + await createAlert({ + name: "Daily critical", + trigger: ALERT_TRIGGER_KINDS.AFTER_SCAN, + condition: { + op: ALERT_AGGREGATE_OPS.ANY, + filter: { severity: ["critical"] }, + }, + }); + const { init } = lastFetchCall(); + expect(init.method).toBe("POST"); + const body = JSON.parse(init.body as string); + expect(body.data.type).toBe("alert-rules"); + expect(body.data.attributes.schema_version).toBe(1); + }); + + it("sends an empty recipient list when provided", async () => { + await createAlert({ + name: "No recipients yet", + trigger: ALERT_TRIGGER_KINDS.AFTER_SCAN, + condition: { + op: ALERT_AGGREGATE_OPS.ANY, + filter: { severity: ["critical"] }, + }, + recipientEmails: [], + }); + const body = JSON.parse(lastFetchCall().init.body as string); + expect(body.data.attributes.recipient_emails).toEqual([]); + }); +}); + +describe("seedAlertRule", () => { + it("posts a JSON:API seeding envelope to /seed", async () => { + const filterBag = { + "filter[severity__in]": "critical", + "filter[sort]": "-severity", + }; + await seedAlertRule(filterBag); + const { url, init } = lastFetchCall(); + expect(url).toMatch(/\/alerts\/rules\/seed$/); + expect(init.method).toBe("POST"); + expect(JSON.parse(init.body as string)).toEqual({ + data: { + type: "alert-rule-seedings", + attributes: { filter_bag: filterBag }, + }, + }); + }); +}); + +describe("updateAlert", () => { + it("PATCHes the alert with the id in the URL", async () => { + await updateAlert("alert-1", { + name: "Updated", + trigger: ALERT_TRIGGER_KINDS.DAILY, + condition: { + op: ALERT_AGGREGATE_OPS.ANY, + filter: { severity: ["critical"] }, + }, + }); + const { url, init } = lastFetchCall(); + expect(url).toContain("/alerts/rules/alert-1"); + expect(init.method).toBe("PATCH"); + }); +}); + +describe("deleteAlert", () => { + it("issues a DELETE against the alert id", async () => { + handleApiResponseMock.mockResolvedValue({ success: true, status: 204 }); + await deleteAlert("alert-1"); + const { init } = lastFetchCall(); + expect(init.method).toBe("DELETE"); + }); +}); + +describe("enable / disable", () => { + it("PATCHes enabled true to the alert rule endpoint", async () => { + await enableAlert("alert-1"); + const { url, init } = lastFetchCall(); + expect(url).toMatch(/\/alerts\/rules\/alert-1$/); + expect(init.method).toBe("PATCH"); + expect(JSON.parse(init.body as string)).toEqual({ + data: { + type: "alert-rules", + id: "alert-1", + attributes: { enabled: true }, + }, + }); + }); + + it("PATCHes enabled false to the alert rule endpoint", async () => { + await disableAlert("alert-1"); + const { url, init } = lastFetchCall(); + expect(url).toMatch(/\/alerts\/rules\/alert-1$/); + expect(init.method).toBe("PATCH"); + expect(JSON.parse(init.body as string)).toEqual({ + data: { + type: "alert-rules", + id: "alert-1", + attributes: { enabled: false }, + }, + }); + }); +}); + +describe("previewAlertCondition", () => { + it("posts a JSON:API preview envelope to /preview", async () => { + const condition = { + op: ALERT_AGGREGATE_OPS.ANY, + filter: { severity: ["critical"] }, + }; + await previewAlertCondition({ condition }); + const { url, init } = lastFetchCall(); + expect(url).toMatch(/\/alerts\/rules\/preview$/); + expect(init.method).toBe("POST"); + expect(init.headers).toEqual( + expect.objectContaining({ + Accept: "application/vnd.api+json", + "Content-Type": "application/vnd.api+json", + }), + ); + expect(JSON.parse(init.body as string)).toEqual({ + data: { + type: "alert-rule-previews", + attributes: { condition }, + }, + }); + }); +}); diff --git a/ui/app/(prowler)/alerts/_actions/alerts.ts b/ui/app/(prowler)/alerts/_actions/alerts.ts new file mode 100644 index 0000000000..a260636c0b --- /dev/null +++ b/ui/app/(prowler)/alerts/_actions/alerts.ts @@ -0,0 +1,214 @@ +"use server"; + +import { apiBaseUrl, getAuthHeaders } from "@/lib"; +import { handleApiError, handleApiResponse } from "@/lib/server-actions-helper"; + +import { + ALERT_SCHEMA_VERSION, + type AlertCondition, + type AlertTriggerKind, +} from "../_types"; + +const ALERT_RULES_API_PATH = "/alerts/rules"; +const ALERTS_REVALIDATE_PATH = "/alerts"; + +export interface AlertPayload { + name: string; + description?: string; + enabled?: boolean; + trigger: AlertTriggerKind; + condition: AlertCondition; + /** + * List of recipient email addresses. The API resolves them to existing + * `AlertRecipient` rows or creates new pending ones with confirmation + * emails. Recipient IDs are NOT used by the rule write path. + */ + recipientEmails?: string[]; +} + +const buildRuleEnvelope = (payload: AlertPayload, alertId?: string) => ({ + data: { + type: "alert-rules", + ...(alertId ? { id: alertId } : {}), + attributes: { + name: payload.name, + description: payload.description ?? "", + enabled: payload.enabled ?? true, + trigger: payload.trigger, + condition: payload.condition, + schema_version: ALERT_SCHEMA_VERSION, + ...(payload.recipientEmails !== undefined + ? { recipient_emails: payload.recipientEmails } + : {}), + }, + }, +}); + +const buildEnabledEnvelope = (alertId: string, enabled: boolean) => ({ + data: { + type: "alert-rules", + id: alertId, + attributes: { enabled }, + }, +}); + +const buildSeedEnvelope = (filterBag: Record) => ({ + data: { + type: "alert-rule-seedings", + attributes: { filter_bag: filterBag }, + }, +}); + +export const listAlerts = async ( + searchParams?: Record, +) => { + const headers = await getAuthHeaders({ contentType: false }); + const url = new URL(`${apiBaseUrl}${ALERT_RULES_API_PATH}`); + + if (searchParams) { + for (const [key, value] of Object.entries(searchParams)) { + if (value !== undefined && value !== "") { + url.searchParams.append(key, value); + } + } + } + + try { + const response = await fetch(url.toString(), { headers }); + return handleApiResponse(response); + } catch (error) { + return handleApiError(error); + } +}; + +export const getAlert = async (alertId: string) => { + const headers = await getAuthHeaders({ contentType: false }); + const url = new URL(`${apiBaseUrl}${ALERT_RULES_API_PATH}/${alertId}`); + + try { + const response = await fetch(url.toString(), { headers }); + return handleApiResponse(response); + } catch (error) { + return handleApiError(error); + } +}; + +export const seedAlertRule = async ( + filterBag: Record, +) => { + const headers = await getAuthHeaders({ contentType: true }); + const url = new URL(`${apiBaseUrl}${ALERT_RULES_API_PATH}/seed`); + + try { + const response = await fetch(url.toString(), { + method: "POST", + headers, + body: JSON.stringify(buildSeedEnvelope(filterBag)), + }); + return handleApiResponse(response); + } catch (error) { + return handleApiError(error); + } +}; + +export const createAlert = async (payload: AlertPayload) => { + const headers = await getAuthHeaders({ contentType: true }); + const url = new URL(`${apiBaseUrl}${ALERT_RULES_API_PATH}`); + + try { + const response = await fetch(url.toString(), { + method: "POST", + headers, + body: JSON.stringify(buildRuleEnvelope(payload)), + }); + return handleApiResponse(response, ALERTS_REVALIDATE_PATH); + } catch (error) { + return handleApiError(error); + } +}; + +export const updateAlert = async (alertId: string, payload: AlertPayload) => { + const headers = await getAuthHeaders({ contentType: true }); + const url = new URL(`${apiBaseUrl}${ALERT_RULES_API_PATH}/${alertId}`); + + try { + const response = await fetch(url.toString(), { + method: "PATCH", + headers, + body: JSON.stringify(buildRuleEnvelope(payload, alertId)), + }); + return handleApiResponse(response, ALERTS_REVALIDATE_PATH); + } catch (error) { + return handleApiError(error); + } +}; + +export const deleteAlert = async (alertId: string) => { + const headers = await getAuthHeaders({ contentType: false }); + const url = new URL(`${apiBaseUrl}${ALERT_RULES_API_PATH}/${alertId}`); + + try { + const response = await fetch(url.toString(), { + method: "DELETE", + headers, + }); + return handleApiResponse(response, ALERTS_REVALIDATE_PATH); + } catch (error) { + return handleApiError(error); + } +}; + +export const enableAlert = async (alertId: string) => { + const headers = await getAuthHeaders({ contentType: true }); + const url = new URL(`${apiBaseUrl}${ALERT_RULES_API_PATH}/${alertId}`); + + try { + const response = await fetch(url.toString(), { + method: "PATCH", + headers, + body: JSON.stringify(buildEnabledEnvelope(alertId, true)), + }); + return handleApiResponse(response, ALERTS_REVALIDATE_PATH); + } catch (error) { + return handleApiError(error); + } +}; + +export const disableAlert = async (alertId: string) => { + const headers = await getAuthHeaders({ contentType: true }); + const url = new URL(`${apiBaseUrl}${ALERT_RULES_API_PATH}/${alertId}`); + + try { + const response = await fetch(url.toString(), { + method: "PATCH", + headers, + body: JSON.stringify(buildEnabledEnvelope(alertId, false)), + }); + return handleApiResponse(response, ALERTS_REVALIDATE_PATH); + } catch (error) { + return handleApiError(error); + } +}; + +export const previewAlertCondition = async (payload: { + condition: AlertCondition; +}) => { + const headers = await getAuthHeaders({ contentType: true }); + const url = new URL(`${apiBaseUrl}${ALERT_RULES_API_PATH}/preview`); + + try { + const response = await fetch(url.toString(), { + method: "POST", + headers, + body: JSON.stringify({ + data: { + type: "alert-rule-previews", + attributes: { condition: payload.condition }, + }, + }), + }); + return handleApiResponse(response); + } catch (error) { + return handleApiError(error); + } +}; diff --git a/ui/app/(prowler)/alerts/_actions/index.ts b/ui/app/(prowler)/alerts/_actions/index.ts new file mode 100644 index 0000000000..e4aa1f6160 --- /dev/null +++ b/ui/app/(prowler)/alerts/_actions/index.ts @@ -0,0 +1,2 @@ +export * from "./alerts"; +export * from "./recipients"; diff --git a/ui/app/(prowler)/alerts/_actions/recipients.test.ts b/ui/app/(prowler)/alerts/_actions/recipients.test.ts new file mode 100644 index 0000000000..1a51a0d34a --- /dev/null +++ b/ui/app/(prowler)/alerts/_actions/recipients.test.ts @@ -0,0 +1,70 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const { + fetchMock, + getAuthHeadersMock, + handleApiErrorMock, + handleApiResponseMock, +} = vi.hoisted(() => ({ + fetchMock: vi.fn(), + getAuthHeadersMock: vi.fn(), + handleApiErrorMock: vi.fn(), + handleApiResponseMock: vi.fn(), +})); + +vi.mock("@/lib", () => ({ + apiBaseUrl: "https://api.test/api/v1", + getAuthHeaders: getAuthHeadersMock, + getErrorMessage: (error: unknown) => + error instanceof Error ? error.message : String(error), +})); + +vi.mock("@/lib/server-actions-helper", () => ({ + handleApiError: handleApiErrorMock, + handleApiResponse: handleApiResponseMock, +})); + +import { listAlertRecipients } from "./recipients"; + +beforeEach(() => { + vi.clearAllMocks(); + vi.stubGlobal("fetch", fetchMock); + fetchMock.mockResolvedValue( + new Response(JSON.stringify({ data: [] }), { + status: 200, + headers: { "Content-Type": "application/vnd.api+json" }, + }), + ); + getAuthHeadersMock.mockResolvedValue({ + Accept: "application/vnd.api+json", + Authorization: "Bearer test-token", + }); + handleApiResponseMock.mockResolvedValue({ data: [] }); + handleApiErrorMock.mockReturnValue({ error: "Unexpected error." }); +}); + +describe("listAlertRecipients", () => { + it("returns whatever handleApiResponse returns", async () => { + handleApiResponseMock.mockResolvedValue({ + data: [ + { + id: "1", + type: "alert-recipients", + attributes: { email: "a@b.test", status: "pending" }, + }, + ], + meta: { pagination: { count: 1, page: 1, pages: 1 } }, + }); + const result = await listAlertRecipients({ + "filter[status]": "pending", + }); + expect(result.data).toHaveLength(1); + expect(result.data[0].attributes.email).toBe("a@b.test"); + }); + + it("forwards searchParams as query string", async () => { + await listAlertRecipients({ "filter[status]": "pending" }); + const [url] = fetchMock.mock.calls.at(-1) ?? [""]; + expect(String(url)).toContain("filter%5Bstatus%5D=pending"); + }); +}); diff --git a/ui/app/(prowler)/alerts/_actions/recipients.ts b/ui/app/(prowler)/alerts/_actions/recipients.ts new file mode 100644 index 0000000000..8d528b19f7 --- /dev/null +++ b/ui/app/(prowler)/alerts/_actions/recipients.ts @@ -0,0 +1,28 @@ +"use server"; + +import { apiBaseUrl, getAuthHeaders } from "@/lib"; +import { handleApiError, handleApiResponse } from "@/lib/server-actions-helper"; + +const RECIPIENTS_PATH = "/alerts/recipients"; + +export const listAlertRecipients = async ( + searchParams?: Record, +) => { + const headers = await getAuthHeaders({ contentType: false }); + const url = new URL(`${apiBaseUrl}${RECIPIENTS_PATH}`); + + if (searchParams) { + for (const [key, value] of Object.entries(searchParams)) { + if (value !== undefined && value !== "") { + url.searchParams.append(key, value); + } + } + } + + try { + const response = await fetch(url.toString(), { headers }); + return handleApiResponse(response); + } catch (error) { + return handleApiError(error); + } +}; diff --git a/ui/app/(prowler)/alerts/_components/__tests__/alert-form-modal.test.tsx b/ui/app/(prowler)/alerts/_components/__tests__/alert-form-modal.test.tsx new file mode 100644 index 0000000000..c3f37cfd45 --- /dev/null +++ b/ui/app/(prowler)/alerts/_components/__tests__/alert-form-modal.test.tsx @@ -0,0 +1,755 @@ +import { render, screen, waitFor, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import type { ReactNode } from "react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { + ALERT_AGGREGATE_OPS, + ALERT_BOOLEAN_OPS, + ALERT_RECIPIENT_STATUS, + ALERT_TRIGGER_KINDS, + type AlertCondition, + type AlertRecipient, + type AlertRule, +} from "@/app/(prowler)/alerts/_types"; +import type { ProviderProps } from "@/types/providers"; + +import { AlertFormModal } from "../alert-form-modal"; + +const recipientsActionMocks = vi.hoisted(() => ({ + listAlertRecipients: vi.fn(), +})); + +const alertsActionMocks = vi.hoisted(() => ({ + previewAlertCondition: vi.fn(), + seedAlertRule: vi.fn(), +})); + +vi.mock( + "@/app/(prowler)/alerts/_actions/recipients", + () => recipientsActionMocks, +); + +vi.mock("@/app/(prowler)/alerts/_actions", () => alertsActionMocks); + +vi.mock( + "@/components/compliance/compliance-header/compliance-scan-info", + () => ({ + ComplianceScanInfo: () => Scan, + }), +); + +vi.mock("@/components/ui/entities/entity-info", () => ({ + EntityInfo: ({ + entityAlias, + entityId, + }: { + entityAlias?: string; + entityId?: string; + }) => {entityAlias ?? entityId}, +})); + +vi.mock("next-auth/react", () => ({ + useSession: () => ({ data: null, status: "unauthenticated" }), +})); + +vi.mock("next/navigation", () => ({ + usePathname: () => "/alerts", + useRouter: () => ({ replace: vi.fn(), push: vi.fn(), refresh: vi.fn() }), + useSearchParams: () => new URLSearchParams(), +})); + +vi.mock("@/components/shadcn/modal", () => ({ + Modal: ({ + open, + title, + description, + className, + onOpenAutoFocus, + children, + }: { + open: boolean; + title?: string; + description?: string; + className?: string; + onOpenAutoFocus?: (event: Event) => void; + children: ReactNode; + }) => + open ? ( +
    + {children} +
    + ) : null, +})); + +class ResizeObserverMock { + observe = vi.fn(); + unobserve = vi.fn(); + disconnect = vi.fn(); +} + +global.ResizeObserver = ResizeObserverMock; +Element.prototype.scrollIntoView = vi.fn(); + +const mockProviders: ProviderProps[] = [ + { + id: "provider-aws-1", + type: "providers", + attributes: { + provider: "aws", + uid: "123456789012", + alias: "Production AWS", + status: "completed", + resources: 42, + connection: { + connected: true, + last_checked_at: "2026-04-30T00:00:00Z", + }, + scanner_args: { + only_logs: false, + excluded_checks: [], + aws_retries_max_attempts: 3, + }, + inserted_at: "2026-04-30T00:00:00Z", + updated_at: "2026-04-30T00:00:00Z", + created_by: { object: "users", id: "user-1" }, + }, + relationships: { + secret: { data: null }, + provider_groups: { meta: { count: 0 }, data: [] }, + }, + }, + { + id: "provider-gcp-1", + type: "providers", + attributes: { + provider: "gcp", + uid: "prowler-prod-project", + alias: "Production GCP", + status: "completed", + resources: 21, + connection: { + connected: true, + last_checked_at: "2026-04-30T00:00:00Z", + }, + scanner_args: { + only_logs: false, + excluded_checks: [], + aws_retries_max_attempts: 3, + }, + inserted_at: "2026-04-30T00:00:00Z", + updated_at: "2026-04-30T00:00:00Z", + created_by: { object: "users", id: "user-1" }, + }, + relationships: { + secret: { data: null }, + provider_groups: { meta: { count: 0 }, data: [] }, + }, + }, +]; + +const createRecipient = ( + id: string, + email: string, + status: AlertRecipient["attributes"]["status"], +): AlertRecipient => ({ + id, + type: "alert-recipients", + attributes: { + email, + status, + inserted_at: "2026-04-30T00:00:00Z", + updated_at: "2026-04-30T00:00:00Z", + }, + relationships: { rules: { data: [] } }, +}); + +const confirmedRecipient = createRecipient( + "recipient-confirmed", + "security@example.com", + ALERT_RECIPIENT_STATUS.CONFIRMED, +); + +const pendingRecipient = createRecipient( + "recipient-pending", + "pending@example.com", + ALERT_RECIPIENT_STATUS.PENDING, +); + +const createEditingAlert = ( + overrides: Partial = {}, +): AlertRule => ({ + id: "alert-1", + type: "alert-rules", + attributes: { + name: "Existing alert", + description: "Existing description", + enabled: true, + trigger: ALERT_TRIGGER_KINDS.AFTER_SCAN, + condition: { + op: ALERT_AGGREGATE_OPS.COUNT_GTE, + filter: { severity: ["critical"] }, + value: 1, + }, + schema_version: 1, + recipient_emails: ["security@example.com"], + inserted_at: "2026-04-30T00:00:00Z", + updated_at: "2026-04-30T00:00:00Z", + ...overrides, + }, +}); + +const mockRecipientsList = () => { + recipientsActionMocks.listAlertRecipients.mockResolvedValue({ + data: [confirmedRecipient, pendingRecipient], + meta: { pagination: { page: 1, pages: 1, count: 2 } }, + }); +}; + +const renderCreateModal = ( + props: Partial> = {}, +) => + render( + , + ); + +const getVisibleFilterTrigger = (label: string): HTMLButtonElement => { + const trigger = screen + .getAllByRole("combobox") + .find( + (element) => + element.textContent?.includes(label) && + !element.closest('[aria-hidden="true"]'), + ); + + expect(trigger).toBeDefined(); + return trigger as HTMLButtonElement; +}; + +describe("AlertFormModal", () => { + beforeEach(() => { + recipientsActionMocks.listAlertRecipients.mockReset(); + recipientsActionMocks.listAlertRecipients.mockReturnValue( + new Promise(() => {}), + ); + alertsActionMocks.previewAlertCondition.mockReset(); + alertsActionMocks.seedAlertRule.mockReset(); + alertsActionMocks.seedAlertRule.mockResolvedValue({ + data: { + attributes: { + condition: { + op: ALERT_AGGREGATE_OPS.ANY, + filter: { provider_type: ["gcp"] }, + }, + schema_version: 1, + warnings: [], + }, + }, + }); + }); + + it("should render the simplified alert form without preview, delivery settings, or nested recipient management", () => { + // Given / When + renderCreateModal({ + providers: mockProviders, + uniqueRegions: ["us-east-1", "europe-west1"], + uniqueServices: ["iam", "cloudsql"], + uniqueCategories: ["identity-security"], + uniqueGroups: ["prod"], + }); + + // Then + expect(screen.getByRole("dialog", { name: "Create Alert" })).toBeVisible(); + expect(screen.getByLabelText(/^name$/i)).toBeVisible(); + expect(screen.getByLabelText(/^description$/i)).toBeVisible(); + expect(screen.getByLabelText(/^frequency$/i)).toBeVisible(); + expect(screen.getByLabelText(/^recipients$/i)).toBeVisible(); + expect(screen.getAllByRole("combobox")).toHaveLength(2); + expect(screen.queryByText("Alert criteria")).not.toBeInTheDocument(); + expect(screen.queryByText(/delivery settings/i)).not.toBeInTheDocument(); + expect( + screen.queryByLabelText(/notification method/i), + ).not.toBeInTheDocument(); + expect(screen.queryByText(/run preview/i)).not.toBeInTheDocument(); + expect( + screen.queryByRole("button", { name: /manage recipients/i }), + ).not.toBeInTheDocument(); + expect(screen.queryByText("Production AWS")).not.toBeInTheDocument(); + expect(screen.queryByText(/resource type/i)).not.toBeInTheDocument(); + expect(screen.queryByText(/^date$/i)).not.toBeInTheDocument(); + }); + + it("should provide accessible dialog description and allow initial focus when editing", () => { + // Given / When + renderCreateModal({ + editingAlert: createEditingAlert(), + }); + + // Then + const dialog = screen.getByRole("dialog", { name: "Edit Alert" }); + expect(dialog).toHaveAccessibleDescription( + "Update recipients, frequency, and finding filters for this alert.", + ); + expect(dialog).toHaveAttribute("data-allows-open-auto-focus", "true"); + }); + + it("should show selected Findings filters as chips while keeping criteria controls hidden", () => { + // Given / When + renderCreateModal({ + seededCondition: { + op: ALERT_AGGREGATE_OPS.ANY, + filter: { severity: ["critical"] }, + }, + selectedFindingsFilterChips: [ + { key: "filter[status__in]", label: "Status", value: "FAIL" }, + { key: "filter[muted]", label: "Muted", value: "false" }, + ], + }); + + // Then + expect( + screen.getByRole("region", { name: /active filters/i }), + ).toHaveTextContent("Status: FAIL"); + expect( + screen.getByRole("region", { name: /active filters/i }), + ).toHaveTextContent("Muted: false"); + expect(screen.queryByText("All Provider")).not.toBeInTheDocument(); + expect(screen.queryByText(/run preview/i)).not.toBeInTheDocument(); + }); + + it("should list tenant recipients with status and submit selected emails", async () => { + // Given + const user = userEvent.setup(); + const onSubmit = vi + .fn() + .mockResolvedValue({ ok: true, alertId: "alert-1" }); + mockRecipientsList(); + renderCreateModal({ onSubmit }); + + // When + await user.type(screen.getByLabelText(/^name$/i), "Critical alerts"); + await user.click(getVisibleFilterTrigger("Select emails")); + expect((await screen.findAllByText("Confirmed")).at(-1)).toBeVisible(); + expect(screen.getAllByText("Pending").at(-1)).toBeVisible(); + const recipientOptions = await screen.findAllByText("pending@example.com"); + const visibleRecipientOption = recipientOptions.at(-1); + expect(visibleRecipientOption).toBeDefined(); + await user.click(visibleRecipientOption as HTMLElement); + await user.click(screen.getByRole("button", { name: /^create$/i })); + + // Then + expect(screen.getAllByText("pending@example.com").at(-1)).toBeVisible(); + await waitFor(() => + expect(onSubmit).toHaveBeenCalledWith( + expect.objectContaining({ + frequency: ALERT_TRIGGER_KINDS.AFTER_SCAN, + recipientEmails: ["pending@example.com"], + }), + ), + ); + const recipientsParams = recipientsActionMocks.listAlertRecipients.mock + .calls[0][0] as Record; + expect(recipientsParams["filter[status]"]).toBeUndefined(); + expect(recipientsParams["page[size]"]).toBe("100"); + }); + + it("should submit the configured alert frequency", async () => { + // Given + const user = userEvent.setup(); + const onSubmit = vi + .fn() + .mockResolvedValue({ ok: true, alertId: "alert-1" }); + mockRecipientsList(); + renderCreateModal({ + defaultFrequency: ALERT_TRIGGER_KINDS.DAILY, + onSubmit, + }); + + // When + await user.type(screen.getByLabelText(/^name$/i), "Daily alerts"); + expect( + screen.getByRole("combobox", { name: /frequency/i }), + ).toHaveTextContent("Daily digest"); + await user.click(getVisibleFilterTrigger("Select emails")); + const recipientOptions = await screen.findAllByText("security@example.com"); + const visibleRecipientOption = recipientOptions.at(-1); + expect(visibleRecipientOption).toBeDefined(); + await user.click(visibleRecipientOption as HTMLElement); + await user.click(screen.getByRole("button", { name: /^create$/i })); + + // Then + await waitFor(() => + expect(onSubmit).toHaveBeenCalledWith( + expect.objectContaining({ + frequency: ALERT_TRIGGER_KINDS.DAILY, + }), + ), + ); + }); + + it("should allow submitting without selected recipients", async () => { + // Given + const user = userEvent.setup(); + const onSubmit = vi + .fn() + .mockResolvedValue({ ok: true, alertId: "alert-1" }); + mockRecipientsList(); + renderCreateModal({ onSubmit }); + + // When + await user.type(screen.getByLabelText(/^name$/i), "Critical alerts"); + await user.click(screen.getByRole("button", { name: /^create$/i })); + + // Then + await waitFor(() => + expect(onSubmit).toHaveBeenCalledWith( + expect.objectContaining({ + recipientEmails: [], + }), + ), + ); + expect( + screen.queryByText(/select at least one recipient/i), + ).not.toBeInTheDocument(); + }); + + it("should render backend submit errors with the design error color", async () => { + // Given + const user = userEvent.setup(); + const onSubmit = vi.fn().mockResolvedValue({ + ok: false, + error: "Backend validation failed", + }); + mockRecipientsList(); + renderCreateModal({ onSubmit }); + + // When + await user.type(screen.getByLabelText(/^name$/i), "Critical alerts"); + await user.click(screen.getByRole("button", { name: /^create$/i })); + + // Then + const errorMessage = await screen.findByText("Backend validation failed"); + expect(errorMessage).toHaveClass("text-text-error-primary"); + }); + + it("should reset form defaults when opening a different alert", () => { + // Given + const { rerender } = render( + , + ); + + // When + rerender( + , + ); + + // Then + expect(screen.getByLabelText(/^name$/i)).toHaveValue("Second alert"); + }); + + it("should render the shared Findings batch filter controls for an existing alert", async () => { + // Given + mockRecipientsList(); + renderCreateModal({ + editingAlert: createEditingAlert({ + condition: { + op: ALERT_BOOLEAN_OPS.AND, + children: [ + { + op: ALERT_AGGREGATE_OPS.COUNT_GTE, + filter: { severity: ["critical"] }, + value: 1, + }, + { + op: ALERT_AGGREGATE_OPS.COUNT_GTE, + filter: { provider_type: ["aws"] }, + value: 1, + }, + ], + }, + }), + providers: mockProviders, + uniqueRegions: ["us-east-1", "europe-west1"], + uniqueServices: ["iam", "cloudsql"], + uniqueResourceTypes: ["AWS::IAM::User"], + uniqueCategories: ["identity-security"], + uniqueGroups: ["prod"], + }); + + // Then + const recipientsTrigger = screen.getByLabelText(/^recipients$/i); + const filtersHeading = screen.getByRole("heading", { name: /^filters$/i }); + + expect(filtersHeading).toBeVisible(); + expect( + recipientsTrigger.compareDocumentPosition(filtersHeading) & + Node.DOCUMENT_POSITION_FOLLOWING, + ).toBeTruthy(); + expect(filtersHeading.closest('[data-slot="card"]')).toBeVisible(); + const filterControls = screen.getByTestId("findings-filter-controls"); + const alertEditGrid = filterControls.querySelector(".grid"); + expect(alertEditGrid).toHaveClass("xl:grid-cols-3", "2xl:grid-cols-3"); + expect(alertEditGrid).not.toHaveClass("xl:grid-cols-4", "2xl:grid-cols-5"); + expect(screen.getAllByText("Amazon Web Services")[0]).toBeVisible(); + expect(screen.getByText("All accounts")).toBeVisible(); + expect(within(filterControls).getByText("All Delta")).toBeVisible(); + expect(within(filterControls).getByText("All Resource Type")).toBeVisible(); + expect( + screen.queryByTestId("findings-expanded-filters"), + ).not.toBeInTheDocument(); + expect( + screen.queryByRole("button", { name: /more filters/i }), + ).not.toBeInTheDocument(); + expect(screen.queryByText("All Status")).not.toBeInTheDocument(); + expect(screen.queryByText("Scan ID")).not.toBeInTheDocument(); + expect(screen.queryByText(/^date$/i)).not.toBeInTheDocument(); + expect(screen.queryByLabelText(/^severity$/i)).not.toBeInTheDocument(); + }); + + it("should save edited filters as a normalized simple condition", async () => { + // Given + const user = userEvent.setup(); + const onSubmit = vi + .fn() + .mockResolvedValue({ ok: true, alertId: "alert-1" }); + mockRecipientsList(); + renderCreateModal({ + editingAlert: createEditingAlert(), + providers: mockProviders, + onSubmit, + }); + + // When + await user.click(screen.getByLabelText(/provider type/i)); + const providerOptions = await screen.findAllByText("Google Cloud Platform"); + const visibleProviderOption = providerOptions.at(-1); + expect(visibleProviderOption).toBeDefined(); + await user.click(visibleProviderOption as HTMLElement); + await user.click(screen.getByRole("button", { name: /^save$/i })); + + // Then + await waitFor(() => + expect(alertsActionMocks.seedAlertRule).toHaveBeenCalled(), + ); + expect(alertsActionMocks.seedAlertRule).toHaveBeenCalledWith( + expect.objectContaining({ + "filter[provider_type__in]": ["gcp"], + }), + ); + await waitFor(() => + expect(onSubmit).toHaveBeenCalledWith( + expect.objectContaining({ + condition: expect.objectContaining({ + filter: { provider_type: ["gcp"] }, + }), + }), + ), + ); + }); + + it("should preview the edited alert using current unsaved filters", async () => { + // Given + const user = userEvent.setup(); + alertsActionMocks.previewAlertCondition.mockResolvedValue({ + data: { + attributes: { + summary: { + finding_count_total: 7, + top_severity: "critical", + }, + sample_finding_ids: [], + evaluation_failed: false, + duration_ms: 42, + }, + }, + }); + mockRecipientsList(); + renderCreateModal({ + editingAlert: createEditingAlert(), + providers: mockProviders, + }); + + // When + await user.click(screen.getByLabelText(/provider type/i)); + const providerOptions = await screen.findAllByText("Google Cloud Platform"); + const visibleProviderOption = providerOptions.at(-1); + expect(visibleProviderOption).toBeDefined(); + await user.click(visibleProviderOption as HTMLElement); + await user.click(screen.getByRole("button", { name: /^test$/i })); + + // Then + await waitFor(() => + expect(alertsActionMocks.seedAlertRule).toHaveBeenCalledWith( + expect.objectContaining({ + "filter[provider_type__in]": ["gcp"], + }), + ), + ); + await waitFor(() => + expect(alertsActionMocks.previewAlertCondition).toHaveBeenCalledWith( + expect.objectContaining({ + condition: expect.objectContaining({ + filter: { provider_type: ["gcp"] }, + }), + }), + ), + ); + const previewHeading = await screen.findByText("Test result"); + expect(previewHeading).toBeVisible(); + const previewCard = previewHeading.closest('[data-slot="card"]'); + expect(previewCard).toBeInTheDocument(); + const previewCardQueries = within(previewCard as HTMLElement); + expect( + previewCardQueries.getByText( + "It found 7 findings, including Critical severity.", + ), + ).toBeVisible(); + expect( + previewCardQueries.queryByText(/^findings$/i), + ).not.toBeInTheDocument(); + expect( + previewCardQueries.queryByText(/^top severity$/i), + ).not.toBeInTheDocument(); + expect( + previewCardQueries.queryByText(/^duration$/i), + ).not.toBeInTheDocument(); + expect(previewCardQueries.queryByText(/42 ms/i)).not.toBeInTheDocument(); + expect( + previewCardQueries.queryByText("Would fire"), + ).not.toBeInTheDocument(); + expect( + previewCardQueries.queryByText("Would not fire"), + ).not.toBeInTheDocument(); + }); + + it("should explain when the edited alert has no matching findings", async () => { + // Given + const user = userEvent.setup(); + alertsActionMocks.previewAlertCondition.mockResolvedValue({ + data: { + attributes: { + summary: { + finding_count_total: 0, + }, + sample_finding_ids: [], + evaluation_failed: false, + }, + }, + }); + mockRecipientsList(); + renderCreateModal({ editingAlert: createEditingAlert() }); + + // When + await user.click(screen.getByRole("button", { name: /^test$/i })); + + // Then + expect( + await screen.findByText( + "These filters did not match any findings for the latest scan.", + ), + ).toBeVisible(); + expect(screen.queryByText("Would fire")).not.toBeInTheDocument(); + expect(screen.queryByText("Would not fire")).not.toBeInTheDocument(); + }); + + it("should render preview errors inline in edit mode", async () => { + // Given + const user = userEvent.setup(); + alertsActionMocks.previewAlertCondition.mockResolvedValue({ + error: "Invalid condition", + }); + mockRecipientsList(); + renderCreateModal({ editingAlert: createEditingAlert() }); + + // When + await user.click(screen.getByRole("button", { name: /^test$/i })); + + // Then + const errorMessage = await screen.findByText(/invalid condition/i); + expect(errorMessage).toBeVisible(); + expect(errorMessage).toHaveClass("text-text-error-primary"); + }); + + it("should hydrate advanced edit mode filters and normalize them on save", async () => { + // Given + const user = userEvent.setup(); + const advancedCondition: AlertCondition = { + op: ALERT_BOOLEAN_OPS.NOT, + child: { + op: ALERT_AGGREGATE_OPS.COUNT_GTE, + filter: { severity: ["critical"] }, + value: 1, + }, + }; + const onSubmit = vi + .fn() + .mockResolvedValue({ ok: true, alertId: "alert-1" }); + alertsActionMocks.seedAlertRule.mockResolvedValue({ + data: { + attributes: { + condition: { + op: ALERT_AGGREGATE_OPS.ANY, + filter: { severity: ["critical"] }, + }, + schema_version: 1, + warnings: [], + }, + }, + }); + mockRecipientsList(); + renderCreateModal({ + editingAlert: createEditingAlert({ + condition: advancedCondition, + recipient_emails: ["security@example.com"], + }), + onSubmit, + }); + + // When + await user.click(screen.getByRole("button", { name: /^save$/i })); + + // Then + await waitFor(() => + expect(onSubmit).toHaveBeenCalledWith( + expect.objectContaining({ + name: "Existing alert", + recipientEmails: ["security@example.com"], + condition: { + op: ALERT_AGGREGATE_OPS.ANY, + filter: { severity: ["critical"] }, + }, + }), + ), + ); + expect( + screen.queryByText(/advanced condition preserved/i), + ).not.toBeInTheDocument(); + }); +}); diff --git a/ui/app/(prowler)/alerts/_components/__tests__/alerts-manager.test.tsx b/ui/app/(prowler)/alerts/_components/__tests__/alerts-manager.test.tsx new file mode 100644 index 0000000000..1ee2c67302 --- /dev/null +++ b/ui/app/(prowler)/alerts/_components/__tests__/alerts-manager.test.tsx @@ -0,0 +1,305 @@ +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { isValidElement, type ReactNode } from "react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { + ALERT_AGGREGATE_OPS, + ALERT_TRIGGER_KINDS, + type AlertRule, +} from "@/app/(prowler)/alerts/_types"; + +import { AlertsManager } from "../alerts-manager"; + +const actionMocks = vi.hoisted(() => ({ + deleteAlert: vi.fn(), + disableAlert: vi.fn(), + enableAlert: vi.fn(), + updateAlert: vi.fn(), +})); + +const routerMocks = vi.hoisted(() => ({ + refresh: vi.fn(), + replace: vi.fn(), + push: vi.fn(), + currentSearch: "", +})); + +const toastMock = vi.hoisted(() => vi.fn()); + +vi.mock("@/app/(prowler)/alerts/_actions", () => actionMocks); + +vi.mock("next/link", () => ({ + default: ({ + children, + href, + className, + }: { + children: ReactNode; + href: string; + className?: string; + }) => ( + + {children} + + ), +})); + +vi.mock("@/lib", () => ({ + cn: (...classes: Array) => + classes.filter(Boolean).join(" "), +})); + +vi.mock("next/navigation", () => ({ + usePathname: () => "/alerts", + useRouter: () => routerMocks, + useSearchParams: () => new URLSearchParams(routerMocks.currentSearch), +})); + +vi.mock("@/components/ui", () => ({ + useToast: () => ({ toast: toastMock }), +})); + +vi.mock("@/components/shadcn", () => ({ + Button: ({ + asChild, + children, + disabled, + onClick, + variant, + }: { + asChild?: boolean; + children: ReactNode; + disabled?: boolean; + onClick?: () => void; + variant?: string; + }) => { + if (asChild && isValidElement(children)) { + return {children}; + } + + return ( + + ); + }, +})); + +vi.mock("../alert-form-modal", () => ({ + AlertFormModal: ({ + open, + editingAlert, + onOpenChange, + }: { + open: boolean; + editingAlert?: AlertRule | null; + onOpenChange: (open: boolean) => void; + }) => + open ? ( +
    + + {editingAlert?.attributes.name} +
    + ) : null, +})); + +vi.mock("../alerts-empty-state", () => ({ + AlertsEmptyState: () =>
    No alerts
    , +})); + +const makeAlert = (enabled: boolean): AlertRule => ({ + id: enabled ? "enabled-alert" : "disabled-alert", + type: "alert-rules", + attributes: { + name: enabled ? "Enabled alert" : "Disabled alert", + description: "", + enabled, + trigger: ALERT_TRIGGER_KINDS.AFTER_SCAN, + condition: { + op: ALERT_AGGREGATE_OPS.ANY, + filter: { severity: ["critical"] }, + }, + schema_version: 1, + recipient_emails: [], + inserted_at: "2026-01-01T00:00:00Z", + updated_at: "2026-01-01T00:00:00Z", + }, +}); + +const renderManager = (alerts: AlertRule[]) => + render( + , + ); + +describe("AlertsManager", () => { + beforeEach(() => { + vi.clearAllMocks(); + routerMocks.currentSearch = ""; + }); + + it("links to Findings from the alerts description", () => { + // Given + renderManager([]); + + // When + const findingsLink = screen.getByRole("link", { name: "Findings" }); + + // Then + expect(findingsLink).toHaveAttribute( + "href", + "/findings?filter[muted]=false&filter[status__in]=FAIL", + ); + expect(findingsLink.closest("[data-variant='link']")).toBeInTheDocument(); + expect(screen.getByRole("link", { name: "here." })).toHaveAttribute( + "href", + "https://docs.prowler.com/user-guide/tutorials/prowler-app-alerts", + ); + expect(screen.getByText(/get notified when findings match/i)).toBeVisible(); + }); + + it("opens the edit modal for an initial editing alert", () => { + // Given + const alert = makeAlert(true); + + // When + render( + , + ); + + // Then + expect( + screen.getByRole("dialog", { name: /edit alert/i }), + ).toHaveTextContent("Enabled alert"); + }); + + it("adds the edit alert id to the URL when opening the edit modal", async () => { + // Given + const user = userEvent.setup(); + const alert = makeAlert(true); + routerMocks.currentSearch = "page=2&filter[enabled]=true"; + renderManager([alert]); + + // When + await user.click( + screen.getByRole("button", { name: /actions for enabled alert/i }), + ); + await user.click(screen.getByRole("menuitem", { name: /edit/i })); + + // Then + expect(routerMocks.replace).toHaveBeenCalledWith( + "/alerts?page=2&filter%5Benabled%5D=true&edit=enabled-alert", + { scroll: false }, + ); + expect( + screen.getByRole("dialog", { name: /edit alert/i }), + ).toHaveTextContent("Enabled alert"); + }); + + it("removes only the edit alert id from the URL when closing the edit modal", async () => { + // Given + const user = userEvent.setup(); + const alert = makeAlert(true); + routerMocks.currentSearch = "page=2&edit=enabled-alert"; + render( + , + ); + + // When + await user.click(screen.getByRole("button", { name: /close modal/i })); + + // Then + expect(routerMocks.replace).toHaveBeenCalledWith("/alerts?page=2", { + scroll: false, + }); + }); + + it("shows a success toast after disabling an alert", async () => { + // Given + const user = userEvent.setup(); + const alert = makeAlert(true); + actionMocks.disableAlert.mockResolvedValue({ data: alert }); + renderManager([alert]); + + // When + await user.click( + screen.getByRole("button", { name: /actions for enabled alert/i }), + ); + await user.click(screen.getByRole("menuitem", { name: /disable/i })); + + // Then + await waitFor(() => + expect(toastMock).toHaveBeenCalledWith({ + title: "Alert disabled", + description: "Enabled alert", + }), + ); + }); + + it("shows a success toast after enabling an alert", async () => { + // Given + const user = userEvent.setup(); + const alert = makeAlert(false); + actionMocks.enableAlert.mockResolvedValue({ data: alert }); + renderManager([alert]); + + // When + await user.click( + screen.getByRole("button", { name: /actions for disabled alert/i }), + ); + await user.click(screen.getByRole("menuitem", { name: /enable/i })); + + // Then + await waitFor(() => + expect(toastMock).toHaveBeenCalledWith({ + title: "Alert enabled", + description: "Disabled alert", + }), + ); + }); +}); diff --git a/ui/app/(prowler)/alerts/_components/__tests__/alerts-table.test.tsx b/ui/app/(prowler)/alerts/_components/__tests__/alerts-table.test.tsx new file mode 100644 index 0000000000..3cea620aa4 --- /dev/null +++ b/ui/app/(prowler)/alerts/_components/__tests__/alerts-table.test.tsx @@ -0,0 +1,253 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import type { ReactNode } from "react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { + ALERT_AGGREGATE_OPS, + ALERT_TRIGGER_KINDS, + type AlertRule, +} from "@/app/(prowler)/alerts/_types"; + +import { AlertsTable } from "../alerts-table"; + +const navigationMocks = vi.hoisted(() => ({ + routerPush: vi.fn(), + currentSearch: "", +})); + +vi.mock("next/navigation", () => ({ + usePathname: () => "/alerts", + useRouter: () => ({ push: navigationMocks.routerPush }), + useSearchParams: () => new URLSearchParams(navigationMocks.currentSearch), +})); + +vi.mock("@/components/ui/table/data-table", () => ({ + DataTable: ({ + columns, + data, + metadata, + }: { + columns: { + id?: string; + size?: number; + minSize?: number; + cell?: (context: { row: { original: AlertRule } }) => ReactNode; + }[]; + data: AlertRule[]; + metadata?: { pagination?: { count?: number } }; + }) => ( +
    + {metadata?.pagination?.count !== undefined && ( + {metadata.pagination.count} Total Entries + )} + + + + {columns.map((column) => ( + + ))} + + + + {data.map((alert) => ( + + {columns.map((column) => ( + + ))} + + ))} + +
    + +
    + {column.cell?.({ row: { original: alert } })} +
    +
    + ), +})); + +vi.mock("@/components/ui/table/data-table-column-header", () => ({ + DataTableColumnHeader: ({ title }: { title: string }) => {title}, +})); + +interface AlertRuleOverrides extends Partial> { + attributes?: Partial; +} + +const makeRule = (overrides: AlertRuleOverrides = {}): AlertRule => ({ + id: overrides.id ?? "alert-1", + type: "alert-rules", + attributes: { + name: "Critical findings", + description: "Notify security", + enabled: true, + trigger: ALERT_TRIGGER_KINDS.AFTER_SCAN, + condition: { + op: ALERT_AGGREGATE_OPS.ANY, + filter: { severity: ["critical"] }, + }, + schema_version: 1, + recipient_emails: ["security@example.com"], + inserted_at: "2026-01-01T10:00:00Z", + updated_at: "2026-01-02T11:30:00Z", + ...overrides.attributes, + }, +}); + +describe("AlertsTable", () => { + beforeEach(() => { + navigationMocks.currentSearch = ""; + navigationMocks.routerPush.mockClear(); + }); + + it("should render alert rows with dropdown actions and shared pagination", () => { + // Given / When + render( + , + ); + + // Then + expect( + screen.getByRole("cell", { name: /critical findings/i }), + ).toBeVisible(); + expect( + screen.getByRole("button", { name: /actions for critical findings/i }), + ).toBeVisible(); + expect( + screen.queryByRole("button", { name: /edit critical findings/i }), + ).not.toBeInTheDocument(); + expect(screen.getByText(/12 total entries/i)).toBeVisible(); + expect(screen.getByTestId("column-actions")).toHaveAttribute( + "data-size", + "72", + ); + expect(screen.getByTestId("column-name")).toHaveAttribute( + "data-size", + "320", + ); + expect(screen.getByTestId("column-inserted_at")).toHaveAttribute( + "data-size", + "170", + ); + expect(screen.getByTestId("column-updated_at")).toHaveAttribute( + "data-size", + "170", + ); + expect(screen.getByText("Jan 01, 2026")).toBeVisible(); + expect(screen.getByText("Jan 02, 2026")).toBeVisible(); + expect( + screen.queryByRole("button", { name: /run preview|test/i }), + ).not.toBeInTheDocument(); + expect( + screen.queryByRole("link", { name: /critical findings/i }), + ).not.toBeInTheDocument(); + }); + + it("should truncate long descriptions in the name column", () => { + // Given + const description = + "This alert description is intentionally long enough to overflow the alerts table if it is not constrained by the cell renderer."; + + // When + render( + , + ); + + // Then + expect(screen.getByText(description)).toHaveClass("truncate"); + expect(screen.getByText(description).parentElement).toHaveClass( + "max-w-[320px]", + ); + expect(screen.getByText(description)).toHaveAttribute("title", description); + }); + + it("should call row action callbacks for edit, toggle, and delete", async () => { + // Given + const user = userEvent.setup(); + const alert = makeRule({ id: "alert-enabled" }); + const onEdit = vi.fn(); + const onToggleEnabled = vi.fn(); + const onDelete = vi.fn(); + render( + , + ); + + // When + await user.click( + screen.getByRole("button", { name: /actions for critical findings/i }), + ); + await user.click(screen.getByRole("menuitem", { name: /edit/i })); + await user.click( + screen.getByRole("button", { name: /actions for critical findings/i }), + ); + await user.click(screen.getByRole("menuitem", { name: /disable/i })); + await user.click( + screen.getByRole("button", { name: /actions for critical findings/i }), + ); + await user.click(screen.getByRole("menuitem", { name: /delete/i })); + + // Then + expect(onEdit).toHaveBeenCalledWith(alert); + expect(onToggleEnabled).toHaveBeenCalledWith(alert); + expect(onDelete).toHaveBeenCalledWith(alert); + }); + + it("should edit the alert directly when clicking the alert name", async () => { + // Given + const user = userEvent.setup(); + const alert = makeRule(); + const onEdit = vi.fn(); + render( + , + ); + + // When + await user.click(screen.getByRole("button", { name: "Critical findings" })); + + // Then + expect(onEdit).toHaveBeenCalledWith(alert); + expect(screen.queryByRole("menuitem", { name: /edit/i })).toBeNull(); + expect(screen.queryByRole("menuitem", { name: /disable/i })).toBeNull(); + expect(screen.queryByRole("menuitem", { name: /delete/i })).toBeNull(); + }); +}); diff --git a/ui/app/(prowler)/alerts/_components/__tests__/seed-from-findings-button.test.tsx b/ui/app/(prowler)/alerts/_components/__tests__/seed-from-findings-button.test.tsx new file mode 100644 index 0000000000..b6fca816df --- /dev/null +++ b/ui/app/(prowler)/alerts/_components/__tests__/seed-from-findings-button.test.tsx @@ -0,0 +1,384 @@ +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import type { ComponentProps, ReactNode } from "react"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import type { AlertCondition } from "@/app/(prowler)/alerts/_types"; +import type { + AlertFormSubmitResult, + AlertFormValues, +} from "@/app/(prowler)/alerts/_types/alert-form"; + +const routerMocks = vi.hoisted(() => ({ + push: vi.fn(), + refresh: vi.fn(), +})); + +const actionMocks = vi.hoisted(() => ({ + createAlert: vi.fn(), + seedAlertRule: vi.fn(), +})); + +const toastMock = vi.hoisted(() => vi.fn()); + +vi.mock("next/navigation", () => ({ + useRouter: () => routerMocks, +})); + +vi.mock("@/components/ui", () => ({ + ToastAction: ({ + asChild, + children, + ...props + }: ComponentProps<"button"> & { + asChild?: boolean; + children?: ReactNode; + }) => (asChild ? children : ), + useToast: () => ({ toast: toastMock }), +})); + +vi.mock("@/app/(prowler)/alerts/_actions", () => ({ + createAlert: actionMocks.createAlert, + seedAlertRule: actionMocks.seedAlertRule, +})); + +vi.mock("@/app/(prowler)/alerts/_components/alert-form-modal", () => ({ + AlertFormModal: ({ + open, + seededCondition, + selectedFindingsFilterChips, + defaultName, + onSubmit, + }: { + open: boolean; + seededCondition?: AlertCondition | null; + selectedFindingsFilterChips?: Array<{ + label: string; + displayValue?: string; + value: string; + }>; + defaultName?: string; + onSubmit: (values: AlertFormValues) => Promise; + }) => + open ? ( +
    + + {JSON.stringify(seededCondition)} + + + {(selectedFindingsFilterChips ?? []) + .map((chip) => `${chip.label}:${chip.displayValue ?? chip.value}`) + .join("|")} + + +
    + ) : null, +})); + +import { SeedFromFindingsButton } from "../seed-from-findings-button"; + +describe("SeedFromFindingsButton", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it("should explain why creating an alert is disabled when no real filters are applied", async () => { + // Given + const user = userEvent.setup(); + render(); + + // When + const button = screen.getByRole("button", { + name: /Create Alert/i, + }); + const tooltipTrigger = button.parentElement; + expect(tooltipTrigger).not.toBeNull(); + await user.hover(tooltipTrigger as HTMLElement); + + // Then + expect(button).toBeDisabled(); + expect( + await screen.findAllByText(/at least one findings filter/i), + ).not.toHaveLength(0); + }); + + it("should enable creation from the first real filter, including unsupported backend filters", () => { + // Given / When + render( + , + ); + + // Then + expect( + screen.getByRole("button", { name: /Create Alert/i }), + ).not.toBeDisabled(); + expect(screen.getByRole("button", { name: /Create Alert/i })).toHaveClass( + "h-10", + ); + }); + + it("should add all severities when Findings only has non-portable default filters", async () => { + // Given + const user = userEvent.setup(); + const seededCondition: AlertCondition = { + op: "any", + filter: { + severity: ["critical", "high", "medium", "low", "informational"], + }, + }; + actionMocks.seedAlertRule.mockResolvedValue({ + data: { + attributes: { + condition: seededCondition, + schema_version: 1, + warnings: [], + }, + }, + }); + const filterBag = { + "filter[status__in]": "FAIL", + "filter[muted]": "false", + "filter[scan__in]": "11111111-1111-1111-1111-111111111111", + }; + render(); + + // When + await user.click(screen.getByRole("button", { name: /Create Alert/i })); + + // Then + await waitFor(() => + expect(actionMocks.seedAlertRule).toHaveBeenCalledWith({ + ...filterBag, + "filter[severity__in]": [ + "critical", + "high", + "medium", + "low", + "informational", + ], + }), + ); + expect(screen.getByRole("dialog", { name: /create alert/i })).toBeVisible(); + expect(screen.getByTestId("seeded-condition")).toHaveTextContent( + "severity", + ); + }); + + it("should seed from the full Findings filter bag before opening the modal", async () => { + // Given + const user = userEvent.setup(); + const seededCondition: AlertCondition = { + op: "any", + filter: { severity: ["critical", "high"] }, + }; + actionMocks.seedAlertRule.mockResolvedValue({ + data: { + attributes: { + condition: seededCondition, + schema_version: 1, + warnings: [], + }, + }, + }); + const filterBag = { + "filter[status__in]": "FAIL", + "filter[muted]": "false", + "filter[scan__in]": "11111111-1111-1111-1111-111111111111", + "filter[severity__in]": "critical,high", + }; + render(); + + // When + await user.click(screen.getByRole("button", { name: /Create Alert/i })); + + // Then + await waitFor(() => + expect(actionMocks.seedAlertRule).toHaveBeenCalledWith(filterBag), + ); + expect(screen.getByRole("dialog", { name: /create alert/i })).toBeVisible(); + expect(routerMocks.push).not.toHaveBeenCalled(); + expect(screen.getByTestId("selected-filter-chips")).toHaveTextContent( + /severity:\+2/i, + ); + expect(screen.getByTestId("seeded-condition")).toHaveTextContent( + "severity", + ); + expect(screen.getByTestId("selected-filter-chips")).not.toHaveTextContent( + /status/i, + ); + }); + + it("should create the alert through the existing alert action from the modal", async () => { + // Given + const user = userEvent.setup(); + const seededCondition: AlertCondition = { + op: "any", + filter: { severity: ["critical"] }, + }; + actionMocks.seedAlertRule.mockResolvedValue({ + data: { + attributes: { + condition: seededCondition, + schema_version: 1, + warnings: [], + }, + }, + }); + actionMocks.createAlert.mockResolvedValue({ + data: { + id: "alert-1", + attributes: { name: "Findings filter alert" }, + }, + }); + render( + , + ); + + // When + await user.click(screen.getByRole("button", { name: /Create Alert/i })); + await user.click( + screen.getByRole("button", { name: /submit mock alert/i }), + ); + + // Then + await waitFor(() => + expect(actionMocks.createAlert).toHaveBeenCalledWith( + expect.objectContaining({ + name: "Findings filter alert", + trigger: "after_scan", + condition: seededCondition, + recipientEmails: ["security@example.com"], + }), + ), + ); + expect(routerMocks.refresh).toHaveBeenCalled(); + expect(toastMock).toHaveBeenCalledWith( + expect.objectContaining({ + title: "Alert created", + action: expect.anything(), + }), + ); + }); + + it("should add a toast action to navigate to alerts after creating an alert", async () => { + // Given + const user = userEvent.setup(); + actionMocks.seedAlertRule.mockResolvedValue({ + data: { + attributes: { + condition: { op: "any", filter: { severity: ["critical"] } }, + schema_version: 1, + warnings: [], + }, + }, + }); + actionMocks.createAlert.mockResolvedValue({ + data: { + id: "alert-1", + attributes: { name: "Findings filter alert" }, + }, + }); + render( + , + ); + + // When + await user.click(screen.getByRole("button", { name: /Create Alert/i })); + await user.click( + screen.getByRole("button", { name: /submit mock alert/i }), + ); + + // Then + await waitFor(() => expect(toastMock).toHaveBeenCalled()); + const toastAction = toastMock.mock.calls[0][0].action; + render(toastAction); + expect(screen.getByRole("link", { name: /view alerts/i })).toHaveAttribute( + "href", + "/alerts", + ); + }); + + it("should show a toast and keep the modal closed when seed fails", async () => { + // Given + const user = userEvent.setup(); + actionMocks.seedAlertRule.mockResolvedValue({ + error: "invalid_shape", + }); + render( + , + ); + + // When + await user.click(screen.getByRole("button", { name: /Create Alert/i })); + + // Then + await waitFor(() => + expect(toastMock).toHaveBeenCalledWith( + expect.objectContaining({ + variant: "destructive", + title: "Alert seed failed", + }), + ), + ); + expect( + screen.queryByRole("dialog", { name: /create alert/i }), + ).not.toBeInTheDocument(); + }); + + it("should render disabled as a Cloud-only feature in OSS", () => { + // Given + render( + , + ); + + // When + const button = screen.getByRole("button", { name: /Create Alert/i }); + + // Then + expect(button).toBeDisabled(); + expect(button.className).not.toContain("min-w"); + expect(button).not.toHaveClass("justify-start"); + const pricingLink = screen.getByRole("link", { + name: /available in prowler cloud/i, + }); + expect(pricingLink).toHaveAttribute("href", "https://prowler.com/pricing"); + expect(pricingLink).toHaveClass("whitespace-nowrap"); + expect(pricingLink).toHaveTextContent("Available in Prowler Cloud"); + expect(pricingLink.closest("button")).toBeNull(); + expect(screen.queryByRole("tooltip")).not.toBeInTheDocument(); + expect(actionMocks.seedAlertRule).not.toHaveBeenCalled(); + }); +}); diff --git a/ui/app/(prowler)/alerts/_components/alert-form-modal.tsx b/ui/app/(prowler)/alerts/_components/alert-form-modal.tsx new file mode 100644 index 0000000000..e3529d4056 --- /dev/null +++ b/ui/app/(prowler)/alerts/_components/alert-form-modal.tsx @@ -0,0 +1,613 @@ +"use client"; + +import { useState } from "react"; + +import { + previewAlertCondition, + seedAlertRule, +} from "@/app/(prowler)/alerts/_actions"; +import { listAlertRecipients } from "@/app/(prowler)/alerts/_actions/recipients"; +import { + ALERT_TRIGGER_KINDS, + type AlertCondition, + type AlertPreviewResponse, + type AlertRecipient, + type AlertRule, + type AlertTriggerKind, +} from "@/app/(prowler)/alerts/_types"; +import type { FilterChip } from "@/components/filters/filter-summary-strip"; +import { FilterSummaryStrip } from "@/components/filters/filter-summary-strip"; +import { FindingsFilterBatchControls } from "@/components/findings/findings-filters"; +import { + Badge, + Button, + Card, + CardContent, + Field, + FieldError, + FieldLabel, + Input, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + Skeleton, + Textarea, +} from "@/components/shadcn"; +import { Modal } from "@/components/shadcn/modal"; +import { + MultiSelect, + MultiSelectContent, + MultiSelectItem, + MultiSelectSelectAll, + MultiSelectSeparator, + MultiSelectTrigger, + MultiSelectValue, +} from "@/components/shadcn/select/multiselect"; +import { useMountEffect } from "@/hooks/use-mount-effect"; +import type { ScanEntity } from "@/types"; +import type { ProviderProps } from "@/types/providers"; + +import { + getAlertFormDefaults, + getEmptyAlertFormDefaults, + getFindingsFiltersFromAlertCondition, +} from "../_lib/alert-adapter"; +import { alertFormSchema } from "../_lib/alert-form-schema"; +import type { + AlertFormSubmitResult, + AlertFormValues, +} from "../_types/alert-form"; +import { ALERT_NOTIFICATION_METHODS } from "../_types/alert-form"; + +interface AlertFormModalProps { + open: boolean; + defaultFrequency: AlertTriggerKind; + providers?: ProviderProps[]; + completedScanIds?: string[]; + scanDetails?: { [key: string]: ScanEntity }[]; + uniqueRegions?: string[]; + uniqueServices?: string[]; + uniqueResourceTypes?: string[]; + uniqueCategories?: string[]; + uniqueGroups?: string[]; + editingAlert?: AlertRule | null; + seededCondition?: AlertCondition | null; + selectedFindingsFilterChips?: FilterChip[]; + defaultName?: string; + onOpenChange: (open: boolean) => void; + onSubmit: (values: AlertFormValues) => Promise; +} + +interface FormErrors { + name?: string; + recipientEmails?: string; + root?: string; +} + +const normalizeEmail = (email: string): string => email.trim().toLowerCase(); + +const getRecipientEmails = (selectedEmails: Set): string[] => + Array.from(selectedEmails); + +const ALERT_FREQUENCY_OPTIONS = [ + { + value: ALERT_TRIGGER_KINDS.AFTER_SCAN, + label: "After each scan", + }, + { + value: ALERT_TRIGGER_KINDS.DAILY, + label: "Daily digest", + }, + { + value: ALERT_TRIGGER_KINDS.BOTH, + label: "After each scan and daily", + }, +] as const; + +const ALERT_SEED_ERROR = "Apply at least one alert-compatible Findings filter."; + +const serializeCondition = (condition: AlertCondition | null): string => + condition ? JSON.stringify(condition) : "none"; + +const getAlertFormModalResetKey = ({ + open, + defaultFrequency, + editingAlert, + seededCondition, +}: Pick< + AlertFormModalProps, + "open" | "defaultFrequency" | "editingAlert" | "seededCondition" +>): string => + [ + open ? "open" : "closed", + editingAlert?.id ?? "create", + editingAlert?.attributes.updated_at ?? "", + defaultFrequency, + serializeCondition(seededCondition ?? null), + ].join("|"); + +const allowInitialDialogFocus = () => undefined; + +const uniqueValues = (values: string[]): string[] => + Array.from(new Set(values)); + +interface PreviewState { + status: "success" | "error"; + data?: AlertPreviewResponse; + error?: string; +} + +const formatPreviewNumber = (value: number): string => + new Intl.NumberFormat("en-US").format(value); + +const getPreviewSeverityLabel = (severity: string): string => + severity.charAt(0).toUpperCase() + severity.slice(1); + +const getPreviewMessage = (data: AlertPreviewResponse): string => { + const totalFindings = data.summary.finding_count_total ?? 0; + if (totalFindings === 0) { + return "These filters did not match any findings for the latest scan."; + } + + const findingLabel = totalFindings === 1 ? "finding" : "findings"; + const topSeverity = data.summary.top_severity; + const severityClause = topSeverity + ? `, including ${getPreviewSeverityLabel(topSeverity)} severity` + : ""; + + return `It found ${formatPreviewNumber(totalFindings)} ${findingLabel}${severityClause}.`; +}; + +const PreviewSummarySkeleton = () => ( + + +
    + + +
    + +
    +
    +); + +const PreviewSummary = ({ preview }: { preview: PreviewState }) => { + if (preview.status === "error") { + return ( + + +
    + + Test result + + Error +
    +

    {preview.error}

    +
    +
    + ); + } + + const data = preview.data; + if (!data) return null; + + return ( + + + + Test result + +

    + {getPreviewMessage(data)} +

    +
    +
    + ); +}; + +const normalizeFindingsFilterKey = (filterKey: string): string => + filterKey.startsWith("filter[") ? filterKey : `filter[${filterKey}]`; + +interface AlertRecipientsSelectProps { + selectedEmails: Set; + onValuesChange: (emails: string[]) => void; +} + +interface RecipientOption { + email: string; + status?: AlertRecipient["attributes"]["status"]; +} + +const getRecipientStatusLabel = ( + status: AlertRecipient["attributes"]["status"], +): string => status.charAt(0).toUpperCase() + status.slice(1); + +const getRecipientOptions = ( + recipients: AlertRecipient[], + selectedEmails: string[], +): RecipientOption[] => { + const options = new Map(); + + recipients.forEach((recipient) => { + const email = normalizeEmail(recipient.attributes.email); + if (!email) return; + options.set(email, { email, status: recipient.attributes.status }); + }); + + selectedEmails.forEach((email) => { + const normalizedEmail = normalizeEmail(email); + if (!normalizedEmail || options.has(normalizedEmail)) return; + options.set(normalizedEmail, { email: normalizedEmail }); + }); + + return Array.from(options.values()).sort((left, right) => + left.email.localeCompare(right.email), + ); +}; + +const AlertRecipientsSelect = ({ + selectedEmails, + onValuesChange, +}: AlertRecipientsSelectProps) => { + const [recipients, setRecipients] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useMountEffect(() => { + listAlertRecipients({ + "page[size]": "100", + sort: "email", + }).then((result) => { + setLoading(false); + if (result?.error) { + setRecipients([]); + setError(result.error); + return; + } + setRecipients(result.data); + setError(null); + }); + }); + + const selectedValues = Array.from(selectedEmails); + const options = getRecipientOptions(recipients, selectedValues); + + return ( +
    + + + + + + option.email)} + > + Select All + + + {options.map((option) => ( + + {option.email} + {option.status && ( + + {getRecipientStatusLabel(option.status)} + + )} + + ))} + + + {error &&

    {error}

    } +
    + ); +}; + +export const AlertFormModal = (props: AlertFormModalProps) => { + const resetKey = getAlertFormModalResetKey(props); + + return ; +}; + +const AlertFormModalContent = ({ + open, + defaultFrequency, + providers = [], + completedScanIds = [], + scanDetails = [], + uniqueRegions = [], + uniqueServices = [], + uniqueResourceTypes = [], + uniqueCategories = [], + uniqueGroups = [], + editingAlert = null, + seededCondition = null, + selectedFindingsFilterChips = [], + defaultName = "Findings filter alert", + onOpenChange, + onSubmit, +}: AlertFormModalProps) => { + const defaults = editingAlert + ? getAlertFormDefaults(editingAlert) + : getEmptyAlertFormDefaults(defaultFrequency, seededCondition ?? undefined); + const initialName = editingAlert + ? defaults.name + : defaults.name || defaultName; + + // Local state needed: user edits are buffered until the modal form is submitted. + const [name, setName] = useState(initialName); + const [description, setDescription] = useState(defaults.description); + const [frequency, setFrequency] = useState( + defaults.frequency, + ); + const [pendingFilters, setPendingFilters] = useState< + Record + >( + editingAlert + ? getFindingsFiltersFromAlertCondition(editingAlert.attributes.condition) + : {}, + ); + const [selectedRecipientEmails, setSelectedRecipientEmails] = useState( + () => new Set(defaults.recipientEmails.map(normalizeEmail)), + ); + const [errors, setErrors] = useState({}); + const [saving, setSaving] = useState(false); + const [previewLoading, setPreviewLoading] = useState(false); + const [preview, setPreview] = useState(null); + + const submitLabel = editingAlert ? "Save" : "Create"; + + const setRecipientEmails = (emails: string[]) => + setSelectedRecipientEmails( + new Set(emails.map(normalizeEmail).filter(Boolean)), + ); + + const setPendingFilter = (filterKey: string, values: string[]) => { + setPendingFilters((current) => ({ + ...current, + [normalizeFindingsFilterKey(filterKey)]: uniqueValues(values), + })); + setPreview(null); + }; + + const getPendingFilterValue = (filterKey: string): string[] => + pendingFilters[normalizeFindingsFilterKey(filterKey)] ?? []; + + const buildCurrentValues = (condition: AlertCondition): AlertFormValues => ({ + name, + description, + method: ALERT_NOTIFICATION_METHODS.EMAIL, + frequency, + condition, + recipientEmails: getRecipientEmails(selectedRecipientEmails), + enabled: defaults.enabled, + }); + + const handlePreview = async () => { + if (!editingAlert) return; + + const seedResult = await seedAlertRule(pendingFilters); + if (seedResult?.error) { + setPreview({ + status: "error", + error: ALERT_SEED_ERROR, + }); + return; + } + + const values = buildCurrentValues(seedResult.data.attributes.condition); + const parsed = alertFormSchema.safeParse(values); + if (!parsed.success) { + setPreview({ + status: "error", + error: "Fix alert fields before running test.", + }); + return; + } + + setPreviewLoading(true); + const result = await previewAlertCondition({ + condition: parsed.data.condition, + }); + setPreviewLoading(false); + + if (result?.error) { + setPreview({ status: "error", error: result.error }); + return; + } + + const previewData = result.data.attributes as AlertPreviewResponse; + if (previewData.evaluation_failed) { + setPreview({ + status: "error", + error: previewData.last_error ?? "Preview evaluation failed.", + }); + return; + } + + setPreview({ status: "success", data: previewData }); + }; + + const handleSubmit = async () => { + const seedResult = editingAlert + ? await seedAlertRule(pendingFilters) + : null; + if (seedResult?.error) { + setErrors({ root: ALERT_SEED_ERROR }); + return; + } + + const values = buildCurrentValues( + seedResult?.data.attributes.condition ?? defaults.condition, + ); + const parsed = alertFormSchema.safeParse(values); + if (!parsed.success) { + const fieldErrors = parsed.error.flatten().fieldErrors; + setErrors({ + name: fieldErrors.name?.[0], + recipientEmails: fieldErrors.recipientEmails?.[0], + }); + return; + } + + setSaving(true); + const result = await onSubmit(parsed.data); + setSaving(false); + if (result.ok) { + setErrors({}); + onOpenChange(false); + return; + } + setErrors({ root: result.error ?? "Could not save alert." }); + }; + + return ( + +
    + + + Name + setName(event.target.value)} + /> + {errors.name && {errors.name}} + + + Description +