diff --git a/.env b/.env index 447fd3c1b4..4a0e1ef2d4 100644 --- a/.env +++ b/.env @@ -6,6 +6,7 @@ PROWLER_UI_VERSION="stable" AUTH_URL=http://localhost:3000 API_BASE_URL=http://prowler-api:8080/api/v1 +NEXT_PUBLIC_API_BASE_URL=${API_BASE_URL} NEXT_PUBLIC_API_DOCS_URL=http://prowler-api:8080/api/v1/docs AUTH_TRUST_HOST=true UI_PORT=3000 @@ -130,7 +131,7 @@ SENTRY_ENVIRONMENT=local SENTRY_RELEASE=local #### Prowler release version #### -NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.6.0 +NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.7.5 # Social login credentials SOCIAL_GOOGLE_OAUTH_CALLBACK_URL="${AUTH_URL}/api/auth/callback/google" @@ -142,8 +143,7 @@ SOCIAL_GITHUB_OAUTH_CLIENT_ID="" SOCIAL_GITHUB_OAUTH_CLIENT_SECRET="" # Single Sign-On (SSO) -SAML_PUBLIC_CERT="" -SAML_PRIVATE_KEY="" +SAML_SSO_CALLBACK_URL="${AUTH_URL}/api/auth/callback/saml" # Lighthouse tracing LANGSMITH_TRACING=false diff --git a/.github/workflows/api-build-lint-push-containers.yml b/.github/workflows/api-build-lint-push-containers.yml index 2165eb7263..5fee1a2d8d 100644 --- a/.github/workflows/api-build-lint-push-containers.yml +++ b/.github/workflows/api-build-lint-push-containers.yml @@ -76,7 +76,7 @@ jobs: password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 + uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 - name: Build and push container image (latest) # Comment the following line for testing diff --git a/.github/workflows/api-codeql.yml b/.github/workflows/api-codeql.yml index 620d9cbaed..2d38107960 100644 --- a/.github/workflows/api-codeql.yml +++ b/.github/workflows/api-codeql.yml @@ -48,12 +48,12 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 + uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 with: languages: ${{ matrix.language }} config-file: ./.github/codeql/api-codeql-config.yml - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 + uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/api-pull-request.yml b/.github/workflows/api-pull-request.yml index db826d7a46..0e55d83bff 100644 --- a/.github/workflows/api-pull-request.yml +++ b/.github/workflows/api-pull-request.yml @@ -136,12 +136,6 @@ jobs: run: | poetry check --lock - - name: Prevents known compatibility error between lxml and libxml2/libxmlsec versions - https://github.com/xmlsec/python-xmlsec/issues/320 - working-directory: ./api - if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' - run: | - poetry run pip install --force-reinstall --no-binary lxml lxml - - name: Lint with ruff working-directory: ./api if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' @@ -169,9 +163,9 @@ jobs: - name: Safety working-directory: ./api if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' - # 76352 and 76353 come from SDK, but they cannot upgrade it yet. It does not affect API + # 76352, 76353, 77323 come from SDK, but they cannot upgrade it yet. It does not affect API run: | - poetry run safety check --ignore 70612,66963,74429,76352,76353 + poetry run safety check --ignore 70612,66963,74429,76352,76353,77323 - name: Vulture working-directory: ./api @@ -211,7 +205,7 @@ jobs: files_ignore: ${{ env.IGNORE_FILES }} - name: Set up Docker Buildx if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 + uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 - name: Build Container if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 diff --git a/.github/workflows/find-secrets.yml b/.github/workflows/find-secrets.yml index 9892364e37..53e17b3e2d 100644 --- a/.github/workflows/find-secrets.yml +++ b/.github/workflows/find-secrets.yml @@ -11,7 +11,7 @@ jobs: with: fetch-depth: 0 - name: TruffleHog OSS - uses: trufflesecurity/trufflehog@90694bf9af66e7536abc5824e7a87246dbf933cb # v3.88.35 + uses: trufflesecurity/trufflehog@6641d4ba5b684fffe195b9820345de1bf19f3181 # v3.89.2 with: path: ./ base: ${{ github.event.repository.default_branch }} diff --git a/.github/workflows/pull-request-check-changelog.yml b/.github/workflows/pull-request-check-changelog.yml index 19237b9c63..d0c41aad1d 100644 --- a/.github/workflows/pull-request-check-changelog.yml +++ b/.github/workflows/pull-request-check-changelog.yml @@ -9,6 +9,8 @@ jobs: if: contains(github.event.pull_request.labels.*.name, 'no-changelog') == false runs-on: ubuntu-latest permissions: + id-token: write + contents: read pull-requests: write env: MONITORED_FOLDERS: "api ui prowler" @@ -45,6 +47,7 @@ jobs: echo "EOF" >> $GITHUB_OUTPUT - name: Find existing changelog comment + if: github.event.pull_request.head.repo.full_name == github.repository id: find_comment uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e #v3.1.0 with: @@ -53,7 +56,7 @@ jobs: body-includes: '' - name: Comment on PR if changelog is missing - if: steps.check_folders.outputs.missing_changelogs != '' + if: github.event.pull_request.head.repo.full_name == github.repository && steps.check_folders.outputs.missing_changelogs != '' uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0 with: issue-number: ${{ github.event.pull_request.number }} @@ -67,7 +70,7 @@ jobs: Please add an entry to the corresponding `CHANGELOG.md` file to maintain a clear history of changes. - name: Comment on PR if all changelogs are present - if: steps.check_folders.outputs.missing_changelogs == '' + if: github.event.pull_request.head.repo.full_name == github.repository && steps.check_folders.outputs.missing_changelogs == '' uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0 with: issue-number: ${{ github.event.pull_request.number }} diff --git a/.github/workflows/sdk-build-lint-push-containers.yml b/.github/workflows/sdk-build-lint-push-containers.yml index 8182ef4c04..ec1d71a0cb 100644 --- a/.github/workflows/sdk-build-lint-push-containers.yml +++ b/.github/workflows/sdk-build-lint-push-containers.yml @@ -123,7 +123,7 @@ jobs: AWS_REGION: ${{ env.AWS_REGION }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 + uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 - name: Build and push container image (latest) if: github.event_name == 'push' diff --git a/.github/workflows/sdk-bump-version.yml b/.github/workflows/sdk-bump-version.yml index e5e5dba4e0..9c75928dfe 100644 --- a/.github/workflows/sdk-bump-version.yml +++ b/.github/workflows/sdk-bump-version.yml @@ -97,6 +97,7 @@ jobs: commit-message: "chore(release): Bump version to v${{ env.BUMP_VERSION_TO }}" branch: "version-bump-to-v${{ env.BUMP_VERSION_TO }}" title: "chore(release): Bump version to v${{ env.BUMP_VERSION_TO }}" + labels: no-changelog body: | ### Description @@ -135,6 +136,7 @@ jobs: commit-message: "chore(release): Bump version to v${{ env.PATCH_VERSION_TO }}" branch: "version-bump-to-v${{ env.PATCH_VERSION_TO }}" title: "chore(release): Bump version to v${{ env.PATCH_VERSION_TO }}" + labels: no-changelog body: | ### Description diff --git a/.github/workflows/sdk-codeql.yml b/.github/workflows/sdk-codeql.yml index 4420f5823d..10d5c93b45 100644 --- a/.github/workflows/sdk-codeql.yml +++ b/.github/workflows/sdk-codeql.yml @@ -56,12 +56,12 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 + uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 with: languages: ${{ matrix.language }} config-file: ./.github/codeql/sdk-codeql-config.yml - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 + uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/ui-build-lint-push-containers.yml b/.github/workflows/ui-build-lint-push-containers.yml index 74cf23499f..bce6ba6a07 100644 --- a/.github/workflows/ui-build-lint-push-containers.yml +++ b/.github/workflows/ui-build-lint-push-containers.yml @@ -30,6 +30,7 @@ env: # Container Registries PROWLERCLOUD_DOCKERHUB_REPOSITORY: prowlercloud PROWLERCLOUD_DOCKERHUB_IMAGE: prowler-ui + NEXT_PUBLIC_API_BASE_URL: http://prowler-api:8080/api/v1 jobs: repository-check: @@ -76,7 +77,7 @@ jobs: password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 + uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 - name: Build and push container image (latest) # Comment the following line for testing @@ -86,6 +87,7 @@ jobs: context: ${{ env.WORKING_DIRECTORY }} build-args: | NEXT_PUBLIC_PROWLER_RELEASE_VERSION=${{ env.SHORT_SHA }} + NEXT_PUBLIC_API_BASE_URL=${{ env.NEXT_PUBLIC_API_BASE_URL }} # Set push: false for testing push: true tags: | @@ -101,6 +103,7 @@ jobs: context: ${{ env.WORKING_DIRECTORY }} build-args: | NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v${{ env.RELEASE_TAG }} + NEXT_PUBLIC_API_BASE_URL=${{ env.NEXT_PUBLIC_API_BASE_URL }} push: true tags: | ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.RELEASE_TAG }} diff --git a/.github/workflows/ui-codeql.yml b/.github/workflows/ui-codeql.yml index ef95253c5f..674bdb8310 100644 --- a/.github/workflows/ui-codeql.yml +++ b/.github/workflows/ui-codeql.yml @@ -48,12 +48,12 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 + uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 with: languages: ${{ matrix.language }} config-file: ./.github/codeql/ui-codeql-config.yml - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 + uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/ui-pull-request.yml b/.github/workflows/ui-pull-request.yml index f217f31c3b..e82b892f48 100644 --- a/.github/workflows/ui-pull-request.yml +++ b/.github/workflows/ui-pull-request.yml @@ -34,21 +34,70 @@ jobs: uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ matrix.node-version }} + cache: 'npm' + cache-dependency-path: './ui/package-lock.json' - name: Install dependencies working-directory: ./ui - run: npm install + run: npm ci - name: Run Healthcheck working-directory: ./ui run: npm run healthcheck - name: Build the application working-directory: ./ui run: npm run build + + e2e-tests: + runs-on: ubuntu-latest + env: + AUTH_SECRET: 'fallback-ci-secret-for-testing' + AUTH_TRUST_HOST: true + NEXTAUTH_URL: http://localhost:3000 + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: '20.x' + cache: 'npm' + cache-dependency-path: './ui/package-lock.json' + - name: Install dependencies + working-directory: ./ui + run: npm ci + - name: Cache Playwright browsers + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + id: playwright-cache + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ hashFiles('ui/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-playwright- + - name: Install Playwright browsers + working-directory: ./ui + if: steps.playwright-cache.outputs.cache-hit != 'true' + run: npm run test:e2e:install + - name: Build the application + working-directory: ./ui + run: npm run build + - name: Run Playwright tests + working-directory: ./ui + run: npm run test:e2e + - name: Upload Playwright report + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + if: failure() + with: + name: playwright-report + path: ui/playwright-report/ + retention-days: 30 + test-container-build: runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 + uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 - name: Build Container uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 with: diff --git a/.gitignore b/.gitignore index d38969db40..1b5075705c 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,16 @@ junit-reports/ # Cursor files .cursorignore +.cursor/ + +# RooCode files +.roo/ +.rooignore +.roomodes + +# Cline files +.cline/ +.clineignore # Terraform .terraform* diff --git a/Dockerfile b/Dockerfile index 0e9fb89a03..5884bda9f3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.12.10-slim-bookworm AS build +FROM python:3.12.11-slim-bookworm AS build LABEL maintainer="https://github.com/prowler-cloud/prowler" LABEL org.opencontainers.image.source="https://github.com/prowler-cloud/prowler" @@ -6,7 +6,8 @@ LABEL org.opencontainers.image.source="https://github.com/prowler-cloud/prowler" ARG POWERSHELL_VERSION=7.5.0 # hadolint ignore=DL3008 -RUN apt-get update && apt-get install -y --no-install-recommends wget libicu72 \ +RUN apt-get update && apt-get install -y --no-install-recommends \ + wget libicu72 libunwind8 libssl3 libcurl4 ca-certificates apt-transport-https gnupg \ && rm -rf /var/lib/apt/lists/* # Install PowerShell @@ -46,10 +47,6 @@ ENV PATH="${HOME}/.local/bin:${PATH}" RUN pip install --no-cache-dir --upgrade pip && \ pip install --no-cache-dir poetry -# By default poetry does not compile Python source files to bytecode during installation. -# This speeds up the installation process, but the first execution may take a little more -# time because Python then compiles source files to bytecode automatically. If you want to -# compile source files to bytecode during installation, you can use the --compile option RUN poetry install --compile && \ rm -rf ~/.cache/pip diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index c340a8117a..59998e9fba 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -2,16 +2,49 @@ All notable changes to the **Prowler API** are documented in this file. -## [v1.9.0] (Prowler UNRELEASED) +## [v1.10.0] (Prowler UNRELEASED) + +### Added + +- SSO with SAML support [(#8175)](https://github.com/prowler-cloud/prowler/pull/8175) +- `/processors` endpoints to post-process findings. Currently, only the Mutelist processor is supported to allow to mute findings. + +### Security + +- Enhanced password validation to enforce 12+ character passwords with special characters, uppercase, lowercase, and numbers [(#8225)](https://github.com/prowler-cloud/prowler/pull/8225) + +--- + +## [v1.9.1] (Prowler v5.8.1) + +### Added +- Custom exception for provider connection errors during scans [(#8234)](https://github.com/prowler-cloud/prowler/pull/8234) + +### Changed +- Summary and overview tasks now use a dedicated queue and no longer propagate errors to compliance tasks [(#8214)](https://github.com/prowler-cloud/prowler/pull/8214) + +### Fixed +- Scan with no resources will not trigger legacy code for findings metadata [(#8183)](https://github.com/prowler-cloud/prowler/pull/8183) +- Invitation email comparison case-insensitive [(#8206)](https://github.com/prowler-cloud/prowler/pull/8206) + +### Removed +- Validation of the provider's secret type during updates [(#8197)](https://github.com/prowler-cloud/prowler/pull/8197) + +--- + +## [v1.9.0] (Prowler v5.8.0) ### Added -- SSO with SAML support [(#7822)](https://github.com/prowler-cloud/prowler/pull/7822) - Support GCP Service Account key [(#7824)](https://github.com/prowler-cloud/prowler/pull/7824) - `GET /compliance-overviews` endpoints to retrieve compliance metadata and specific requirements statuses [(#7877)](https://github.com/prowler-cloud/prowler/pull/7877) - Lighthouse configuration support [(#7848)](https://github.com/prowler-cloud/prowler/pull/7848) ### Changed - Reworked `GET /compliance-overviews` to return proper requirement metrics [(#7877)](https://github.com/prowler-cloud/prowler/pull/7877) +- Optional `user` and `password` for M365 provider [(#7992)](https://github.com/prowler-cloud/prowler/pull/7992) + +### Fixed +- Scheduled scans are no longer deleted when their daily schedule run is disabled [(#8082)](https://github.com/prowler-cloud/prowler/pull/8082) --- diff --git a/api/Dockerfile b/api/Dockerfile index d2e31faefc..7595b0be45 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -57,10 +57,6 @@ RUN poetry install --no-root && \ RUN poetry run python "$(poetry env info --path)/src/prowler/prowler/providers/m365/lib/powershell/m365_powershell.py" -# Prevents known compatibility error between lxml and libxml2/libxmlsec versions. -# See: https://github.com/xmlsec/python-xmlsec/issues/320 -RUN poetry run pip install --force-reinstall --no-binary lxml lxml - COPY src/backend/ ./backend/ COPY docker-entrypoint.sh ./docker-entrypoint.sh diff --git a/api/README.md b/api/README.md index a1241372ca..ba8896ff35 100644 --- a/api/README.md +++ b/api/README.md @@ -257,7 +257,7 @@ cd src/backend 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` +> The default credentials are `dev@prowler.com:Thisisapassword123@` or `dev2@prowler.com:Thisisapassword123@` ## Run tests diff --git a/api/docker-entrypoint.sh b/api/docker-entrypoint.sh index 4ff9cc4cd1..9d424432cd 100755 --- a/api/docker-entrypoint.sh +++ b/api/docker-entrypoint.sh @@ -32,7 +32,7 @@ start_prod_server() { start_worker() { echo "Starting the worker..." - poetry run python -m celery -A config.celery worker -l "${DJANGO_LOGGING_LEVEL:-info}" -Q celery,scans,scan-reports,deletion,backfill -E --max-tasks-per-child 1 + poetry run python -m celery -A config.celery worker -l "${DJANGO_LOGGING_LEVEL:-info}" -Q celery,scans,scan-reports,deletion,backfill,overview -E --max-tasks-per-child 1 } start_worker_beat() { diff --git a/api/poetry.lock b/api/poetry.lock index 452590b8c4..07ecc9ac73 100644 --- a/api/poetry.lock +++ b/api/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. [[package]] name = "about-time" @@ -880,7 +880,6 @@ description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" groups = ["main", "dev"] -markers = "platform_python_implementation != \"PyPy\"" files = [ {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, @@ -950,6 +949,7 @@ files = [ {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, ] +markers = {dev = "platform_python_implementation != \"PyPy\""} [package.dependencies] pycparser = "*" @@ -2684,144 +2684,150 @@ adal = ["adal (>=1.0.2)"] [[package]] name = "lxml" -version = "5.4.0" +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.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e7bc6df34d42322c5289e37e9971d6ed114e3776b45fa879f734bded9d1fea9c"}, - {file = "lxml-5.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6854f8bd8a1536f8a1d9a3655e6354faa6406621cf857dc27b681b69860645c7"}, - {file = "lxml-5.4.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:696ea9e87442467819ac22394ca36cb3d01848dad1be6fac3fb612d3bd5a12cf"}, - {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ef80aeac414f33c24b3815ecd560cee272786c3adfa5f31316d8b349bfade28"}, - {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b9c2754cef6963f3408ab381ea55f47dabc6f78f4b8ebb0f0b25cf1ac1f7609"}, - {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7a62cc23d754bb449d63ff35334acc9f5c02e6dae830d78dab4dd12b78a524f4"}, - {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f82125bc7203c5ae8633a7d5d20bcfdff0ba33e436e4ab0abc026a53a8960b7"}, - {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b67319b4aef1a6c56576ff544b67a2a6fbd7eaee485b241cabf53115e8908b8f"}, - {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:a8ef956fce64c8551221f395ba21d0724fed6b9b6242ca4f2f7beb4ce2f41997"}, - {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:0a01ce7d8479dce84fc03324e3b0c9c90b1ece9a9bb6a1b6c9025e7e4520e78c"}, - {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:91505d3ddebf268bb1588eb0f63821f738d20e1e7f05d3c647a5ca900288760b"}, - {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a3bcdde35d82ff385f4ede021df801b5c4a5bcdfb61ea87caabcebfc4945dc1b"}, - {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aea7c06667b987787c7d1f5e1dfcd70419b711cdb47d6b4bb4ad4b76777a0563"}, - {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a7fb111eef4d05909b82152721a59c1b14d0f365e2be4c742a473c5d7372f4f5"}, - {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:43d549b876ce64aa18b2328faff70f5877f8c6dede415f80a2f799d31644d776"}, - {file = "lxml-5.4.0-cp310-cp310-win32.whl", hash = "sha256:75133890e40d229d6c5837b0312abbe5bac1c342452cf0e12523477cd3aa21e7"}, - {file = "lxml-5.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:de5b4e1088523e2b6f730d0509a9a813355b7f5659d70eb4f319c76beea2e250"}, - {file = "lxml-5.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:98a3912194c079ef37e716ed228ae0dcb960992100461b704aea4e93af6b0bb9"}, - {file = "lxml-5.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ea0252b51d296a75f6118ed0d8696888e7403408ad42345d7dfd0d1e93309a7"}, - {file = "lxml-5.4.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92b69441d1bd39f4940f9eadfa417a25862242ca2c396b406f9272ef09cdcaa"}, - {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20e16c08254b9b6466526bc1828d9370ee6c0d60a4b64836bc3ac2917d1e16df"}, - {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7605c1c32c3d6e8c990dd28a0970a3cbbf1429d5b92279e37fda05fb0c92190e"}, - {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ecf4c4b83f1ab3d5a7ace10bafcb6f11df6156857a3c418244cef41ca9fa3e44"}, - {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cef4feae82709eed352cd7e97ae062ef6ae9c7b5dbe3663f104cd2c0e8d94ba"}, - {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:df53330a3bff250f10472ce96a9af28628ff1f4efc51ccba351a8820bca2a8ba"}, - {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:aefe1a7cb852fa61150fcb21a8c8fcea7b58c4cb11fbe59c97a0a4b31cae3c8c"}, - {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ef5a7178fcc73b7d8c07229e89f8eb45b2908a9238eb90dcfc46571ccf0383b8"}, - {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d2ed1b3cb9ff1c10e6e8b00941bb2e5bb568b307bfc6b17dffbbe8be5eecba86"}, - {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:72ac9762a9f8ce74c9eed4a4e74306f2f18613a6b71fa065495a67ac227b3056"}, - {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f5cb182f6396706dc6cc1896dd02b1c889d644c081b0cdec38747573db88a7d7"}, - {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:3a3178b4873df8ef9457a4875703488eb1622632a9cee6d76464b60e90adbfcd"}, - {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e094ec83694b59d263802ed03a8384594fcce477ce484b0cbcd0008a211ca751"}, - {file = "lxml-5.4.0-cp311-cp311-win32.whl", hash = "sha256:4329422de653cdb2b72afa39b0aa04252fca9071550044904b2e7036d9d97fe4"}, - {file = "lxml-5.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd3be6481ef54b8cfd0e1e953323b7aa9d9789b94842d0e5b142ef4bb7999539"}, - {file = "lxml-5.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b5aff6f3e818e6bdbbb38e5967520f174b18f539c2b9de867b1e7fde6f8d95a4"}, - {file = "lxml-5.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942a5d73f739ad7c452bf739a62a0f83e2578afd6b8e5406308731f4ce78b16d"}, - {file = "lxml-5.4.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:460508a4b07364d6abf53acaa0a90b6d370fafde5693ef37602566613a9b0779"}, - {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529024ab3a505fed78fe3cc5ddc079464e709f6c892733e3f5842007cec8ac6e"}, - {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ca56ebc2c474e8f3d5761debfd9283b8b18c76c4fc0967b74aeafba1f5647f9"}, - {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a81e1196f0a5b4167a8dafe3a66aa67c4addac1b22dc47947abd5d5c7a3f24b5"}, - {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00b8686694423ddae324cf614e1b9659c2edb754de617703c3d29ff568448df5"}, - {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c5681160758d3f6ac5b4fea370495c48aac0989d6a0f01bb9a72ad8ef5ab75c4"}, - {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:2dc191e60425ad70e75a68c9fd90ab284df64d9cd410ba8d2b641c0c45bc006e"}, - {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:67f779374c6b9753ae0a0195a892a1c234ce8416e4448fe1e9f34746482070a7"}, - {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:79d5bfa9c1b455336f52343130b2067164040604e41f6dc4d8313867ed540079"}, - {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3d3c30ba1c9b48c68489dc1829a6eede9873f52edca1dda900066542528d6b20"}, - {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1af80c6316ae68aded77e91cd9d80648f7dd40406cef73df841aa3c36f6907c8"}, - {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4d885698f5019abe0de3d352caf9466d5de2baded00a06ef3f1216c1a58ae78f"}, - {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea53d51859b6c64e7c51d522c03cc2c48b9b5d6172126854cc7f01aa11f52bc"}, - {file = "lxml-5.4.0-cp312-cp312-win32.whl", hash = "sha256:d90b729fd2732df28130c064aac9bb8aff14ba20baa4aee7bd0795ff1187545f"}, - {file = "lxml-5.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1dc4ca99e89c335a7ed47d38964abcb36c5910790f9bd106f2a8fa2ee0b909d2"}, - {file = "lxml-5.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:773e27b62920199c6197130632c18fb7ead3257fce1ffb7d286912e56ddb79e0"}, - {file = "lxml-5.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ce9c671845de9699904b1e9df95acfe8dfc183f2310f163cdaa91a3535af95de"}, - {file = "lxml-5.4.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9454b8d8200ec99a224df8854786262b1bd6461f4280064c807303c642c05e76"}, - {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cccd007d5c95279e529c146d095f1d39ac05139de26c098166c4beb9374b0f4d"}, - {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fce1294a0497edb034cb416ad3e77ecc89b313cff7adbee5334e4dc0d11f422"}, - {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24974f774f3a78ac12b95e3a20ef0931795ff04dbb16db81a90c37f589819551"}, - {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:497cab4d8254c2a90bf988f162ace2ddbfdd806fce3bda3f581b9d24c852e03c"}, - {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e794f698ae4c5084414efea0f5cc9f4ac562ec02d66e1484ff822ef97c2cadff"}, - {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:2c62891b1ea3094bb12097822b3d44b93fc6c325f2043c4d2736a8ff09e65f60"}, - {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:142accb3e4d1edae4b392bd165a9abdee8a3c432a2cca193df995bc3886249c8"}, - {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1a42b3a19346e5601d1b8296ff6ef3d76038058f311902edd574461e9c036982"}, - {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4291d3c409a17febf817259cb37bc62cb7eb398bcc95c1356947e2871911ae61"}, - {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4f5322cf38fe0e21c2d73901abf68e6329dc02a4994e483adbcf92b568a09a54"}, - {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0be91891bdb06ebe65122aa6bf3fc94489960cf7e03033c6f83a90863b23c58b"}, - {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:15a665ad90054a3d4f397bc40f73948d48e36e4c09f9bcffc7d90c87410e478a"}, - {file = "lxml-5.4.0-cp313-cp313-win32.whl", hash = "sha256:d5663bc1b471c79f5c833cffbc9b87d7bf13f87e055a5c86c363ccd2348d7e82"}, - {file = "lxml-5.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:bcb7a1096b4b6b24ce1ac24d4942ad98f983cd3810f9711bcd0293f43a9d8b9f"}, - {file = "lxml-5.4.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:7be701c24e7f843e6788353c055d806e8bd8466b52907bafe5d13ec6a6dbaecd"}, - {file = "lxml-5.4.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb54f7c6bafaa808f27166569b1511fc42701a7713858dddc08afdde9746849e"}, - {file = "lxml-5.4.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97dac543661e84a284502e0cf8a67b5c711b0ad5fb661d1bd505c02f8cf716d7"}, - {file = "lxml-5.4.0-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:c70e93fba207106cb16bf852e421c37bbded92acd5964390aad07cb50d60f5cf"}, - {file = "lxml-5.4.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9c886b481aefdf818ad44846145f6eaf373a20d200b5ce1a5c8e1bc2d8745410"}, - {file = "lxml-5.4.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:fa0e294046de09acd6146be0ed6727d1f42ded4ce3ea1e9a19c11b6774eea27c"}, - {file = "lxml-5.4.0-cp36-cp36m-win32.whl", hash = "sha256:61c7bbf432f09ee44b1ccaa24896d21075e533cd01477966a5ff5a71d88b2f56"}, - {file = "lxml-5.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7ce1a171ec325192c6a636b64c94418e71a1964f56d002cc28122fceff0b6121"}, - {file = "lxml-5.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:795f61bcaf8770e1b37eec24edf9771b307df3af74d1d6f27d812e15a9ff3872"}, - {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29f451a4b614a7b5b6c2e043d7b64a15bd8304d7e767055e8ab68387a8cacf4e"}, - {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:891f7f991a68d20c75cb13c5c9142b2a3f9eb161f1f12a9489c82172d1f133c0"}, - {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4aa412a82e460571fad592d0f93ce9935a20090029ba08eca05c614f99b0cc92"}, - {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:ac7ba71f9561cd7d7b55e1ea5511543c0282e2b6450f122672a2694621d63b7e"}, - {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:c5d32f5284012deaccd37da1e2cd42f081feaa76981f0eaa474351b68df813c5"}, - {file = "lxml-5.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:ce31158630a6ac85bddd6b830cffd46085ff90498b397bd0a259f59d27a12188"}, - {file = "lxml-5.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:31e63621e073e04697c1b2d23fcb89991790eef370ec37ce4d5d469f40924ed6"}, - {file = "lxml-5.4.0-cp37-cp37m-win32.whl", hash = "sha256:be2ba4c3c5b7900246a8f866580700ef0d538f2ca32535e991027bdaba944063"}, - {file = "lxml-5.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:09846782b1ef650b321484ad429217f5154da4d6e786636c38e434fa32e94e49"}, - {file = "lxml-5.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eaf24066ad0b30917186420d51e2e3edf4b0e2ea68d8cd885b14dc8afdcf6556"}, - {file = "lxml-5.4.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b31a3a77501d86d8ade128abb01082724c0dfd9524f542f2f07d693c9f1175f"}, - {file = "lxml-5.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e108352e203c7afd0eb91d782582f00a0b16a948d204d4dec8565024fafeea5"}, - {file = "lxml-5.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a11a96c3b3f7551c8a8109aa65e8594e551d5a84c76bf950da33d0fb6dfafab7"}, - {file = "lxml-5.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:ca755eebf0d9e62d6cb013f1261e510317a41bf4650f22963474a663fdfe02aa"}, - {file = "lxml-5.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:4cd915c0fb1bed47b5e6d6edd424ac25856252f09120e3e8ba5154b6b921860e"}, - {file = "lxml-5.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:226046e386556a45ebc787871d6d2467b32c37ce76c2680f5c608e25823ffc84"}, - {file = "lxml-5.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b108134b9667bcd71236c5a02aad5ddd073e372fb5d48ea74853e009fe38acb6"}, - {file = "lxml-5.4.0-cp38-cp38-win32.whl", hash = "sha256:1320091caa89805df7dcb9e908add28166113dcd062590668514dbd510798c88"}, - {file = "lxml-5.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:073eb6dcdf1f587d9b88c8c93528b57eccda40209cf9be549d469b942b41d70b"}, - {file = "lxml-5.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bda3ea44c39eb74e2488297bb39d47186ed01342f0022c8ff407c250ac3f498e"}, - {file = "lxml-5.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9ceaf423b50ecfc23ca00b7f50b64baba85fb3fb91c53e2c9d00bc86150c7e40"}, - {file = "lxml-5.4.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:664cdc733bc87449fe781dbb1f309090966c11cc0c0cd7b84af956a02a8a4729"}, - {file = "lxml-5.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67ed8a40665b84d161bae3181aa2763beea3747f748bca5874b4af4d75998f87"}, - {file = "lxml-5.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b4a3bd174cc9cdaa1afbc4620c049038b441d6ba07629d89a83b408e54c35cd"}, - {file = "lxml-5.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:b0989737a3ba6cf2a16efb857fb0dfa20bc5c542737fddb6d893fde48be45433"}, - {file = "lxml-5.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:dc0af80267edc68adf85f2a5d9be1cdf062f973db6790c1d065e45025fa26140"}, - {file = "lxml-5.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:639978bccb04c42677db43c79bdaa23785dc7f9b83bfd87570da8207872f1ce5"}, - {file = "lxml-5.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5a99d86351f9c15e4a901fc56404b485b1462039db59288b203f8c629260a142"}, - {file = "lxml-5.4.0-cp39-cp39-win32.whl", hash = "sha256:3e6d5557989cdc3ebb5302bbdc42b439733a841891762ded9514e74f60319ad6"}, - {file = "lxml-5.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:a8c9b7f16b63e65bbba889acb436a1034a82d34fa09752d754f88d708eca80e1"}, - {file = "lxml-5.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1b717b00a71b901b4667226bba282dd462c42ccf618ade12f9ba3674e1fabc55"}, - {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27a9ded0f0b52098ff89dd4c418325b987feed2ea5cc86e8860b0f844285d740"}, - {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b7ce10634113651d6f383aa712a194179dcd496bd8c41e191cec2099fa09de5"}, - {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53370c26500d22b45182f98847243efb518d268374a9570409d2e2276232fd37"}, - {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c6364038c519dffdbe07e3cf42e6a7f8b90c275d4d1617a69bb59734c1a2d571"}, - {file = "lxml-5.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b12cb6527599808ada9eb2cd6e0e7d3d8f13fe7bbb01c6311255a15ded4c7ab4"}, - {file = "lxml-5.4.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5f11a1526ebd0dee85e7b1e39e39a0cc0d9d03fb527f56d8457f6df48a10dc0c"}, - {file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48b4afaf38bf79109bb060d9016fad014a9a48fb244e11b94f74ae366a64d252"}, - {file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de6f6bb8a7840c7bf216fb83eec4e2f79f7325eca8858167b68708b929ab2172"}, - {file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5cca36a194a4eb4e2ed6be36923d3cffd03dcdf477515dea687185506583d4c9"}, - {file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b7c86884ad23d61b025989d99bfdd92a7351de956e01c61307cb87035960bcb1"}, - {file = "lxml-5.4.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:53d9469ab5460402c19553b56c3648746774ecd0681b1b27ea74d5d8a3ef5590"}, - {file = "lxml-5.4.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:56dbdbab0551532bb26c19c914848d7251d73edb507c3079d6805fa8bba5b706"}, - {file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14479c2ad1cb08b62bb941ba8e0e05938524ee3c3114644df905d2331c76cd57"}, - {file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32697d2ea994e0db19c1df9e40275ffe84973e4232b5c274f47e7c1ec9763cdd"}, - {file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:24f6df5f24fc3385f622c0c9d63fe34604893bc1a5bdbb2dbf5870f85f9a404a"}, - {file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:151d6c40bc9db11e960619d2bf2ec5829f0aaffb10b41dcf6ad2ce0f3c0b2325"}, - {file = "lxml-5.4.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4025bf2884ac4370a3243c5aa8d66d3cb9e15d3ddd0af2d796eccc5f0244390e"}, - {file = "lxml-5.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9459e6892f59ecea2e2584ee1058f5d8f629446eab52ba2305ae13a32a059530"}, - {file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47fb24cc0f052f0576ea382872b3fc7e1f7e3028e53299ea751839418ade92a6"}, - {file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50441c9de951a153c698b9b99992e806b71c1f36d14b154592580ff4a9d0d877"}, - {file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:ab339536aa798b1e17750733663d272038bf28069761d5be57cb4a9b0137b4f8"}, - {file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9776af1aad5a4b4a1317242ee2bea51da54b2a7b7b48674be736d463c999f37d"}, - {file = "lxml-5.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:63e7968ff83da2eb6fdda967483a7a023aa497d85ad8f05c3ad9b1f2e8c84987"}, - {file = "lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd"}, + {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] @@ -3854,26 +3860,26 @@ testing = ["google-api-core (>=1.31.5)"] [[package]] name = "protobuf" -version = "6.30.2" +version = "6.31.1" description = "" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "protobuf-6.30.2-cp310-abi3-win32.whl", hash = "sha256:b12ef7df7b9329886e66404bef5e9ce6a26b54069d7f7436a0853ccdeb91c103"}, - {file = "protobuf-6.30.2-cp310-abi3-win_amd64.whl", hash = "sha256:7653c99774f73fe6b9301b87da52af0e69783a2e371e8b599b3e9cb4da4b12b9"}, - {file = "protobuf-6.30.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:0eb523c550a66a09a0c20f86dd554afbf4d32b02af34ae53d93268c1f73bc65b"}, - {file = "protobuf-6.30.2-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:50f32cc9fd9cb09c783ebc275611b4f19dfdfb68d1ee55d2f0c7fa040df96815"}, - {file = "protobuf-6.30.2-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:4f6c687ae8efae6cf6093389a596548214467778146b7245e886f35e1485315d"}, - {file = "protobuf-6.30.2-cp39-cp39-win32.whl", hash = "sha256:524afedc03b31b15586ca7f64d877a98b184f007180ce25183d1a5cb230ee72b"}, - {file = "protobuf-6.30.2-cp39-cp39-win_amd64.whl", hash = "sha256:acec579c39c88bd8fbbacab1b8052c793efe83a0a5bd99db4a31423a25c0a0e2"}, - {file = "protobuf-6.30.2-py3-none-any.whl", hash = "sha256:ae86b030e69a98e08c77beab574cbcb9fff6d031d57209f574a5aea1445f4b51"}, - {file = "protobuf-6.30.2.tar.gz", hash = "sha256:35c859ae076d8c56054c25b59e5e59638d86545ed6e2b6efac6be0b6ea3ba048"}, + {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 = "prowler" -version = "5.6.0" +version = "5.8.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.9.1,<3.13" @@ -3921,6 +3927,7 @@ numpy = "2.0.2" pandas = "2.2.3" py-ocsf-models = "0.3.1" pydantic = "1.10.21" +pygithub = "2.5.0" python-dateutil = ">=2.9.0.post0,<3.0.0" pytz = "2025.1" schema = "0.7.7" @@ -3933,7 +3940,7 @@ tzlocal = "5.3.1" type = "git" url = "https://github.com/prowler-cloud/prowler.git" reference = "master" -resolved_reference = "9828824b737b8deda61f4a6646b54e0ad45033b9" +resolved_reference = "ea97de7f43a2063476b49f7697bb6c7b51137c11" [[package]] name = "psutil" @@ -4110,11 +4117,11 @@ description = "C parser in Python" optional = false python-versions = ">=3.8" groups = ["main", "dev"] -markers = "platform_python_implementation != \"PyPy\"" files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] +markers = {dev = "platform_python_implementation != \"PyPy\""} [[package]] name = "pycurl" @@ -4225,6 +4232,26 @@ typing-extensions = ">=4.2.0" dotenv = ["python-dotenv (>=0.10.4)"] email = ["email-validator (>=1.0.3)"] +[[package]] +name = "pygithub" +version = "2.5.0" +description = "Use the full Github API v3" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "PyGithub-2.5.0-py3-none-any.whl", hash = "sha256:b0b635999a658ab8e08720bdd3318893ff20e2275f6446fcf35bf3f44f2c0fd2"}, + {file = "pygithub-2.5.0.tar.gz", hash = "sha256:e1613ac508a9be710920d26eb18b1905ebd9926aa49398e88151c1b526aad3cf"}, +] + +[package.dependencies] +Deprecated = "*" +pyjwt = {version = ">=2.4.0", extras = ["crypto"]} +pynacl = ">=1.4.0" +requests = ">=2.14.0" +typing-extensions = ">=4.0.0" +urllib3 = ">=1.26.0" + [[package]] name = "pygments" version = "2.19.1" @@ -4289,6 +4316,33 @@ tomlkit = ">=0.10.1" spelling = ["pyenchant (>=3.2,<4.0)"] testutils = ["gitpython (>3)"] +[[package]] +name = "pynacl" +version = "1.5.0" +description = "Python binding to the Networking and Cryptography (NaCl) library" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"}, + {file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"}, + {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"}, + {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"}, +] + +[package.dependencies] +cffi = ">=1.4.1" + +[package.extras] +docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] +tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] + [[package]] name = "pyparsing" version = "3.2.3" @@ -4934,7 +4988,6 @@ files = [ {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"}, @@ -4943,7 +4996,6 @@ files = [ {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"}, @@ -4952,7 +5004,6 @@ files = [ {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"}, @@ -4961,7 +5012,6 @@ files = [ {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"}, @@ -4970,7 +5020,6 @@ files = [ {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"}, @@ -5645,35 +5694,70 @@ files = [ [[package]] name = "xmlsec" -version = "1.3.15" +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.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:60209f82a254a1d6083397c4eeae131e7ac2f64bfddb97f2b0b240369f03c4df"}, - {file = "xmlsec-1.3.15-cp310-cp310-win32.whl", hash = "sha256:a62be0f8964bbec1efd2ca39b025c40da620a2ef9cb5440ff4ffa7e0c6906f70"}, - {file = "xmlsec-1.3.15-cp310-cp310-win_amd64.whl", hash = "sha256:685b92860bbf048e3b725bd5e9310bd4d3515f7eafcb2c284dda62078a1ce90c"}, - {file = "xmlsec-1.3.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c760230d4f77b7828857d076434e0810850eb2603775dc92fa9f760a98c2f694"}, - {file = "xmlsec-1.3.15-cp311-cp311-win32.whl", hash = "sha256:901458034b7476e1fd0881a85814e184d00eec2b5df33b1ceeb312681e8cb9e8"}, - {file = "xmlsec-1.3.15-cp311-cp311-win_amd64.whl", hash = "sha256:2ecbb65eea79a25769fbaa56c9e8bc4553aea63a9704795e962dfe06679b0191"}, - {file = "xmlsec-1.3.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0edff08e0442cdcc82bebf353ba4bcfd5a022f4b2751052ee1564afc5c78bef4"}, - {file = "xmlsec-1.3.15-cp312-cp312-win32.whl", hash = "sha256:e5c402e5633fd39f75fe124219d66d383a040ba04d0de54e024afeb7fe7d3e3a"}, - {file = "xmlsec-1.3.15-cp312-cp312-win_amd64.whl", hash = "sha256:0c47f2347e8dcc0a48648b9702af53179618c204414a8e36926a9f61214ebf0b"}, - {file = "xmlsec-1.3.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6ac2154311d32a6571e22f224ed16356029e59bd5ca76edeb3922a809adfe89c"}, - {file = "xmlsec-1.3.15-cp313-cp313-win32.whl", hash = "sha256:5ed218129f89b0592926ad2be42c017bece469db9b7380dc41bc09b01ca26d5d"}, - {file = "xmlsec-1.3.15-cp313-cp313-win_amd64.whl", hash = "sha256:5fc29e69b064323317b3862751a3a8107670e0a17510ca4517bbdc1939a90b1a"}, - {file = "xmlsec-1.3.15-cp36-cp36m-win32.whl", hash = "sha256:d0404dd76097b1f6dcbeff404c46cf045442a8cf9500f60c46a26ae03130ab9c"}, - {file = "xmlsec-1.3.15-cp36-cp36m-win_amd64.whl", hash = "sha256:672bb43a12d6b8e2e4a392ef495ea731ded5acc1585f9358174295a6fb5df262"}, - {file = "xmlsec-1.3.15-cp37-cp37m-win32.whl", hash = "sha256:96e24b22e862f0c50840a5af23cb7df186e7a1547b311a67ebca5b1e43ea0d86"}, - {file = "xmlsec-1.3.15-cp37-cp37m-win_amd64.whl", hash = "sha256:bec066ce81a82a5a2b994b1e7be2af11715fd716a55754c645668acf9c5a64c0"}, - {file = "xmlsec-1.3.15-cp38-cp38-win32.whl", hash = "sha256:95e80981b2e0ea74a7040cbf66b40072f4424298d7b50c3e587a026a7dab34ad"}, - {file = "xmlsec-1.3.15-cp38-cp38-win_amd64.whl", hash = "sha256:c2a40f8549769ba5fdc223f0ae564d3b4d4ca52b6461d46bc508d3321267b2ad"}, - {file = "xmlsec-1.3.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a2d5692a683054dec769f4a1d6e8fade88ddcfc2cef89b20d0ecc1c75deb0dd6"}, - {file = "xmlsec-1.3.15-cp39-cp39-macosx_13_0_arm64.whl", hash = "sha256:f0115d3b4f156df2cfee8424d75dcb7f5ca2cb4870af18b713098830493d3cb0"}, - {file = "xmlsec-1.3.15-cp39-cp39-win32.whl", hash = "sha256:ffb32d3c5af289c8598d4f9215c9f8f6c208f1551e78f0180f525bc08c8a67d2"}, - {file = "xmlsec-1.3.15-cp39-cp39-win_amd64.whl", hash = "sha256:3211da05c11c7a0d2b913a7834bff59e649150f41127949b3322442bc3986b56"}, - {file = "xmlsec-1.3.15.tar.gz", hash = "sha256:baa856b83d0012e278e6f6cbec96ac8128de667ca9fa9a2eeb02c752e816f6d8"}, + {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] @@ -5821,4 +5905,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.11,<3.13" -content-hash = "0750d4d8d4c0b020c87a5c6e3c459f1f5f445e6f1395f7e492adea9a901e2056" +content-hash = "6802b33984c2f8438c9dc02dac0a0c14d5a78af60251bd0c80ca59bc2182c48e" diff --git a/api/pyproject.toml b/api/pyproject.toml index 30b1229280..b01e2cda14 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -23,12 +23,14 @@ dependencies = [ "drf-spectacular==0.27.2", "drf-spectacular-jsonapi==0.5.1", "gunicorn==23.0.0", + "lxml==5.3.2", "prowler @ git+https://github.com/prowler-cloud/prowler.git@master", "psycopg2-binary==2.9.9", "pytest-celery[redis] (>=1.0.1,<2.0.0)", "sentry-sdk[django] (>=2.20.0,<3.0.0)", "uuid6==2024.7.10", - "openai (>=1.82.0,<2.0.0)" + "openai (>=1.82.0,<2.0.0)", + "xmlsec==1.3.14" ] description = "Prowler's API (Django/DRF)" license = "Apache-2.0" diff --git a/api/src/backend/api/adapters.py b/api/src/backend/api/adapters.py index 3cee70761f..bf3fb54655 100644 --- a/api/src/backend/api/adapters.py +++ b/api/src/backend/api/adapters.py @@ -3,14 +3,7 @@ from django.db import transaction from api.db_router import MainRouter from api.db_utils import rls_transaction -from api.models import ( - Membership, - Role, - SAMLConfiguration, - Tenant, - User, - UserRoleRelationship, -) +from api.models import Membership, Role, Tenant, User, UserRoleRelationship class ProwlerSocialAccountAdapter(DefaultSocialAccountAdapter): @@ -24,7 +17,7 @@ class ProwlerSocialAccountAdapter(DefaultSocialAccountAdapter): def pre_social_login(self, request, sociallogin): # Link existing accounts with the same email address email = sociallogin.account.extra_data.get("email") - if sociallogin.account.provider == "saml": + if sociallogin.provider.id == "saml": email = sociallogin.user.email if email: existing_user = self.get_user_by_email(email) @@ -38,57 +31,10 @@ class ProwlerSocialAccountAdapter(DefaultSocialAccountAdapter): """ with transaction.atomic(using=MainRouter.admin_db): user = super().save_user(request, sociallogin, form) - provider = sociallogin.account.provider + provider = sociallogin.provider.id extra = sociallogin.account.extra_data - if provider == "saml": - # Handle SAML-specific logic - user.first_name = extra.get("firstName", [""])[0] - user.last_name = extra.get("lastName", [""])[0] - user.company_name = extra.get("organization", [""])[0] - user.name = f"{user.first_name} {user.last_name}".strip() - user.save(using=MainRouter.admin_db) - - email_domain = user.email.split("@")[-1] - tenant = ( - SAMLConfiguration.objects.using(MainRouter.admin_db) - .get(email_domain=email_domain) - .tenant - ) - - with rls_transaction(str(tenant.id)): - role_name = extra.get("userType", ["saml_default_role"])[0].strip() - - try: - role = Role.objects.using(MainRouter.admin_db).get( - name=role_name, tenant_id=tenant.id - ) - except Role.DoesNotExist: - role = Role.objects.using(MainRouter.admin_db).create( - name=role_name, - tenant_id=tenant.id, - manage_users=False, - manage_account=False, - manage_billing=False, - manage_providers=False, - manage_integrations=False, - manage_scans=False, - unlimited_visibility=False, - ) - - Membership.objects.using(MainRouter.admin_db).create( - user=user, - tenant=tenant, - role=Membership.RoleChoices.MEMBER, - ) - - UserRoleRelationship.objects.using(MainRouter.admin_db).create( - user=user, - role=role, - tenant_id=tenant.id, - ) - - else: + if provider != "saml": # Handle other providers (e.g., GitHub, Google) user.save(using=MainRouter.admin_db) social_account_name = extra.get("name") diff --git a/api/src/backend/api/db_utils.py b/api/src/backend/api/db_utils.py index d163a02fd3..c2da9ca32f 100644 --- a/api/src/backend/api/db_utils.py +++ b/api/src/backend/api/db_utils.py @@ -529,3 +529,15 @@ class IntegrationTypeEnum(EnumType): class IntegrationTypeEnumField(PostgresEnumField): def __init__(self, *args, **kwargs): super().__init__("integration_type", *args, **kwargs) + + +# Postgres enum definition for Processor type + + +class ProcessorTypeEnum(EnumType): + enum_type_name = "processor_type" + + +class ProcessorTypeEnumField(PostgresEnumField): + def __init__(self, *args, **kwargs): + super().__init__("processor_type", *args, **kwargs) diff --git a/api/src/backend/api/exceptions.py b/api/src/backend/api/exceptions.py index 14f7227d9a..a6034de806 100644 --- a/api/src/backend/api/exceptions.py +++ b/api/src/backend/api/exceptions.py @@ -57,6 +57,11 @@ class TaskInProgressException(TaskManagementError): super().__init__() +# Provider connection errors +class ProviderConnectionError(Exception): + """Base exception for provider connection errors.""" + + def custom_exception_handler(exc, context): if isinstance(exc, django_validation_error): if hasattr(exc, "error_dict"): diff --git a/api/src/backend/api/filters.py b/api/src/backend/api/filters.py index 35ebb6a611..ee69359000 100644 --- a/api/src/backend/api/filters.py +++ b/api/src/backend/api/filters.py @@ -28,6 +28,7 @@ from api.models import ( Invitation, Membership, PermissionChoices, + Processor, Provider, ProviderGroup, ProviderSecret, @@ -704,3 +705,12 @@ class IntegrationFilter(FilterSet): fields = { "inserted_at": ["date", "gte", "lte"], } + + +class ProcessorFilter(FilterSet): + processor_type = ChoiceFilter(choices=Processor.ProcessorChoices.choices) + processor_type__in = ChoiceInFilter( + choices=Processor.ProcessorChoices.choices, + field_name="processor_type", + lookup_expr="in", + ) diff --git a/api/src/backend/api/fixtures/dev/0_dev_users.json b/api/src/backend/api/fixtures/dev/0_dev_users.json index 87ce06239f..61fb0bd883 100644 --- a/api/src/backend/api/fixtures/dev/0_dev_users.json +++ b/api/src/backend/api/fixtures/dev/0_dev_users.json @@ -3,7 +3,7 @@ "model": "api.user", "pk": "8b38e2eb-6689-4f1e-a4ba-95b275130200", "fields": { - "password": "pbkdf2_sha256$720000$vA62S78kog2c2ytycVQdke$Fp35GVLLMyy5fUq3krSL9I02A+ocQ+RVa4S22LIAO5s=", + "password": "pbkdf2_sha256$870000$Z63pGJ7nre48hfcGbk5S0O$rQpKczAmijs96xa+gPVJifpT3Fetb8DOusl5Eq6gxac=", "last_login": null, "name": "Devie Prowlerson", "email": "dev@prowler.com", @@ -16,7 +16,7 @@ "model": "api.user", "pk": "b6493a3a-c997-489b-8b99-278bf74de9f6", "fields": { - "password": "pbkdf2_sha256$720000$vA62S78kog2c2ytycVQdke$Fp35GVLLMyy5fUq3krSL9I02A+ocQ+RVa4S22LIAO5s=", + "password": "pbkdf2_sha256$870000$Z63pGJ7nre48hfcGbk5S0O$rQpKczAmijs96xa+gPVJifpT3Fetb8DOusl5Eq6gxac=", "last_login": null, "name": "Devietoo Prowlerson", "email": "dev2@prowler.com", diff --git a/api/src/backend/api/migrations/0031_lighthouseconfiguration.py b/api/src/backend/api/migrations/0030_lighthouseconfiguration.py similarity index 97% rename from api/src/backend/api/migrations/0031_lighthouseconfiguration.py rename to api/src/backend/api/migrations/0030_lighthouseconfiguration.py index 4406dc74f2..b12b8cac01 100644 --- a/api/src/backend/api/migrations/0031_lighthouseconfiguration.py +++ b/api/src/backend/api/migrations/0030_lighthouseconfiguration.py @@ -11,7 +11,7 @@ import api.rls class Migration(migrations.Migration): dependencies = [ - ("api", "0030_samlconfigurations"), + ("api", "0029_findings_check_index_parent"), ] operations = [ @@ -54,6 +54,7 @@ class Migration(migrations.Migration): ("gpt-4o-mini-2024-07-18", "GPT-4o Mini v2024-07-18"), ("gpt-4o-mini", "GPT-4o Mini Default"), ], + default="gpt-4o-2024-08-06", help_text="Must be one of the supported model names", max_length=50, ), diff --git a/api/src/backend/api/migrations/0031_scan_disable_on_cascade_periodic_tasks.py b/api/src/backend/api/migrations/0031_scan_disable_on_cascade_periodic_tasks.py new file mode 100644 index 0000000000..c7e990ed13 --- /dev/null +++ b/api/src/backend/api/migrations/0031_scan_disable_on_cascade_periodic_tasks.py @@ -0,0 +1,24 @@ +# Generated by Django 5.1.10 on 2025-06-23 10:04 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("api", "0030_lighthouseconfiguration"), + ("django_celery_beat", "0019_alter_periodictasks_options"), + ] + + operations = [ + migrations.AlterField( + model_name="scan", + name="scheduler_task", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="django_celery_beat.periodictask", + ), + ), + ] diff --git a/api/src/backend/api/migrations/0030_samlconfigurations.py b/api/src/backend/api/migrations/0032_saml.py similarity index 76% rename from api/src/backend/api/migrations/0030_samlconfigurations.py rename to api/src/backend/api/migrations/0032_saml.py index b5ec1fea1a..f1481e104b 100644 --- a/api/src/backend/api/migrations/0030_samlconfigurations.py +++ b/api/src/backend/api/migrations/0032_saml.py @@ -1,57 +1,61 @@ -# Generated by Django 5.1.8 on 2025-05-15 09:54 +# Generated by Django 5.1.10 on 2025-07-02 15:47 import uuid import django.db.models.deletion +from django.conf import settings from django.db import migrations, models +import api.db_utils import api.rls class Migration(migrations.Migration): dependencies = [ - ("api", "0029_findings_check_index_parent"), + ("api", "0031_scan_disable_on_cascade_periodic_tasks"), ] operations = [ + migrations.AlterField( + model_name="integration", + name="integration_type", + field=api.db_utils.IntegrationTypeEnumField( + choices=[ + ("amazon_s3", "Amazon S3"), + ("aws_security_hub", "AWS Security Hub"), + ("jira", "JIRA"), + ("slack", "Slack"), + ] + ), + ), migrations.CreateModel( - name="SAMLDomainIndex", + name="SAMLToken", fields=[ ( "id", - models.BigAutoField( - auto_created=True, + models.UUIDField( + default=uuid.uuid4, + editable=False, primary_key=True, serialize=False, - verbose_name="ID", ), ), - ("email_domain", models.CharField(max_length=254, unique=True)), + ("inserted_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("expires_at", models.DateTimeField(editable=False)), + ("token", models.JSONField(unique=True)), ( - "tenant", + "user", models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="api.tenant" + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, ), ), ], options={ - "db_table": "saml_domain_index", + "db_table": "saml_tokens", }, ), - migrations.AddConstraint( - model_name="samldomainindex", - constraint=models.UniqueConstraint( - fields=("email_domain", "tenant"), - name="unique_resources_by_email_domain", - ), - ), - migrations.AddConstraint( - model_name="samldomainindex", - constraint=api.rls.BaseSecurityConstraint( - name="statements_on_samldomainindex", - statements=["SELECT", "INSERT", "UPDATE", "DELETE"], - ), - ), migrations.CreateModel( name="SAMLConfiguration", fields=[ @@ -105,16 +109,42 @@ class Migration(migrations.Migration): fields=("tenant",), name="unique_samlconfig_per_tenant" ), ), - migrations.AlterField( - model_name="integration", - name="integration_type", - field=api.db_utils.IntegrationTypeEnumField( - choices=[ - ("amazon_s3", "Amazon S3"), - ("aws_security_hub", "AWS Security Hub"), - ("jira", "JIRA"), - ("slack", "Slack"), - ] + migrations.CreateModel( + name="SAMLDomainIndex", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("email_domain", models.CharField(max_length=254, unique=True)), + ( + "tenant", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="api.tenant" + ), + ), + ], + options={ + "db_table": "saml_domain_index", + }, + ), + migrations.AddConstraint( + model_name="samldomainindex", + constraint=models.UniqueConstraint( + fields=("email_domain", "tenant"), + name="unique_resources_by_email_domain", + ), + ), + migrations.AddConstraint( + model_name="samldomainindex", + constraint=api.rls.BaseSecurityConstraint( + name="statements_on_samldomainindex", + statements=["SELECT", "INSERT", "UPDATE", "DELETE"], ), ), ] diff --git a/api/src/backend/api/migrations/0033_processors_enum.py b/api/src/backend/api/migrations/0033_processors_enum.py new file mode 100644 index 0000000000..7dbad72241 --- /dev/null +++ b/api/src/backend/api/migrations/0033_processors_enum.py @@ -0,0 +1,34 @@ +# Generated by Django 5.1.5 on 2025-03-03 15:46 + +from functools import partial + +from django.db import migrations + +from api.db_utils import PostgresEnumMigration, ProcessorTypeEnum, register_enum +from api.models import Processor + +ProcessorTypeEnumMigration = PostgresEnumMigration( + enum_name="processor_type", + enum_values=tuple( + processor_type[0] for processor_type in Processor.ProcessorChoices.choices + ), +) + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ("api", "0032_saml"), + ] + + operations = [ + migrations.RunPython( + ProcessorTypeEnumMigration.create_enum_type, + reverse_code=ProcessorTypeEnumMigration.drop_enum_type, + ), + migrations.RunPython( + partial(register_enum, enum_class=ProcessorTypeEnum), + reverse_code=migrations.RunPython.noop, + ), + ] diff --git a/api/src/backend/api/migrations/0034_processors.py b/api/src/backend/api/migrations/0034_processors.py new file mode 100644 index 0000000000..3df4eaf53b --- /dev/null +++ b/api/src/backend/api/migrations/0034_processors.py @@ -0,0 +1,88 @@ +# Generated by Django 5.1.5 on 2025-03-26 13:04 + +import uuid + +import django.db.models.deletion +from django.db import migrations, models + +import api.db_utils +import api.rls +from api.rls import RowLevelSecurityConstraint + + +class Migration(migrations.Migration): + dependencies = [ + ("api", "0033_processors_enum"), + ] + + operations = [ + migrations.CreateModel( + name="Processor", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("inserted_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "processor_type", + api.db_utils.ProcessorTypeEnumField( + choices=[("mutelist", "Mutelist")] + ), + ), + ("configuration", models.JSONField(default=dict)), + ( + "tenant", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="api.tenant" + ), + ), + ], + options={ + "db_table": "processors", + "abstract": False, + "indexes": [ + models.Index( + fields=["tenant_id", "id"], name="processor_tenant_id_idx" + ), + models.Index( + fields=["tenant_id", "processor_type"], + name="processor_tenant_type_idx", + ), + ], + }, + ), + migrations.AddConstraint( + model_name="processor", + constraint=models.UniqueConstraint( + fields=("tenant_id", "processor_type"), + name="unique_processor_types_tenant", + ), + ), + migrations.AddConstraint( + model_name="processor", + constraint=RowLevelSecurityConstraint( + "tenant_id", + name="rls_on_processor", + statements=["SELECT", "INSERT", "UPDATE", "DELETE"], + ), + ), + migrations.AddField( + model_name="scan", + name="processor", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="scans", + related_query_name="scan", + to="api.processor", + ), + ), + ] diff --git a/api/src/backend/api/migrations/0035_finding_muted_reason.py b/api/src/backend/api/migrations/0035_finding_muted_reason.py new file mode 100644 index 0000000000..c847864830 --- /dev/null +++ b/api/src/backend/api/migrations/0035_finding_muted_reason.py @@ -0,0 +1,22 @@ +import django.core.validators +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("api", "0034_processors"), + ] + + operations = [ + migrations.AddField( + model_name="finding", + name="muted_reason", + field=models.TextField( + blank=True, + max_length=500, + null=True, + validators=[django.core.validators.MinLengthValidator(3)], + ), + ), + ] diff --git a/api/src/backend/api/models.py b/api/src/backend/api/models.py index 57ae119d66..0d486afed3 100644 --- a/api/src/backend/api/models.py +++ b/api/src/backend/api/models.py @@ -2,6 +2,7 @@ import json import logging import re import xml.etree.ElementTree as ET +from datetime import datetime, timedelta, timezone from uuid import UUID, uuid4 from allauth.socialaccount.models import SocialApp @@ -33,6 +34,7 @@ from api.db_utils import ( IntegrationTypeEnumField, InvitationStateEnumField, MemberRoleEnumField, + ProcessorTypeEnumField, ProviderEnumField, ProviderSecretTypeEnumField, ScanTriggerEnumField, @@ -408,20 +410,6 @@ class Scan(RowLevelSecurityProtectedModel): name = models.CharField( blank=True, null=True, max_length=100, validators=[MinLengthValidator(3)] ) - provider = models.ForeignKey( - Provider, - on_delete=models.CASCADE, - related_name="scans", - related_query_name="scan", - ) - task = models.ForeignKey( - Task, - on_delete=models.CASCADE, - related_name="scans", - related_query_name="scan", - null=True, - blank=True, - ) trigger = ScanTriggerEnumField( choices=TriggerChoices.choices, ) @@ -437,11 +425,31 @@ class Scan(RowLevelSecurityProtectedModel): completed_at = models.DateTimeField(null=True, blank=True) next_scan_at = models.DateTimeField(null=True, blank=True) scheduler_task = models.ForeignKey( - PeriodicTask, on_delete=models.CASCADE, null=True, blank=True + PeriodicTask, on_delete=models.SET_NULL, null=True, blank=True ) output_location = models.CharField(blank=True, null=True, max_length=200) - - # TODO: mutelist foreign key + provider = models.ForeignKey( + Provider, + on_delete=models.CASCADE, + related_name="scans", + related_query_name="scan", + ) + task = models.ForeignKey( + Task, + on_delete=models.CASCADE, + related_name="scans", + related_query_name="scan", + null=True, + blank=True, + ) + processor = models.ForeignKey( + "Processor", + on_delete=models.SET_NULL, + related_name="scans", + related_query_name="scan", + null=True, + blank=True, + ) class Meta(RowLevelSecurityProtectedModel.Meta): db_table = "scans" @@ -697,6 +705,9 @@ class Finding(PostgresPartitionedModel, RowLevelSecurityProtectedModel): check_id = models.CharField(max_length=100, blank=False, null=False) check_metadata = models.JSONField(default=dict, null=False) muted = models.BooleanField(default=False, null=False) + muted_reason = models.TextField( + blank=True, null=True, validators=[MinLengthValidator(3)], max_length=500 + ) compliance = models.JSONField(default=dict, null=True, blank=True) # Denormalize resource data for performance @@ -942,6 +953,11 @@ class Invitation(RowLevelSecurityProtectedModel): null=True, ) + def save(self, *args, **kwargs): + if self.email: + self.email = self.email.strip().lower() + super().save(*args, **kwargs) + class Meta(RowLevelSecurityProtectedModel.Meta): db_table = "invitations" @@ -1370,6 +1386,26 @@ class IntegrationProviderRelationship(RowLevelSecurityProtectedModel): ] +class SAMLToken(models.Model): + id = models.UUIDField(primary_key=True, default=uuid4, editable=False) + inserted_at = models.DateTimeField(auto_now_add=True, editable=False) + updated_at = models.DateTimeField(auto_now=True, editable=False) + expires_at = models.DateTimeField(editable=False) + token = models.JSONField(unique=True) + user = models.ForeignKey(User, on_delete=models.CASCADE) + + class Meta: + db_table = "saml_tokens" + + def save(self, *args, **kwargs): + if not self.expires_at: + self.expires_at = datetime.now(timezone.utc) + timedelta(seconds=15) + super().save(*args, **kwargs) + + def is_expired(self) -> bool: + return datetime.now(timezone.utc) >= self.expires_at + + class SAMLDomainIndex(models.Model): """ Public index of SAML domains. No RLS. Used for fast lookup in SAML login flow. @@ -1447,7 +1483,7 @@ class SAMLConfiguration(RowLevelSecurityProtectedModel): ), ] - def clean(self, old_email_domain=None): + def clean(self, old_email_domain=None, is_create=False): # Domain must not contain @ if "@" in self.email_domain: raise ValidationError({"email_domain": "Domain must not contain @"}) @@ -1471,6 +1507,25 @@ class SAMLConfiguration(RowLevelSecurityProtectedModel): {"tenant": "There is a problem with your email domain."} ) + # The entityID must be unique in the system + idp_settings = self._parsed_metadata + entity_id = idp_settings.get("entity_id") + + if entity_id: + # Find any SocialApp with this entityID + q = SocialApp.objects.filter(provider="saml", provider_id=entity_id) + + # If updating, exclude our own SocialApp from the check + if not is_create: + q = q.exclude(client_id=old_email_domain) + else: + q = q.exclude(client_id=self.email_domain) + + if q.exists(): + raise ValidationError( + {"metadata_xml": "There is a problem with your metadata."} + ) + def save(self, *args, **kwargs): self.email_domain = self.email_domain.strip().lower() is_create = not SAMLConfiguration.objects.filter(pk=self.pk).exists() @@ -1483,7 +1538,8 @@ class SAMLConfiguration(RowLevelSecurityProtectedModel): old_email_domain = None old_metadata_xml = None - self.clean(old_email_domain) + self._parsed_metadata = self._parse_metadata() + self.clean(old_email_domain, is_create) super().save(*args, **kwargs) if is_create or ( @@ -1501,6 +1557,12 @@ class SAMLConfiguration(RowLevelSecurityProtectedModel): email_domain=self.email_domain, defaults={"tenant": self.tenant} ) + def delete(self, *args, **kwargs): + super().delete(*args, **kwargs) + + SocialApp.objects.filter(provider="saml", client_id=self.email_domain).delete() + SAMLDomainIndex.objects.filter(email_domain=self.email_domain).delete() + def _parse_metadata(self): """ Parse the raw IdP metadata XML and extract: @@ -1520,6 +1582,8 @@ class SAMLConfiguration(RowLevelSecurityProtectedModel): # Entity ID entity_id = root.attrib.get("entityID") + if not entity_id: + raise ValidationError({"metadata_xml": "Missing entityID in metadata."}) # SSO endpoint (must exist) sso = root.find(".//md:IDPSSODescriptor/md:SingleSignOnService", ns) @@ -1558,9 +1622,8 @@ class SAMLConfiguration(RowLevelSecurityProtectedModel): Create or update the corresponding SocialApp based on email_domain. If the domain changed, update the matching SocialApp. """ - idp_settings = self._parse_metadata() settings_dict = SOCIALACCOUNT_PROVIDERS["saml"].copy() - settings_dict["idp"] = idp_settings + settings_dict["idp"] = self._parsed_metadata current_site = Site.objects.get(id=settings.SITE_ID) @@ -1568,19 +1631,24 @@ class SAMLConfiguration(RowLevelSecurityProtectedModel): provider="saml", client_id=previous_email_domain or self.email_domain ) + client_id = self.email_domain[:191] + name = f"SAML-{self.email_domain}"[:40] + if social_app_qs.exists(): social_app = social_app_qs.first() - social_app.client_id = self.email_domain - social_app.name = f"{self.tenant.name} SAML ({self.email_domain})" + social_app.client_id = client_id + social_app.name = name social_app.settings = settings_dict + social_app.provider_id = self._parsed_metadata["entity_id"] social_app.save() social_app.sites.set([current_site]) else: social_app = SocialApp.objects.create( provider="saml", - client_id=self.email_domain, - name=f"{self.tenant.name} SAML ({self.email_domain})", + client_id=client_id, + name=name, settings=settings_dict, + provider_id=self._parsed_metadata["entity_id"], ) social_app.sites.set([current_site]) @@ -1759,3 +1827,42 @@ class LighthouseConfiguration(RowLevelSecurityProtectedModel): class JSONAPIMeta: resource_name = "lighthouse-configurations" + + +class Processor(RowLevelSecurityProtectedModel): + class ProcessorChoices(models.TextChoices): + MUTELIST = "mutelist", _("Mutelist") + + id = models.UUIDField(primary_key=True, default=uuid4, editable=False) + inserted_at = models.DateTimeField(auto_now_add=True, editable=False) + updated_at = models.DateTimeField(auto_now=True, editable=False) + processor_type = ProcessorTypeEnumField(choices=ProcessorChoices.choices) + configuration = models.JSONField(default=dict) + + class Meta(RowLevelSecurityProtectedModel.Meta): + db_table = "processors" + + constraints = [ + models.UniqueConstraint( + fields=("tenant_id", "processor_type"), + name="unique_processor_types_tenant", + ), + RowLevelSecurityConstraint( + field="tenant_id", + name="rls_on_%(class)s", + statements=["SELECT", "INSERT", "UPDATE", "DELETE"], + ), + ] + indexes = [ + models.Index( + fields=["tenant_id", "id"], + name="processor_tenant_id_idx", + ), + models.Index( + fields=["tenant_id", "processor_type"], + name="processor_tenant_type_idx", + ), + ] + + class JSONAPIMeta: + resource_name = "processors" diff --git a/api/src/backend/api/specs/v1.yaml b/api/src/backend/api/specs/v1.yaml index 7ea2f063c9..0164b63b7c 100644 --- a/api/src/backend/api/specs/v1.yaml +++ b/api/src/backend/api/specs/v1.yaml @@ -389,6 +389,7 @@ paths: - updated_at - first_seen_at - muted + - muted_reason - url - scan - resources @@ -860,6 +861,7 @@ paths: - updated_at - first_seen_at - muted + - muted_reason - url - scan - resources @@ -1354,6 +1356,7 @@ paths: - updated_at - first_seen_at - muted + - muted_reason - url - scan - resources @@ -2877,6 +2880,180 @@ paths: schema: $ref: '#/components/schemas/OpenApiResponseResponse' description: '' + /api/v1/lighthouse-configurations: + get: + operationId: lighthouse_configurations_list + description: Retrieve a list of all Lighthouse configurations. + summary: List all Lighthouse configurations + parameters: + - in: query + name: fields[lighthouse-configurations] + schema: + type: array + items: + type: string + enum: + - name + - api_key + - model + - temperature + - max_tokens + - business_context + - is_active + - inserted_at + - updated_at + - url + description: endpoint return only specific fields in the response on a per-type + basis by including a fields[TYPE] query parameter. + explode: false + - name: filter[search] + required: false + in: query + description: A search term. + schema: + type: string + - name: page[number] + required: false + in: query + description: A page number within the paginated result set. + schema: + type: integer + - name: page[size] + required: false + in: query + description: Number of results to return per page. + schema: + type: integer + - name: sort + required: false + in: query + description: '[list of fields to sort by](https://jsonapi.org/format/#fetching-sorting)' + schema: + type: array + items: + type: string + enum: + - name + - -name + - inserted_at + - -inserted_at + - updated_at + - -updated_at + - is_active + - -is_active + explode: false + tags: + - Lighthouse + security: + - jwtAuth: [] + responses: + '200': + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/PaginatedLighthouseConfigList' + description: '' + post: + operationId: lighthouse_configurations_create + description: Create a new Lighthouse configuration with the specified details. + summary: Create a new Lighthouse configuration + tags: + - Lighthouse + requestBody: + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/LighthouseConfigCreateRequest' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/LighthouseConfigCreateRequest' + multipart/form-data: + schema: + $ref: '#/components/schemas/LighthouseConfigCreateRequest' + required: true + security: + - jwtAuth: [] + responses: + '201': + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/LighthouseConfigCreateResponse' + description: '' + /api/v1/lighthouse-configurations/{id}: + patch: + operationId: lighthouse_configurations_partial_update + description: Update certain fields of an existing Lighthouse configuration. + summary: Partially update a Lighthouse configuration + parameters: + - in: path + name: id + schema: + type: string + required: true + tags: + - Lighthouse + requestBody: + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/PatchedLighthouseConfigUpdateRequest' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedLighthouseConfigUpdateRequest' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedLighthouseConfigUpdateRequest' + required: true + security: + - jwtAuth: [] + responses: + '200': + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/LighthouseConfigUpdateResponse' + description: '' + delete: + operationId: lighthouse_configurations_destroy + description: Remove a Lighthouse configuration by its ID. + summary: Delete a Lighthouse configuration + parameters: + - in: path + name: id + schema: + type: string + required: true + tags: + - Lighthouse + security: + - jwtAuth: [] + responses: + '204': + description: No response body + /api/v1/lighthouse-configurations/{id}/connection: + post: + operationId: lighthouse_configurations_connection_create + description: Verify the connection to the OpenAI API for a specific Lighthouse + configuration. + summary: Check the connection to the OpenAI API + parameters: + - in: path + name: id + schema: + type: string + required: true + tags: + - Lighthouse + security: + - jwtAuth: [] + responses: + '202': + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/OpenApiResponseResponse' + description: '' /api/v1/overviews/findings: get: operationId: overviews_findings_retrieve @@ -3366,6 +3543,222 @@ paths: schema: $ref: '#/components/schemas/OverviewServiceResponse' description: '' + /api/v1/processors: + get: + operationId: processors_list + description: Retrieve a list of all configured processors with options for filtering + by various criteria. + summary: List all processors + parameters: + - in: query + name: fields[processors] + schema: + type: array + items: + type: string + enum: + - inserted_at + - updated_at + - processor_type + - configuration + - url + description: endpoint return only specific fields in the response on a per-type + basis by including a fields[TYPE] query parameter. + explode: false + - in: query + name: filter[processor_type] + schema: + type: string + enum: + - mutelist + description: '* `mutelist` - Mutelist' + - in: query + name: filter[processor_type__in] + schema: + type: array + items: + type: string + enum: + - mutelist + description: |- + Multiple values may be separated by commas. + + * `mutelist` - Mutelist + explode: false + style: form + - name: filter[search] + required: false + in: query + description: A search term. + schema: + type: string + - name: page[number] + required: false + in: query + description: A page number within the paginated result set. + schema: + type: integer + - name: page[size] + required: false + in: query + description: Number of results to return per page. + schema: + type: integer + - name: sort + required: false + in: query + description: '[list of fields to sort by](https://jsonapi.org/format/#fetching-sorting)' + schema: + type: array + items: + type: string + enum: + - id + - -id + - inserted_at + - -inserted_at + - updated_at + - -updated_at + - processor_type + - -processor_type + - configuration + - -configuration + - url + - -url + explode: false + tags: + - Processor + security: + - jwtAuth: [] + responses: + '200': + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/PaginatedProcessorList' + description: '' + post: + operationId: processors_create + description: Register a new processor with the system, providing necessary configuration + details. There can only be one processor of each type per tenant. + summary: Create a new processor + tags: + - Processor + requestBody: + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/ProcessorCreateRequest' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/ProcessorCreateRequest' + multipart/form-data: + schema: + $ref: '#/components/schemas/ProcessorCreateRequest' + required: true + security: + - jwtAuth: [] + responses: + '201': + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/ProcessorCreateResponse' + description: '' + /api/v1/processors/{id}: + get: + operationId: processors_retrieve + description: Fetch detailed information about a specific processor by its ID. + summary: Retrieve processor details + parameters: + - in: query + name: fields[processors] + schema: + type: array + items: + type: string + enum: + - inserted_at + - updated_at + - processor_type + - configuration + - url + description: endpoint return only specific fields in the response on a per-type + basis by including a fields[TYPE] query parameter. + explode: false + - in: path + name: id + schema: + type: string + format: uuid + description: A UUID string identifying this processor. + required: true + tags: + - Processor + security: + - jwtAuth: [] + responses: + '200': + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/ProcessorResponse' + description: '' + patch: + operationId: processors_partial_update + description: Modify certain fields of an existing processor without affecting + other settings. + summary: Partially update a processor + parameters: + - in: path + name: id + schema: + type: string + format: uuid + description: A UUID string identifying this processor. + required: true + tags: + - Processor + requestBody: + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/PatchedProcessorUpdateRequest' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedProcessorUpdateRequest' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedProcessorUpdateRequest' + required: true + security: + - jwtAuth: [] + responses: + '200': + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/ProcessorUpdateResponse' + description: '' + delete: + operationId: processors_destroy + description: Remove a processor from the system by its ID. + summary: Delete a processor + parameters: + - in: path + name: id + schema: + type: string + format: uuid + description: A UUID string identifying this processor. + required: true + tags: + - Processor + security: + - jwtAuth: [] + responses: + '204': + description: No response body /api/v1/provider-groups: get: operationId: provider_groups_list @@ -5198,6 +5591,7 @@ paths: - completed_at - scheduled_at - next_scan_at + - processor - url description: endpoint return only specific fields in the response on a per-type basis by including a fields[TYPE] query parameter. @@ -5509,6 +5903,7 @@ paths: - completed_at - scheduled_at - next_scan_at + - processor - url description: endpoint return only specific fields in the response on a per-type basis by including a fields[TYPE] query parameter. @@ -7345,6 +7740,11 @@ components: nullable: true muted: type: boolean + muted_reason: + type: string + nullable: true + minLength: 3 + maxLength: 500 required: - uid - status @@ -8464,6 +8864,290 @@ components: $ref: '#/components/schemas/InvitationUpdate' required: - data + LighthouseConfig: + type: object + required: + - type + - id + additionalProperties: false + properties: + type: + allOf: + - $ref: '#/components/schemas/Type4bfEnum' + description: The [type](https://jsonapi.org/format/#document-resource-object-identification) + member is used to describe resource objects that share common attributes + and relationships. + id: + type: string + format: uuid + attributes: + type: object + properties: + name: + type: string + description: Name of the configuration + maxLength: 100 + minLength: 3 + api_key: + type: string + model: + enum: + - gpt-4o-2024-11-20 + - gpt-4o-2024-08-06 + - gpt-4o-2024-05-13 + - gpt-4o + - gpt-4o-mini-2024-07-18 + - gpt-4o-mini + type: string + description: |- + Must be one of the supported model names + + * `gpt-4o-2024-11-20` - GPT-4o v2024-11-20 + * `gpt-4o-2024-08-06` - GPT-4o v2024-08-06 + * `gpt-4o-2024-05-13` - GPT-4o v2024-05-13 + * `gpt-4o` - GPT-4o Default + * `gpt-4o-mini-2024-07-18` - GPT-4o Mini v2024-07-18 + * `gpt-4o-mini` - GPT-4o Mini Default + temperature: + type: number + format: double + description: Must be between 0 and 1 + max_tokens: + type: integer + maximum: 2147483647 + minimum: -2147483648 + description: Must be between 500 and 5000 + business_context: + type: string + description: Additional business context for this AI model configuration + is_active: + type: boolean + readOnly: true + inserted_at: + type: string + format: date-time + readOnly: true + updated_at: + type: string + format: date-time + readOnly: true + required: + - name + LighthouseConfigCreate: + type: object + required: + - type + additionalProperties: false + properties: + type: + allOf: + - $ref: '#/components/schemas/Type4bfEnum' + description: The [type](https://jsonapi.org/format/#document-resource-object-identification) + member is used to describe resource objects that share common attributes + and relationships. + attributes: + type: object + properties: + name: + type: string + description: Name of the configuration + maxLength: 100 + minLength: 3 + api_key: + type: string + writeOnly: true + model: + enum: + - gpt-4o-2024-11-20 + - gpt-4o-2024-08-06 + - gpt-4o-2024-05-13 + - gpt-4o + - gpt-4o-mini-2024-07-18 + - gpt-4o-mini + type: string + description: |- + Must be one of the supported model names + + * `gpt-4o-2024-11-20` - GPT-4o v2024-11-20 + * `gpt-4o-2024-08-06` - GPT-4o v2024-08-06 + * `gpt-4o-2024-05-13` - GPT-4o v2024-05-13 + * `gpt-4o` - GPT-4o Default + * `gpt-4o-mini-2024-07-18` - GPT-4o Mini v2024-07-18 + * `gpt-4o-mini` - GPT-4o Mini Default + temperature: + type: number + format: double + description: Must be between 0 and 1 + max_tokens: + type: integer + maximum: 2147483647 + minimum: -2147483648 + description: Must be between 500 and 5000 + business_context: + type: string + description: Additional business context for this AI model configuration + is_active: + type: boolean + readOnly: true + inserted_at: + type: string + format: date-time + readOnly: true + updated_at: + type: string + format: date-time + readOnly: true + required: + - name + - api_key + LighthouseConfigCreateRequest: + type: object + properties: + data: + type: object + required: + - type + additionalProperties: false + properties: + type: + type: string + description: The [type](https://jsonapi.org/format/#document-resource-object-identification) + member is used to describe resource objects that share common attributes + and relationships. + enum: + - lighthouse-configurations + attributes: + type: object + properties: + name: + type: string + minLength: 3 + description: Name of the configuration + maxLength: 100 + api_key: + type: string + writeOnly: true + minLength: 1 + model: + enum: + - gpt-4o-2024-11-20 + - gpt-4o-2024-08-06 + - gpt-4o-2024-05-13 + - gpt-4o + - gpt-4o-mini-2024-07-18 + - gpt-4o-mini + type: string + description: |- + Must be one of the supported model names + + * `gpt-4o-2024-11-20` - GPT-4o v2024-11-20 + * `gpt-4o-2024-08-06` - GPT-4o v2024-08-06 + * `gpt-4o-2024-05-13` - GPT-4o v2024-05-13 + * `gpt-4o` - GPT-4o Default + * `gpt-4o-mini-2024-07-18` - GPT-4o Mini v2024-07-18 + * `gpt-4o-mini` - GPT-4o Mini Default + temperature: + type: number + format: double + description: Must be between 0 and 1 + max_tokens: + type: integer + maximum: 2147483647 + minimum: -2147483648 + description: Must be between 500 and 5000 + business_context: + type: string + description: Additional business context for this AI model configuration + is_active: + type: boolean + readOnly: true + inserted_at: + type: string + format: date-time + readOnly: true + updated_at: + type: string + format: date-time + readOnly: true + required: + - name + - api_key + required: + - data + LighthouseConfigCreateResponse: + type: object + properties: + data: + $ref: '#/components/schemas/LighthouseConfigCreate' + required: + - data + LighthouseConfigUpdate: + type: object + required: + - type + - id + additionalProperties: false + properties: + type: + allOf: + - $ref: '#/components/schemas/Type4bfEnum' + description: The [type](https://jsonapi.org/format/#document-resource-object-identification) + member is used to describe resource objects that share common attributes + and relationships. + id: + type: string + format: uuid + attributes: + type: object + properties: + name: + type: string + description: Name of the configuration + maxLength: 100 + minLength: 3 + api_key: + type: string + writeOnly: true + model: + enum: + - gpt-4o-2024-11-20 + - gpt-4o-2024-08-06 + - gpt-4o-2024-05-13 + - gpt-4o + - gpt-4o-mini-2024-07-18 + - gpt-4o-mini + type: string + description: |- + Must be one of the supported model names + + * `gpt-4o-2024-11-20` - GPT-4o v2024-11-20 + * `gpt-4o-2024-08-06` - GPT-4o v2024-08-06 + * `gpt-4o-2024-05-13` - GPT-4o v2024-05-13 + * `gpt-4o` - GPT-4o Default + * `gpt-4o-mini-2024-07-18` - GPT-4o Mini v2024-07-18 + * `gpt-4o-mini` - GPT-4o Mini Default + temperature: + type: number + format: double + description: Must be between 0 and 1 + max_tokens: + type: integer + maximum: 2147483647 + minimum: -2147483648 + description: Must be between 500 and 5000 + business_context: + type: string + description: Additional business context for this AI model configuration + is_active: + type: boolean + readOnly: true + LighthouseConfigUpdateResponse: + type: object + properties: + data: + $ref: '#/components/schemas/LighthouseConfigUpdate' + required: + - data Membership: type: object required: @@ -8830,6 +9514,15 @@ components: $ref: '#/components/schemas/Invitation' required: - data + PaginatedLighthouseConfigList: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/LighthouseConfig' + required: + - data PaginatedMembershipList: type: object properties: @@ -8839,6 +9532,15 @@ components: $ref: '#/components/schemas/Membership' required: - data + PaginatedProcessorList: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Processor' + required: + - data PaginatedProviderGroupList: type: object properties: @@ -9153,6 +9855,173 @@ components: title: roles required: - data + PatchedLighthouseConfigUpdateRequest: + type: object + properties: + data: + type: object + required: + - type + - id + additionalProperties: false + properties: + type: + type: string + description: The [type](https://jsonapi.org/format/#document-resource-object-identification) + member is used to describe resource objects that share common attributes + and relationships. + enum: + - lighthouse-configurations + id: + type: string + format: uuid + attributes: + type: object + properties: + name: + type: string + minLength: 3 + description: Name of the configuration + maxLength: 100 + api_key: + type: string + writeOnly: true + minLength: 1 + model: + enum: + - gpt-4o-2024-11-20 + - gpt-4o-2024-08-06 + - gpt-4o-2024-05-13 + - gpt-4o + - gpt-4o-mini-2024-07-18 + - gpt-4o-mini + type: string + description: |- + Must be one of the supported model names + + * `gpt-4o-2024-11-20` - GPT-4o v2024-11-20 + * `gpt-4o-2024-08-06` - GPT-4o v2024-08-06 + * `gpt-4o-2024-05-13` - GPT-4o v2024-05-13 + * `gpt-4o` - GPT-4o Default + * `gpt-4o-mini-2024-07-18` - GPT-4o Mini v2024-07-18 + * `gpt-4o-mini` - GPT-4o Mini Default + temperature: + type: number + format: double + description: Must be between 0 and 1 + max_tokens: + type: integer + maximum: 2147483647 + minimum: -2147483648 + description: Must be between 500 and 5000 + business_context: + type: string + description: Additional business context for this AI model configuration + is_active: + type: boolean + readOnly: true + required: + - data + PatchedProcessorUpdateRequest: + type: object + properties: + data: + type: object + required: + - type + - id + additionalProperties: false + properties: + type: + type: string + description: The [type](https://jsonapi.org/format/#document-resource-object-identification) + member is used to describe resource objects that share common attributes + and relationships. + enum: + - processors + id: {} + attributes: + type: object + properties: + inserted_at: + type: string + format: date-time + readOnly: true + updated_at: + type: string + format: date-time + readOnly: true + configuration: + oneOf: + - type: object + title: Mutelist + properties: + Mutelist: + type: object + properties: + Accounts: + type: object + patternProperties: + .*: + type: object + properties: + Checks: + type: object + patternProperties: + .*: + type: object + properties: + Regions: + type: array + items: + type: string + Resources: + type: array + items: + type: string + Tags: + type: array + items: + type: string + Exceptions: + type: object + properties: + Accounts: + type: array + items: + type: string + Regions: + type: array + items: + type: string + Resources: + type: array + items: + type: string + Tags: + type: array + items: + type: string + additionalProperties: false + Description: + type: string + required: + - Regions + - Resources + additionalProperties: false + additionalProperties: false + required: + - Checks + additionalProperties: false + additionalProperties: false + required: + - Accounts + additionalProperties: false + additionalProperties: false + required: + - configuration + required: + - data PatchedProviderGroupMembershipRequest: type: object properties: @@ -9329,7 +10198,6 @@ components: * `static` - Key-value pairs * `role` - Role assumption * `service_account` - GCP Service Account Key - readOnly: true secret: oneOf: - type: object @@ -9926,6 +10794,425 @@ components: - email required: - data + Processor: + type: object + required: + - type + - id + additionalProperties: false + properties: + type: + allOf: + - $ref: '#/components/schemas/Type552Enum' + description: The [type](https://jsonapi.org/format/#document-resource-object-identification) + member is used to describe resource objects that share common attributes + and relationships. + id: + type: string + format: uuid + attributes: + type: object + properties: + inserted_at: + type: string + format: date-time + readOnly: true + updated_at: + type: string + format: date-time + readOnly: true + processor_type: + enum: + - mutelist + type: string + description: '* `mutelist` - Mutelist' + configuration: + oneOf: + - type: object + title: Mutelist + properties: + Mutelist: + type: object + properties: + Accounts: + type: object + patternProperties: + .*: + type: object + properties: + Checks: + type: object + patternProperties: + .*: + type: object + properties: + Regions: + type: array + items: + type: string + Resources: + type: array + items: + type: string + Tags: + type: array + items: + type: string + Exceptions: + type: object + properties: + Accounts: + type: array + items: + type: string + Regions: + type: array + items: + type: string + Resources: + type: array + items: + type: string + Tags: + type: array + items: + type: string + additionalProperties: false + Description: + type: string + required: + - Regions + - Resources + additionalProperties: false + additionalProperties: false + required: + - Checks + additionalProperties: false + additionalProperties: false + required: + - Accounts + additionalProperties: false + additionalProperties: false + required: + - processor_type + - configuration + ProcessorCreate: + type: object + required: + - type + additionalProperties: false + properties: + type: + allOf: + - $ref: '#/components/schemas/Type552Enum' + description: The [type](https://jsonapi.org/format/#document-resource-object-identification) + member is used to describe resource objects that share common attributes + and relationships. + attributes: + type: object + properties: + inserted_at: + type: string + format: date-time + readOnly: true + updated_at: + type: string + format: date-time + readOnly: true + processor_type: + enum: + - mutelist + type: string + description: '* `mutelist` - Mutelist' + configuration: + oneOf: + - type: object + title: Mutelist + properties: + Mutelist: + type: object + properties: + Accounts: + type: object + patternProperties: + .*: + type: object + properties: + Checks: + type: object + patternProperties: + .*: + type: object + properties: + Regions: + type: array + items: + type: string + Resources: + type: array + items: + type: string + Tags: + type: array + items: + type: string + Exceptions: + type: object + properties: + Accounts: + type: array + items: + type: string + Regions: + type: array + items: + type: string + Resources: + type: array + items: + type: string + Tags: + type: array + items: + type: string + additionalProperties: false + Description: + type: string + required: + - Regions + - Resources + additionalProperties: false + additionalProperties: false + required: + - Checks + additionalProperties: false + additionalProperties: false + required: + - Accounts + additionalProperties: false + additionalProperties: false + required: + - processor_type + - configuration + ProcessorCreateRequest: + type: object + properties: + data: + type: object + required: + - type + additionalProperties: false + properties: + type: + type: string + description: The [type](https://jsonapi.org/format/#document-resource-object-identification) + member is used to describe resource objects that share common attributes + and relationships. + enum: + - processors + attributes: + type: object + properties: + inserted_at: + type: string + format: date-time + readOnly: true + updated_at: + type: string + format: date-time + readOnly: true + processor_type: + enum: + - mutelist + type: string + description: '* `mutelist` - Mutelist' + configuration: + oneOf: + - type: object + title: Mutelist + properties: + Mutelist: + type: object + properties: + Accounts: + type: object + patternProperties: + .*: + type: object + properties: + Checks: + type: object + patternProperties: + .*: + type: object + properties: + Regions: + type: array + items: + type: string + Resources: + type: array + items: + type: string + Tags: + type: array + items: + type: string + Exceptions: + type: object + properties: + Accounts: + type: array + items: + type: string + Regions: + type: array + items: + type: string + Resources: + type: array + items: + type: string + Tags: + type: array + items: + type: string + additionalProperties: false + Description: + type: string + required: + - Regions + - Resources + additionalProperties: false + additionalProperties: false + required: + - Checks + additionalProperties: false + additionalProperties: false + required: + - Accounts + additionalProperties: false + additionalProperties: false + required: + - processor_type + - configuration + required: + - data + ProcessorCreateResponse: + type: object + properties: + data: + $ref: '#/components/schemas/ProcessorCreate' + required: + - data + ProcessorResponse: + type: object + properties: + data: + $ref: '#/components/schemas/Processor' + required: + - data + ProcessorUpdate: + type: object + required: + - type + - id + additionalProperties: false + properties: + type: + allOf: + - $ref: '#/components/schemas/Type552Enum' + description: The [type](https://jsonapi.org/format/#document-resource-object-identification) + member is used to describe resource objects that share common attributes + and relationships. + id: {} + attributes: + type: object + properties: + inserted_at: + type: string + format: date-time + readOnly: true + updated_at: + type: string + format: date-time + readOnly: true + configuration: + oneOf: + - type: object + title: Mutelist + properties: + Mutelist: + type: object + properties: + Accounts: + type: object + patternProperties: + .*: + type: object + properties: + Checks: + type: object + patternProperties: + .*: + type: object + properties: + Regions: + type: array + items: + type: string + Resources: + type: array + items: + type: string + Tags: + type: array + items: + type: string + Exceptions: + type: object + properties: + Accounts: + type: array + items: + type: string + Regions: + type: array + items: + type: string + Resources: + type: array + items: + type: string + Tags: + type: array + items: + type: string + additionalProperties: false + Description: + type: string + required: + - Regions + - Resources + additionalProperties: false + additionalProperties: false + required: + - Checks + additionalProperties: false + additionalProperties: false + required: + - Accounts + additionalProperties: false + additionalProperties: false + required: + - configuration + ProcessorUpdateResponse: + type: object + properties: + data: + $ref: '#/components/schemas/ProcessorUpdate' + required: + - data Provider: type: object required: @@ -11087,7 +12374,6 @@ components: * `static` - Key-value pairs * `role` - Role assumption * `service_account` - GCP Service Account Key - readOnly: true secret: oneOf: - type: object @@ -12095,6 +13381,31 @@ components: description: The identifier of the related object. title: Resource Identifier nullable: true + processor: + type: object + properties: + data: + type: object + properties: + id: + type: string + format: uuid + type: + type: string + enum: + - processors + title: Resource Type Name + description: The [type](https://jsonapi.org/format/#document-resource-object-identification) + member is used to describe resource objects that share common + attributes and relationships. + required: + - id + - type + required: + - data + description: The identifier of the related object. + title: Resource Identifier + nullable: true required: - provider ScanCreateRequest: @@ -12649,6 +13960,14 @@ components: type: string enum: - provider-groups + Type4bfEnum: + type: string + enum: + - lighthouse-configurations + Type552Enum: + type: string + enum: + - processors Type6bbEnum: type: string enum: @@ -12970,3 +14289,10 @@ tags: description: Endpoints for managing third-party integrations, including registration, configuration, retrieval, and deletion of integrations such as S3, JIRA, or other services. +- name: Lighthouse + description: Endpoints for managing Lighthouse configurations, including creation, + retrieval, updating, and deletion of configurations such as OpenAI keys, models, + and business context. +- name: Processor + description: Endpoints for managing post-processors used to process Prowler findings, + including registration, configuration, and deletion of post-processing actions. diff --git a/api/src/backend/api/tests/integration/test_authentication.py b/api/src/backend/api/tests/integration/test_authentication.py index f41e8207c4..24d8e16f76 100644 --- a/api/src/backend/api/tests/integration/test_authentication.py +++ b/api/src/backend/api/tests/integration/test_authentication.py @@ -11,7 +11,7 @@ def test_basic_authentication(): client = APIClient() test_user = "test_email@prowler.com" - test_password = "test_password" + test_password = "Test_password@1" # Check that a 401 is returned when no basic authentication is provided no_auth_response = client.get(reverse("provider-list")) @@ -108,7 +108,7 @@ def test_user_me_when_inviting_users(create_test_user, tenants_fixture, roles_fi user1_email = "user1@testing.com" user2_email = "user2@testing.com" - password = "thisisapassword123" + password = "Thisisapassword123@" user1_response = client.post( reverse("user-list"), @@ -187,7 +187,7 @@ class TestTokenSwitchTenant: client = APIClient() test_user = "test_email@prowler.com" - test_password = "test_password" + test_password = "Test_password1@" # Check that we can create a new user without any kind of authentication user_creation_response = client.post( diff --git a/api/src/backend/api/tests/integration/test_providers.py b/api/src/backend/api/tests/integration/test_providers.py index 0f17c2d839..9c91ad2c07 100644 --- a/api/src/backend/api/tests/integration/test_providers.py +++ b/api/src/backend/api/tests/integration/test_providers.py @@ -17,7 +17,7 @@ def test_delete_provider_without_executing_task( client = APIClient() test_user = "test_email@prowler.com" - test_password = "test_password" + test_password = "Test_password1@" prowler_task = tasks_fixture[0] task_mock = Mock() diff --git a/api/src/backend/api/tests/test_adapters.py b/api/src/backend/api/tests/test_adapters.py index b5cf1564d7..4351e3de3c 100644 --- a/api/src/backend/api/tests/test_adapters.py +++ b/api/src/backend/api/tests/test_adapters.py @@ -5,8 +5,6 @@ from allauth.socialaccount.models import SocialLogin from django.contrib.auth import get_user_model from api.adapters import ProwlerSocialAccountAdapter -from api.db_router import MainRouter -from api.models import Membership, SAMLConfiguration, Tenant User = get_user_model() @@ -27,7 +25,8 @@ class TestProwlerSocialAccountAdapter: sociallogin = MagicMock(spec=SocialLogin) sociallogin.account = MagicMock() - sociallogin.account.provider = "saml" + sociallogin.provider = MagicMock() + sociallogin.provider.id = "saml" sociallogin.account.extra_data = {} sociallogin.user = create_test_user sociallogin.connect = MagicMock() @@ -46,37 +45,12 @@ class TestProwlerSocialAccountAdapter: sociallogin = MagicMock(spec=SocialLogin) sociallogin.account = MagicMock() - sociallogin.account.provider = "github" + sociallogin.provider = MagicMock() + sociallogin.user = MagicMock() + sociallogin.provider.id = "saml" sociallogin.account.extra_data = {} sociallogin.connect = MagicMock() adapter.pre_social_login(rf.get("/"), sociallogin) sociallogin.connect.assert_not_called() - - def test_save_user_saml_flow( - self, - rf, - saml_setup, - saml_sociallogin, - ): - adapter = ProwlerSocialAccountAdapter() - request = rf.get("/") - saml_sociallogin.user.email = saml_setup["email"] - - tenant = Tenant.objects.using(MainRouter.admin_db).get( - id=saml_setup["tenant_id"] - ) - saml_config = SAMLConfiguration.objects.using(MainRouter.admin_db).get( - tenant=tenant - ) - assert saml_config.email_domain == saml_setup["domain"] - - user = adapter.save_user(request, saml_sociallogin) - - assert user.email == saml_setup["email"] - assert ( - Membership.objects.using(MainRouter.admin_db) - .filter(user=user, tenant=tenant) - .exists() - ) diff --git a/api/src/backend/api/tests/test_models.py b/api/src/backend/api/tests/test_models.py index d834143ed1..79d405d4a7 100644 --- a/api/src/backend/api/tests/test_models.py +++ b/api/src/backend/api/tests/test_models.py @@ -3,7 +3,7 @@ from allauth.socialaccount.models import SocialApp from django.core.exceptions import ValidationError from api.db_router import MainRouter -from api.models import Resource, ResourceTag, SAMLConfiguration, Tenant +from api.models import Resource, ResourceTag, SAMLConfiguration, SAMLDomainIndex @pytest.mark.django_db @@ -142,8 +142,8 @@ class TestSAMLConfigurationModel: """ - def test_creates_valid_configuration(self): - tenant = Tenant.objects.using(MainRouter.admin_db).create(name="Tenant A") + def test_creates_valid_configuration(self, tenants_fixture): + tenant = tenants_fixture[0] config = SAMLConfiguration.objects.using(MainRouter.admin_db).create( email_domain="ssoexample.com", metadata_xml=TestSAMLConfigurationModel.VALID_METADATA, @@ -153,8 +153,8 @@ class TestSAMLConfigurationModel: assert config.email_domain == "ssoexample.com" assert SocialApp.objects.filter(client_id="ssoexample.com").exists() - def test_email_domain_with_at_symbol_fails(self): - tenant = Tenant.objects.using(MainRouter.admin_db).create(name="Tenant B") + def test_email_domain_with_at_symbol_fails(self, tenants_fixture): + tenant = tenants_fixture[0] config = SAMLConfiguration( email_domain="invalid@domain.com", metadata_xml=TestSAMLConfigurationModel.VALID_METADATA, @@ -168,9 +168,8 @@ class TestSAMLConfigurationModel: assert "email_domain" in errors assert "Domain must not contain @" in errors["email_domain"][0] - def test_duplicate_email_domain_fails(self): - tenant1 = Tenant.objects.using(MainRouter.admin_db).create(name="Tenant C1") - tenant2 = Tenant.objects.using(MainRouter.admin_db).create(name="Tenant C2") + def test_duplicate_email_domain_fails(self, tenants_fixture): + tenant1, tenant2, *_ = tenants_fixture SAMLConfiguration.objects.using(MainRouter.admin_db).create( email_domain="duplicate.com", @@ -191,8 +190,8 @@ class TestSAMLConfigurationModel: assert "tenant" in errors assert "There is a problem with your email domain." in errors["tenant"][0] - def test_duplicate_tenant_config_fails(self): - tenant = Tenant.objects.using(MainRouter.admin_db).create(name="Tenant D") + def test_duplicate_tenant_config_fails(self, tenants_fixture): + tenant = tenants_fixture[0] SAMLConfiguration.objects.using(MainRouter.admin_db).create( email_domain="unique1.com", @@ -216,8 +215,8 @@ class TestSAMLConfigurationModel: in errors["tenant"][0] ) - def test_invalid_metadata_xml_fails(self): - tenant = Tenant.objects.using(MainRouter.admin_db).create(name="Tenant E") + def test_invalid_metadata_xml_fails(self, tenants_fixture): + tenant = tenants_fixture[0] config = SAMLConfiguration( email_domain="brokenxml.com", metadata_xml="", @@ -232,8 +231,8 @@ class TestSAMLConfigurationModel: assert "Invalid XML" in errors["metadata_xml"][0] assert "not well-formed" in errors["metadata_xml"][0] - def test_metadata_missing_sso_fails(self): - tenant = Tenant.objects.using(MainRouter.admin_db).create(name="Tenant F") + def test_metadata_missing_sso_fails(self, tenants_fixture): + tenant = tenants_fixture[0] xml = """ """ @@ -250,8 +249,8 @@ class TestSAMLConfigurationModel: assert "metadata_xml" in errors assert "Missing SingleSignOnService" in errors["metadata_xml"][0] - def test_metadata_missing_certificate_fails(self): - tenant = Tenant.objects.using(MainRouter.admin_db).create(name="Tenant G") + def test_metadata_missing_certificate_fails(self, tenants_fixture): + tenant = tenants_fixture[0] xml = """ @@ -269,3 +268,59 @@ class TestSAMLConfigurationModel: errors = exc_info.value.message_dict assert "metadata_xml" in errors assert "X509Certificate" in errors["metadata_xml"][0] + + def test_deletes_saml_configuration_and_related_objects(self, tenants_fixture): + tenant = tenants_fixture[0] + email_domain = "deleteme.com" + + # Create the configuration + config = SAMLConfiguration.objects.using(MainRouter.admin_db).create( + email_domain=email_domain, + metadata_xml=TestSAMLConfigurationModel.VALID_METADATA, + tenant=tenant, + ) + + # Verify that the SocialApp and SAMLDomainIndex exist + assert SocialApp.objects.filter(client_id=email_domain).exists() + assert ( + SAMLDomainIndex.objects.using(MainRouter.admin_db) + .filter(email_domain=email_domain) + .exists() + ) + + # Delete the configuration + config.delete() + + # Verify that the configuration and its related objects are deleted + assert ( + not SAMLConfiguration.objects.using(MainRouter.admin_db) + .filter(pk=config.pk) + .exists() + ) + assert not SocialApp.objects.filter(client_id=email_domain).exists() + assert ( + not SAMLDomainIndex.objects.using(MainRouter.admin_db) + .filter(email_domain=email_domain) + .exists() + ) + + def test_duplicate_entity_id_fails_on_creation(self, tenants_fixture): + tenant1, tenant2, *_ = tenants_fixture + SAMLConfiguration.objects.using(MainRouter.admin_db).create( + email_domain="first.com", + metadata_xml=self.VALID_METADATA, + tenant=tenant1, + ) + + config = SAMLConfiguration( + email_domain="second.com", + metadata_xml=self.VALID_METADATA, + tenant=tenant2, + ) + + with pytest.raises(ValidationError) as exc_info: + config.save() + + errors = exc_info.value.message_dict + assert "metadata_xml" in errors + assert "There is a problem with your metadata." in errors["metadata_xml"][0] diff --git a/api/src/backend/api/tests/test_rbac.py b/api/src/backend/api/tests/test_rbac.py index 7365bae3d4..19ea256723 100644 --- a/api/src/backend/api/tests/test_rbac.py +++ b/api/src/backend/api/tests/test_rbac.py @@ -60,7 +60,7 @@ class TestUserViewSet: def test_create_user_with_all_permissions(self, authenticated_client_rbac): valid_user_payload = { "name": "test", - "password": "newpassword123", + "password": "Newpassword123@", "email": "new_user@test.com", } response = authenticated_client_rbac.post( @@ -74,7 +74,7 @@ class TestUserViewSet: ): valid_user_payload = { "name": "test", - "password": "newpassword123", + "password": "Newpassword123@", "email": "new_user@test.com", } response = authenticated_client_no_permissions_rbac.post( @@ -321,7 +321,7 @@ class TestProviderViewSet: @pytest.mark.django_db class TestLimitedVisibility: TEST_EMAIL = "rbac@rbac.com" - TEST_PASSWORD = "thisisapassword123" + TEST_PASSWORD = "Thisisapassword123@" @pytest.fixture def limited_admin_user( diff --git a/api/src/backend/api/tests/test_utils.py b/api/src/backend/api/tests/test_utils.py index 0777267484..352c8262fa 100644 --- a/api/src/backend/api/tests/test_utils.py +++ b/api/src/backend/api/tests/test_utils.py @@ -131,6 +131,21 @@ class TestInitializeProwlerProvider: initialize_prowler_provider(provider) mock_return_prowler_provider.return_value.assert_called_once_with(key="value") + @patch("api.utils.return_prowler_provider") + def test_initialize_prowler_provider_with_mutelist( + self, mock_return_prowler_provider + ): + provider = MagicMock() + provider.secret.secret = {"key": "value"} + mutelist_processor = MagicMock() + mutelist_processor.configuration = {"Mutelist": {"key": "value"}} + mock_return_prowler_provider.return_value = MagicMock() + + initialize_prowler_provider(provider, mutelist_processor) + mock_return_prowler_provider.return_value.assert_called_once_with( + key="value", mutelist_content={"key": "value"} + ) + class TestProwlerProviderConnectionTest: @patch("api.utils.return_prowler_provider") @@ -200,6 +215,25 @@ class TestGetProwlerProviderKwargs: expected_result = {**secret_dict, **expected_extra_kwargs} assert result == expected_result + def test_get_prowler_provider_kwargs_with_mutelist(self): + provider_uid = "provider_uid" + secret_dict = {"key": "value"} + secret_mock = MagicMock() + secret_mock.secret = secret_dict + + mutelist_processor = MagicMock() + mutelist_processor.configuration = {"Mutelist": {"key": "value"}} + + provider = MagicMock() + provider.provider = Provider.ProviderChoices.AWS.value + provider.secret = secret_mock + provider.uid = provider_uid + + result = get_prowler_provider_kwargs(provider, mutelist_processor) + + expected_result = {**secret_dict, "mutelist_content": {"key": "value"}} + assert result == expected_result + def test_get_prowler_provider_kwargs_unsupported_provider(self): # Setup provider_uid = "provider_uid" @@ -254,7 +288,7 @@ class TestValidateInvitation: assert result == invitation mock_db.get.assert_called_once_with( - token="VALID_TOKEN", email="user@example.com" + token="VALID_TOKEN", email__iexact="user@example.com" ) def test_invitation_not_found_raises_validation_error(self): @@ -269,7 +303,7 @@ class TestValidateInvitation: "invitation_token": "Invalid invitation code." } mock_db.get.assert_called_once_with( - token="INVALID_TOKEN", email="user@example.com" + token="INVALID_TOKEN", email__iexact="user@example.com" ) def test_invitation_not_found_raises_not_found(self): @@ -284,7 +318,7 @@ class TestValidateInvitation: assert exc_info.value.detail == "Invitation is not valid." mock_db.get.assert_called_once_with( - token="INVALID_TOKEN", email="user@example.com" + token="INVALID_TOKEN", email__iexact="user@example.com" ) def test_invitation_expired(self, invitation): @@ -332,5 +366,27 @@ class TestValidateInvitation: "invitation_token": "Invalid invitation code." } mock_db.get.assert_called_once_with( - token="VALID_TOKEN", email="different@example.com" + token="VALID_TOKEN", email__iexact="different@example.com" + ) + + def test_valid_invitation_uppercase_email(self): + """Test that validate_invitation works with case-insensitive email lookup.""" + uppercase_email = "USER@example.com" + + invitation = MagicMock(spec=Invitation) + invitation.token = "VALID_TOKEN" + invitation.email = uppercase_email + invitation.expires_at = datetime.now(timezone.utc) + timedelta(days=1) + invitation.state = Invitation.State.PENDING + invitation.tenant = MagicMock() + + with patch("api.utils.Invitation.objects.using") as mock_using: + mock_db = mock_using.return_value + mock_db.get.return_value = invitation + + result = validate_invitation("VALID_TOKEN", "user@example.com") + + assert result == invitation + mock_db.get.assert_called_once_with( + token="VALID_TOKEN", email__iexact="user@example.com" ) diff --git a/api/src/backend/api/tests/test_views.py b/api/src/backend/api/tests/test_views.py index 8771e6a1ee..b064ee352f 100644 --- a/api/src/backend/api/tests/test_views.py +++ b/api/src/backend/api/tests/test_views.py @@ -5,7 +5,10 @@ import os import tempfile from datetime import datetime, timedelta, timezone from pathlib import Path +from types import SimpleNamespace from unittest.mock import ANY, MagicMock, Mock, patch +from urllib.parse import parse_qs, urlparse +from uuid import uuid4 import jwt import pytest @@ -26,6 +29,7 @@ from api.models import ( Integration, Invitation, Membership, + Processor, Provider, ProviderGroup, ProviderGroupMembership, @@ -33,6 +37,7 @@ from api.models import ( Role, RoleProviderGroupRelationship, SAMLConfiguration, + SAMLToken, Scan, StateChoices, Task, @@ -88,7 +93,7 @@ class TestUserViewSet: def test_users_create(self, client): valid_user_payload = { "name": "test", - "password": "newpassword123", + "password": "NewPassword123!", "email": "NeWuSeR@example.com", } response = client.post( @@ -129,6 +134,10 @@ class TestUserViewSet: "password1", # Common password and too similar to a common password "dev12345", # Similar to username ("querty12" * 9) + "a", # Too long, 73 characters + "NewPassword123", # No special character + "newpassword123@", # No uppercase letter + "NEWPASSWORD123", # No lowercase letter + "NewPassword@", # No number ], ) def test_users_create_invalid_passwords(self, authenticated_client, password): @@ -159,7 +168,7 @@ class TestUserViewSet: # First user created; no errors should occur user_payload = { "name": "test_email_validator", - "password": "newpassword123", + "password": "Newpassword123@", "email": "nonexistentemail@prowler.com", } response = authenticated_client.post( @@ -169,7 +178,7 @@ class TestUserViewSet: user_payload = { "name": "test_email_validator", - "password": "newpassword123", + "password": "Newpassword123@", "email": email, } response = authenticated_client.post( @@ -262,6 +271,10 @@ class TestUserViewSet: # Fails UserAttributeSimilarityValidator (too similar to email) "dev12345", "test@prowler.com", + "NewPassword123", # No special character + "newpassword123@", # No uppercase letter + "NEWPASSWORD123", # No lowercase letter + "NewPassword@", # No number ], ) def test_users_partial_update_invalid_password( @@ -1241,10 +1254,10 @@ class TestProviderViewSet: ("uid.icontains", "1", 5), ("alias", "aws_testing_1", 1), ("alias.icontains", "aws", 2), - ("inserted_at", TODAY, 5), - ("inserted_at.gte", "2024-01-01", 5), + ("inserted_at", TODAY, 6), + ("inserted_at.gte", "2024-01-01", 6), ("inserted_at.lte", "2024-01-01", 0), - ("updated_at.gte", "2024-01-01", 5), + ("updated_at.gte", "2024-01-01", 6), ("updated_at.lte", "2024-01-01", 0), ] ), @@ -1723,6 +1736,50 @@ class TestProviderSecretViewSet: "kubeconfig_content": "kubeconfig-content", }, ), + # M365 with STATIC secret - no user or password + ( + Provider.ProviderChoices.M365.value, + ProviderSecret.TypeChoices.STATIC, + { + "client_id": "client-id", + "client_secret": "client-secret", + "tenant_id": "tenant-id", + }, + ), + # M365 with user only + ( + Provider.ProviderChoices.M365.value, + ProviderSecret.TypeChoices.STATIC, + { + "client_id": "client-id", + "client_secret": "client-secret", + "tenant_id": "tenant-id", + "user": "test@domain.com", + }, + ), + # M365 with password only + ( + Provider.ProviderChoices.M365.value, + ProviderSecret.TypeChoices.STATIC, + { + "client_id": "client-id", + "client_secret": "client-secret", + "tenant_id": "tenant-id", + "password": "supersecret", + }, + ), + # M365 with user and password + ( + Provider.ProviderChoices.M365.value, + ProviderSecret.TypeChoices.STATIC, + { + "client_id": "client-id", + "client_secret": "client-secret", + "tenant_id": "tenant-id", + "user": "test@domain.com", + "password": "supersecret", + }, + ), ], ) def test_provider_secrets_create_valid( @@ -1734,7 +1791,10 @@ class TestProviderSecretViewSet: secret_data, ): # Get the provider from the fixture and set its type - provider = Provider.objects.filter(provider=provider_type)[0] + try: + provider = Provider.objects.filter(provider=provider_type)[0] + except IndexError: + print(f"Provider {provider_type} not found") data = { "data": { @@ -1987,6 +2047,104 @@ class TestProviderSecretViewSet: ) assert response.status_code == status.HTTP_400_BAD_REQUEST + def test_provider_secrets_partial_update_with_secret_type( + self, authenticated_client, provider_secret_fixture + ): + provider_secret, *_ = provider_secret_fixture + data = { + "data": { + "type": "provider-secrets", + "id": str(provider_secret.id), + "attributes": { + "name": "new_name", + "secret": { + "service_account_key": {}, + }, + "secret_type": "service_account", + }, + "relationships": { + "provider": { + "data": { + "type": "providers", + "id": str(provider_secret.provider.id), + } + } + }, + } + } + response = authenticated_client.patch( + reverse("providersecret-detail", kwargs={"pk": provider_secret.id}), + data=json.dumps(data), + content_type="application/vnd.api+json", + ) + assert response.status_code == status.HTTP_200_OK + provider_secret.refresh_from_db() + assert provider_secret.name == "new_name" + assert provider_secret.secret == {"service_account_key": {}} + + def test_provider_secrets_partial_update_with_invalid_secret_type( + self, authenticated_client, provider_secret_fixture + ): + provider_secret, *_ = provider_secret_fixture + data = { + "data": { + "type": "provider-secrets", + "id": str(provider_secret.id), + "attributes": { + "name": "new_name", + "secret": { + "service_account_key": {}, + }, + "secret_type": "static", + }, + "relationships": { + "provider": { + "data": { + "type": "providers", + "id": str(provider_secret.provider.id), + } + } + }, + } + } + response = authenticated_client.patch( + reverse("providersecret-detail", kwargs={"pk": provider_secret.id}), + data=json.dumps(data), + content_type="application/vnd.api+json", + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + def test_provider_secrets_partial_update_without_secret_type_but_different( + self, authenticated_client, provider_secret_fixture + ): + provider_secret, *_ = provider_secret_fixture + data = { + "data": { + "type": "provider-secrets", + "id": str(provider_secret.id), + "attributes": { + "name": "new_name", + "secret": { + "service_account_key": {}, + }, + }, + "relationships": { + "provider": { + "data": { + "type": "providers", + "id": str(provider_secret.provider.id), + } + } + }, + } + } + response = authenticated_client.patch( + reverse("providersecret-detail", kwargs={"pk": provider_secret.id}), + data=json.dumps(data), + content_type="application/vnd.api+json", + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + @pytest.mark.django_db class TestScanViewSet: @@ -3329,6 +3487,61 @@ class TestFindingViewSet: ] } + def test_findings_metadata_backfill( + self, authenticated_client, scans_fixture, findings_fixture + ): + scan = scans_fixture[0] + scan.unique_resource_count = 1 + scan.save() + + with patch( + "api.v1.views.backfill_scan_resource_summaries_task.apply_async" + ) as mock_backfill_task: + response = authenticated_client.get( + reverse("finding-metadata"), + {"filter[scan]": str(scan.id)}, + ) + assert response.status_code == status.HTTP_200_OK + mock_backfill_task.assert_called() + + def test_findings_metadata_backfill_no_resources( + self, authenticated_client, scans_fixture + ): + scan_id = str(scans_fixture[0].id) + with patch( + "api.v1.views.backfill_scan_resource_summaries_task.apply_async" + ) as mock_backfill_task: + response = authenticated_client.get( + reverse("finding-metadata"), + {"filter[scan]": scan_id}, + ) + assert response.status_code == status.HTTP_200_OK + mock_backfill_task.assert_not_called() + + def test_findings_metadata_latest_backfill( + self, authenticated_client, scans_fixture, findings_fixture + ): + scan = scans_fixture[0] + scan.unique_resource_count = 1 + scan.save() + + with patch( + "api.v1.views.backfill_scan_resource_summaries_task.apply_async" + ) as mock_backfill_task: + response = authenticated_client.get(reverse("finding-metadata_latest")) + assert response.status_code == status.HTTP_200_OK + mock_backfill_task.assert_called() + + def test_findings_metadata_latest_backfill_no_resources( + self, authenticated_client, scans_fixture + ): + with patch( + "api.v1.views.backfill_scan_resource_summaries_task.apply_async" + ) as mock_backfill_task: + response = authenticated_client.get(reverse("finding-metadata_latest")) + assert response.status_code == status.HTTP_200_OK + mock_backfill_task.assert_not_called() + def test_findings_latest(self, authenticated_client, latest_scan_finding): response = authenticated_client.get( reverse("finding-latest"), @@ -3745,7 +3958,7 @@ class TestInvitationViewSet: data = { "name": "test", - "password": "newpassword123", + "password": "Newpassword123@", "email": invitation.email, } assert invitation.state == Invitation.State.PENDING.value @@ -3837,7 +4050,7 @@ class TestInvitationViewSet: data = { "name": "test", - "password": "newpassword123", + "password": "Newpassword123@", "email": new_email, } @@ -5561,6 +5774,76 @@ class TestIntegrationViewSet: assert response.status_code == status.HTTP_400_BAD_REQUEST +@pytest.mark.django_db +class TestSAMLTokenValidation: + def test_valid_token_returns_tokens(self, authenticated_client, create_test_user): + user = create_test_user + valid_token_data = { + "access": "mock_access_token", + "refresh": "mock_refresh_token", + } + saml_token = SAMLToken.objects.create( + token=valid_token_data, + user=user, + expires_at=datetime.now(timezone.utc) + timedelta(seconds=10), + ) + + url = reverse("token-saml") + response = authenticated_client.post(f"{url}?id={saml_token.id}") + + assert response.status_code == status.HTTP_200_OK + assert response.json() == {"data": valid_token_data} + assert not SAMLToken.objects.filter(id=saml_token.id).exists() + + def test_invalid_token_id_returns_404(self, authenticated_client): + url = reverse("token-saml") + response = authenticated_client.post(f"{url}?id={str(uuid4())}") + + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response.json()["errors"]["detail"] == "Invalid token ID." + + def test_expired_token_returns_400(self, authenticated_client, create_test_user): + user = create_test_user + expired_token_data = { + "access": "expired_access_token", + "refresh": "expired_refresh_token", + } + saml_token = SAMLToken.objects.create( + token=expired_token_data, + user=user, + expires_at=datetime.now(timezone.utc) - timedelta(seconds=1), + ) + + url = reverse("token-saml") + response = authenticated_client.post(f"{url}?id={saml_token.id}") + + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json()["errors"]["detail"] == "Token expired." + assert SAMLToken.objects.filter(id=saml_token.id).exists() + + def test_token_can_be_used_only_once(self, authenticated_client, create_test_user): + user = create_test_user + token_data = { + "access": "single_use_token", + "refresh": "single_use_refresh", + } + saml_token = SAMLToken.objects.create( + token=token_data, + user=user, + expires_at=datetime.now(timezone.utc) + timedelta(seconds=10), + ) + + url = reverse("token-saml") + + # First use: should succeed + response1 = authenticated_client.post(f"{url}?id={saml_token.id}") + assert response1.status_code == status.HTTP_200_OK + + # Second use: should fail (already deleted) + response2 = authenticated_client.post(f"{url}?id={saml_token.id}") + assert response2.status_code == status.HTTP_404_NOT_FOUND + + @pytest.mark.django_db class TestSAMLInitiateAPIView: def test_valid_email_domain_and_certificates( @@ -5575,11 +5858,11 @@ class TestSAMLInitiateAPIView: response = authenticated_client.post(url, data=payload, format="json") assert response.status_code == status.HTTP_302_FOUND - assert f"email={saml_setup['email']}" in response.url assert ( reverse("saml_login", kwargs={"organization_slug": saml_setup["domain"]}) in response.url ) + assert "SAMLRequest" not in response.url def test_invalid_email_domain(self, authenticated_client): url = reverse("api_saml_initiate") @@ -5590,21 +5873,6 @@ class TestSAMLInitiateAPIView: assert response.status_code == status.HTTP_403_FORBIDDEN assert response.json()["errors"]["detail"] == "Unauthorized domain." - def test_missing_certificates(self, authenticated_client, saml_setup, monkeypatch): - monkeypatch.setenv("SAML_PUBLIC_CERT", "") - monkeypatch.setenv("SAML_PRIVATE_KEY", "") - - url = reverse("api_saml_initiate") - payload = {"email_domain": saml_setup["email"]} - - response = authenticated_client.post(url, data=payload, format="json") - - assert response.status_code == status.HTTP_403_FORBIDDEN - assert ( - response.json()["errors"]["detail"] - == "SAML configuration is invalid: missing certificates." - ) - @pytest.mark.django_db class TestSAMLConfigurationViewSet: @@ -5710,7 +5978,8 @@ class TestSAMLConfigurationViewSet: @pytest.mark.django_db class TestTenantFinishACSView: - def test_dispatch_skips_if_user_not_authenticated(self): + def test_dispatch_skips_if_user_not_authenticated(self, monkeypatch): + monkeypatch.setenv("AUTH_URL", "http://localhost") request = RequestFactory().get( reverse("saml_finish_acs", kwargs={"organization_slug": "testtenant"}) ) @@ -5731,7 +6000,8 @@ class TestTenantFinishACSView: assert response.status_code in [200, 302] - def test_dispatch_skips_if_social_app_not_found(self, users_fixture): + def test_dispatch_skips_if_social_app_not_found(self, users_fixture, monkeypatch): + monkeypatch.setenv("AUTH_URL", "http://localhost") request = RequestFactory().get( reverse("saml_finish_acs", kwargs={"organization_slug": "testtenant"}) ) @@ -5752,14 +6022,15 @@ class TestTenantFinishACSView: assert isinstance(response, JsonResponse) or response.status_code in [200, 302] - def test_dispatch_sets_user_profile_and_assigns_role( - self, create_test_user, tenants_fixture, saml_setup + def test_dispatch_sets_user_profile_and_assigns_role_and_creates_token( + self, create_test_user, tenants_fixture, saml_setup, settings, monkeypatch ): + monkeypatch.setenv("SAML_SSO_CALLBACK_URL", "http://localhost/sso-complete") user = create_test_user - original_email = user.email original_name = user.name original_company = user.company_name - user.email = f"doe@{saml_setup['email']}" + user.company_name = "testing_company" + user.is_authenticate = True social_account = SocialAccount( user=user, @@ -5767,8 +6038,8 @@ class TestTenantFinishACSView: extra_data={ "firstName": ["John"], "lastName": ["Doe"], - "organization": ["TestOrg"], - "userType": ["saml_default_role"], + "organization": ["testing_company"], + "userType": ["no_permissions"], }, ) @@ -5781,34 +6052,48 @@ class TestTenantFinishACSView: patch( "allauth.socialaccount.providers.saml.views.get_app_or_404" ) as mock_get_app_or_404, - patch("allauth.socialaccount.models.SocialApp.objects.get"), + patch( + "allauth.socialaccount.models.SocialApp.objects.get" + ) as mock_socialapp_get, patch( "allauth.socialaccount.models.SocialAccount.objects.get" - ) as mock_socialaccount_get, - patch("api.v1.serializers.TokenSocialLoginSerializer") as mock_serializer, + ) as mock_sa_get, + patch("api.models.SAMLDomainIndex.objects.get") as mock_saml_domain_get, + patch("api.models.SAMLConfiguration.objects.get") as mock_saml_config_get, + patch("api.models.User.objects.get") as mock_user_get, ): mock_get_app_or_404.return_value = MagicMock( provider="saml", client_id="testtenant", name="Test App", settings={} ) - - mock_socialaccount_get.return_value = social_account - - mock_instance = mock_serializer.return_value - mock_instance.is_valid.return_value = True - mock_instance.validated_data = { - "token": "mocktoken", - "refresh_token": "mockrefresh", - } + mock_sa_get.return_value = social_account + mock_socialapp_get.return_value = MagicMock(provider_id="saml") + mock_saml_domain_get.return_value = SimpleNamespace( + tenant_id=tenants_fixture[0].id + ) + mock_saml_config_get.return_value = MagicMock() + mock_user_get.return_value = user view = TenantFinishACSView.as_view() response = view(request, organization_slug="testtenant") - assert response.status_code == 200 + assert response.status_code == 302 + + expected_callback_host = "localhost" + parsed_url = urlparse(response.url) + assert parsed_url.netloc == expected_callback_host + query_params = parse_qs(parsed_url.query) + assert "id" in query_params + + token_id = query_params["id"][0] + token_obj = SAMLToken.objects.get(id=token_id) + assert token_obj.user == user + assert not token_obj.is_expired() + user.refresh_from_db() assert user.name == "John Doe" - assert user.company_name == "TestOrg" + assert user.company_name == "testing_company" - role = Role.objects.using(MainRouter.admin_db).get(name="saml_default_role") + role = Role.objects.using(MainRouter.admin_db).get(name="no_permissions") assert role.tenant == tenants_fixture[0] assert ( @@ -5816,7 +6101,14 @@ class TestTenantFinishACSView: .filter(user=user, tenant_id=tenants_fixture[0].id) .exists() ) - user.email = original_email + + membership = Membership.objects.using(MainRouter.admin_db).get( + user=user, tenant=tenants_fixture[0] + ) + assert membership.role == Membership.RoleChoices.MEMBER + assert membership.user == user + assert membership.tenant == tenants_fixture[0] + user.name = original_name user.company_name = original_company user.save() @@ -6152,3 +6444,186 @@ class TestLighthouseConfigViewSet: reverse("lighthouseconfiguration-connection", kwargs={"pk": "random_id"}) ) assert response.status_code == status.HTTP_404_NOT_FOUND + + +@pytest.mark.django_db +class TestProcessorViewSet: + valid_mutelist_configuration = """Mutelist: + Accounts: + '*': + Checks: + iam_user_hardware_mfa_enabled: + Regions: + - '*' + Resources: + - '*' + """ + + def test_list_processors(self, authenticated_client, processor_fixture): + response = authenticated_client.get(reverse("processor-list")) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["data"]) == 1 + + def test_retrieve_processor(self, authenticated_client, processor_fixture): + processor = processor_fixture + response = authenticated_client.get( + reverse("processor-detail", kwargs={"pk": processor.id}) + ) + assert response.status_code == status.HTTP_200_OK + + def test_create_processor_valid(self, authenticated_client): + payload = { + "data": { + "type": "processors", + "attributes": { + "processor_type": "mutelist", + "configuration": self.valid_mutelist_configuration, + }, + }, + } + response = authenticated_client.post( + reverse("processor-list"), + data=payload, + content_type="application/vnd.api+json", + ) + assert response.status_code == status.HTTP_201_CREATED + + @pytest.mark.parametrize( + "invalid_configuration", + [ + None, + "", + "invalid configuration", + {"invalid": "configuration"}, + ], + ) + def test_create_processor_invalid( + self, authenticated_client, invalid_configuration + ): + payload = { + "data": { + "type": "processors", + "attributes": { + "processor_type": "mutelist", + "configuration": invalid_configuration, + }, + }, + } + response = authenticated_client.post( + reverse("processor-list"), + data=payload, + content_type="application/vnd.api+json", + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + def test_update_processor_valid(self, authenticated_client, processor_fixture): + processor = processor_fixture + payload = { + "data": { + "type": "processors", + "id": str(processor.id), + "attributes": { + "configuration": { + "Mutelist": { + "Accounts": { + "1234567890": { + "Checks": { + "iam_user_hardware_mfa_enabled": { + "Regions": ["*"], + "Resources": ["*"], + } + } + } + } + } + }, + }, + }, + } + response = authenticated_client.patch( + reverse("processor-detail", kwargs={"pk": processor.id}), + data=payload, + content_type="application/vnd.api+json", + ) + assert response.status_code == status.HTTP_200_OK + processor.refresh_from_db() + assert ( + processor.configuration["Mutelist"]["Accounts"]["1234567890"] + == payload["data"]["attributes"]["configuration"]["Mutelist"]["Accounts"][ + "1234567890" + ] + ) + + @pytest.mark.parametrize( + "invalid_configuration", + [ + None, + "", + "invalid configuration", + {"invalid": "configuration"}, + ], + ) + def test_update_processor_invalid( + self, authenticated_client, processor_fixture, invalid_configuration + ): + processor = processor_fixture + payload = { + "data": { + "type": "processors", + "id": str(processor.id), + "attributes": { + "configuration": invalid_configuration, + }, + }, + } + response = authenticated_client.patch( + reverse("processor-detail", kwargs={"pk": processor.id}), + data=payload, + content_type="application/vnd.api+json", + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + def test_delete_processor(self, authenticated_client, processor_fixture): + processor = processor_fixture + response = authenticated_client.delete( + reverse("processor-detail", kwargs={"pk": processor.id}) + ) + assert response.status_code == status.HTTP_204_NO_CONTENT + assert not Processor.objects.filter(id=processor.id).exists() + + def test_processors_filters(self, authenticated_client, processor_fixture): + response = authenticated_client.get( + reverse("processor-list"), + {"filter[processor_type]": "mutelist"}, + ) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0]["attributes"]["processor_type"] == "mutelist" + + def test_processors_filters_invalid(self, authenticated_client): + response = authenticated_client.get( + reverse("processor-list"), + {"filter[processor_type]": "invalid"}, + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + def test_processors_create_another_with_same_type( + self, authenticated_client, processor_fixture + ): + pass + + payload = { + "data": { + "type": "processors", + "attributes": { + "processor_type": "mutelist", + "configuration": self.valid_mutelist_configuration, + }, + }, + } + response = authenticated_client.post( + reverse("processor-list"), + data=payload, + content_type="application/vnd.api+json", + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST diff --git a/api/src/backend/api/utils.py b/api/src/backend/api/utils.py index ce3f4feb52..5a601ef7c9 100644 --- a/api/src/backend/api/utils.py +++ b/api/src/backend/api/utils.py @@ -7,7 +7,7 @@ from rest_framework.exceptions import NotFound, ValidationError from api.db_router import MainRouter from api.exceptions import InvitationTokenExpiredException -from api.models import Invitation, Provider, Resource +from api.models import Invitation, Processor, Provider, Resource from api.v1.serializers import FindingMetadataSerializer from prowler.providers.aws.aws_provider import AwsProvider from prowler.providers.azure.azure_provider import AzureProvider @@ -83,11 +83,14 @@ def return_prowler_provider( return prowler_provider -def get_prowler_provider_kwargs(provider: Provider) -> dict: +def get_prowler_provider_kwargs( + provider: Provider, mutelist_processor: Processor | None = None +) -> dict: """Get the Prowler provider kwargs based on the given provider type. Args: provider (Provider): The provider object containing the provider type and associated secret. + mutelist_processor (Processor): The mutelist processor object containing the mutelist configuration. Returns: dict: The provider kwargs for the corresponding provider class. @@ -105,16 +108,24 @@ def get_prowler_provider_kwargs(provider: Provider) -> dict: } elif provider.provider == Provider.ProviderChoices.KUBERNETES.value: prowler_provider_kwargs = {**prowler_provider_kwargs, "context": provider.uid} + + if mutelist_processor: + mutelist_content = mutelist_processor.configuration.get("Mutelist", {}) + if mutelist_content: + prowler_provider_kwargs["mutelist_content"] = mutelist_content + return prowler_provider_kwargs def initialize_prowler_provider( provider: Provider, + mutelist_processor: Processor | None = None, ) -> AwsProvider | AzureProvider | GcpProvider | KubernetesProvider | M365Provider: """Initialize a Prowler provider instance based on the given provider type. Args: provider (Provider): The provider object containing the provider type and associated secrets. + mutelist_processor (Processor): The mutelist processor object containing the mutelist configuration. Returns: AwsProvider | AzureProvider | GcpProvider | KubernetesProvider | M365Provider: An instance of the corresponding provider class @@ -122,7 +133,7 @@ def initialize_prowler_provider( provider's secrets. """ prowler_provider = return_prowler_provider(provider) - prowler_provider_kwargs = get_prowler_provider_kwargs(provider) + prowler_provider_kwargs = get_prowler_provider_kwargs(provider, mutelist_processor) return prowler_provider(**prowler_provider_kwargs) @@ -187,7 +198,7 @@ def validate_invitation( # Admin DB connector is used to bypass RLS protection since the invitation belongs to a tenant the user # is not a member of yet invitation = Invitation.objects.using(MainRouter.admin_db).get( - token=invitation_token, email=email + token=invitation_token, email__iexact=email ) except Invitation.DoesNotExist: if raise_not_found: diff --git a/api/src/backend/api/v1/serializer_utils/base.py b/api/src/backend/api/v1/serializer_utils/base.py new file mode 100644 index 0000000000..2e51ebc55e --- /dev/null +++ b/api/src/backend/api/v1/serializer_utils/base.py @@ -0,0 +1,23 @@ +import yaml +from rest_framework_json_api import serializers +from rest_framework_json_api.serializers import ValidationError + + +class BaseValidateSerializer(serializers.Serializer): + def validate(self, data): + if hasattr(self, "initial_data"): + initial_data = set(self.initial_data.keys()) - {"id", "type"} + unknown_keys = initial_data - set(self.fields.keys()) + if unknown_keys: + raise ValidationError(f"Invalid fields: {unknown_keys}") + return data + + +class YamlOrJsonField(serializers.JSONField): + def to_internal_value(self, data): + if isinstance(data, str): + try: + data = yaml.safe_load(data) + except yaml.YAMLError as exc: + raise serializers.ValidationError("Invalid YAML format") from exc + return super().to_internal_value(data) diff --git a/api/src/backend/api/v1/serializer_utils/integrations.py b/api/src/backend/api/v1/serializer_utils/integrations.py index ec57d14874..a42ca8dcfe 100644 --- a/api/src/backend/api/v1/serializer_utils/integrations.py +++ b/api/src/backend/api/v1/serializer_utils/integrations.py @@ -1,19 +1,7 @@ from drf_spectacular.utils import extend_schema_field from rest_framework_json_api import serializers -from rest_framework_json_api.serializers import ValidationError - -class BaseValidateSerializer(serializers.Serializer): - def validate(self, data): - if hasattr(self, "initial_data"): - initial_data = set(self.initial_data.keys()) - {"id", "type"} - unknown_keys = initial_data - set(self.fields.keys()) - if unknown_keys: - raise ValidationError(f"Invalid fields: {unknown_keys}") - return data - - -# Integrations +from api.v1.serializer_utils.base import BaseValidateSerializer class S3ConfigSerializer(BaseValidateSerializer): diff --git a/api/src/backend/api/v1/serializer_utils/processors.py b/api/src/backend/api/v1/serializer_utils/processors.py new file mode 100644 index 0000000000..4022f3f2bc --- /dev/null +++ b/api/src/backend/api/v1/serializer_utils/processors.py @@ -0,0 +1,21 @@ +from drf_spectacular.utils import extend_schema_field + +from api.v1.serializer_utils.base import YamlOrJsonField + +from prowler.lib.mutelist.mutelist import mutelist_schema + + +@extend_schema_field( + { + "oneOf": [ + { + "type": "object", + "title": "Mutelist", + "properties": {"Mutelist": mutelist_schema}, + "additionalProperties": False, + }, + ] + } +) +class ProcessorConfigField(YamlOrJsonField): + pass diff --git a/api/src/backend/api/v1/serializers.py b/api/src/backend/api/v1/serializers.py index 73d9cea5b8..2c9b044d51 100644 --- a/api/src/backend/api/v1/serializers.py +++ b/api/src/backend/api/v1/serializers.py @@ -7,6 +7,7 @@ from django.contrib.auth.models import update_last_login from django.contrib.auth.password_validation import validate_password from drf_spectacular.utils import extend_schema_field from jwt.exceptions import InvalidKeyError +from rest_framework.validators import UniqueTogetherValidator from rest_framework_json_api import serializers from rest_framework_json_api.serializers import ValidationError from rest_framework_simplejwt.exceptions import TokenError @@ -21,6 +22,7 @@ from api.models import ( InvitationRoleRelationship, LighthouseConfiguration, Membership, + Processor, Provider, ProviderGroup, ProviderGroupMembership, @@ -44,7 +46,9 @@ from api.v1.serializer_utils.integrations import ( IntegrationCredentialField, S3ConfigSerializer, ) +from api.v1.serializer_utils.processors import ProcessorConfigField from api.v1.serializer_utils.providers import ProviderSecretField +from prowler.lib.mutelist.mutelist import Mutelist # Tokens @@ -130,6 +134,12 @@ class TokenSerializer(BaseTokenSerializer): class TokenSocialLoginSerializer(BaseTokenSerializer): email = serializers.EmailField(write_only=True) + tenant_id = serializers.UUIDField( + write_only=True, + required=False, + help_text="If not provided, the tenant ID of the first membership that was added" + " to the user will be used.", + ) # Output tokens refresh = serializers.CharField(read_only=True) @@ -851,6 +861,7 @@ class ScanSerializer(RLSSerializer): "completed_at", "scheduled_at", "next_scan_at", + "processor", "url", ] @@ -1014,8 +1025,8 @@ class ResourceSerializer(RLSSerializer): } included_serializers = { - "findings": "api.v1.serializers.FindingSerializer", - "provider": "api.v1.serializers.ProviderSerializer", + "findings": "api.v1.serializers.FindingIncludeSerializer", + "provider": "api.v1.serializers.ProviderIncludeSerializer", } @extend_schema_field( @@ -1038,7 +1049,7 @@ class ResourceSerializer(RLSSerializer): class ResourceIncludeSerializer(RLSSerializer): """ - Serializer for the Resource model. + Serializer for the included Resource model. """ tags = serializers.SerializerMethodField() @@ -1104,6 +1115,7 @@ class FindingSerializer(RLSSerializer): "updated_at", "first_seen_at", "muted", + "muted_reason", "url", # Relationships "scan", @@ -1116,6 +1128,28 @@ class FindingSerializer(RLSSerializer): } +class FindingIncludeSerializer(RLSSerializer): + """ + Serializer for the include Finding model. + """ + + class Meta: + model = Finding + fields = [ + "id", + "uid", + "status", + "severity", + "check_id", + "check_metadata", + "inserted_at", + "updated_at", + "first_seen_at", + "muted", + "muted_reason", + ] + + # To be removed when the related endpoint is removed as well class FindingDynamicFilterSerializer(serializers.Serializer): services = serializers.ListField(child=serializers.CharField(), allow_empty=True) @@ -1200,8 +1234,8 @@ class M365ProviderSecret(serializers.Serializer): client_id = serializers.CharField() client_secret = serializers.CharField() tenant_id = serializers.CharField() - user = serializers.EmailField() - password = serializers.CharField() + user = serializers.EmailField(required=False) + password = serializers.CharField(required=False) class Meta: resource_name = "provider-secrets" @@ -1309,12 +1343,13 @@ class ProviderSecretUpdateSerializer(BaseWriteProviderSecretSerializer): "inserted_at": {"read_only": True}, "updated_at": {"read_only": True}, "provider": {"read_only": True}, - "secret_type": {"read_only": True}, + "secret_type": {"required": False}, } def validate(self, attrs): provider = self.instance.provider - secret_type = self.instance.secret_type + # To allow updating a secret with the same type without making the `secret_type` mandatory + secret_type = attrs.get("secret_type") or self.instance.secret_type secret = attrs.get("secret") validated_attrs = super().validate(attrs) @@ -2065,6 +2100,128 @@ class IntegrationUpdateSerializer(BaseWriteIntegrationSerializer): return super().update(instance, validated_data) +# Processors + + +class ProcessorSerializer(RLSSerializer): + """ + Serializer for the Processor model. + """ + + configuration = ProcessorConfigField() + + class Meta: + model = Processor + fields = [ + "id", + "inserted_at", + "updated_at", + "processor_type", + "configuration", + "url", + ] + + +class ProcessorCreateSerializer(RLSSerializer, BaseWriteSerializer): + configuration = ProcessorConfigField(required=True) + + class Meta: + model = Processor + fields = [ + "inserted_at", + "updated_at", + "processor_type", + "configuration", + ] + extra_kwargs = { + "inserted_at": {"read_only": True}, + "updated_at": {"read_only": True}, + } + validators = [ + UniqueTogetherValidator( + queryset=Processor.objects.all(), + fields=["processor_type"], + message="A processor with the same type already exists.", + ) + ] + + def validate(self, attrs): + validated_attrs = super().validate(attrs) + self.validate_processor_data(attrs) + return validated_attrs + + def validate_processor_data(self, attrs): + processor_type = attrs.get("processor_type") + configuration = attrs.get("configuration") + if processor_type == "mutelist": + self.validate_mutelist_configuration(configuration) + + def validate_mutelist_configuration(self, configuration): + if not isinstance(configuration, dict): + raise serializers.ValidationError("Invalid Mutelist configuration.") + + mutelist_configuration = configuration.get("Mutelist", {}) + + if not mutelist_configuration: + raise serializers.ValidationError( + "Invalid Mutelist configuration: 'Mutelist' is a required property." + ) + + try: + Mutelist.validate_mutelist(mutelist_configuration, raise_on_exception=True) + return + except Exception as error: + raise serializers.ValidationError( + f"Invalid Mutelist configuration: {error}" + ) + + +class ProcessorUpdateSerializer(BaseWriteSerializer): + configuration = ProcessorConfigField(required=True) + + class Meta: + model = Processor + fields = [ + "inserted_at", + "updated_at", + "configuration", + ] + extra_kwargs = { + "inserted_at": {"read_only": True}, + "updated_at": {"read_only": True}, + } + + def validate(self, attrs): + validated_attrs = super().validate(attrs) + self.validate_processor_data(attrs) + return validated_attrs + + def validate_processor_data(self, attrs): + processor_type = self.instance.processor_type + configuration = attrs.get("configuration") + if processor_type == "mutelist": + self.validate_mutelist_configuration(configuration) + + def validate_mutelist_configuration(self, configuration): + if not isinstance(configuration, dict): + raise serializers.ValidationError("Invalid Mutelist configuration.") + + mutelist_configuration = configuration.get("Mutelist", {}) + + if not mutelist_configuration: + raise serializers.ValidationError( + "Invalid Mutelist configuration: 'Mutelist' is a required property." + ) + + try: + Mutelist.validate_mutelist(mutelist_configuration, raise_on_exception=True) + return + except Exception as error: + raise serializers.ValidationError( + f"Invalid Mutelist configuration: {error}" + ) + + # SSO diff --git a/api/src/backend/api/v1/urls.py b/api/src/backend/api/v1/urls.py index c96a6f56d0..8ee4458fc1 100644 --- a/api/src/backend/api/v1/urls.py +++ b/api/src/backend/api/v1/urls.py @@ -1,9 +1,11 @@ +from allauth.socialaccount.providers.saml.views import ACSView, MetadataView, SLSView from django.urls import include, path from drf_spectacular.views import SpectacularRedocView from rest_framework_nested import routers from api.v1.views import ( ComplianceOverviewViewSet, + CustomSAMLLoginView, CustomTokenObtainView, CustomTokenRefreshView, CustomTokenSwitchTenantView, @@ -16,6 +18,7 @@ from api.v1.views import ( LighthouseConfigViewSet, MembershipViewSet, OverviewViewSet, + ProcessorViewSet, ProviderGroupProvidersRelationshipView, ProviderGroupViewSet, ProviderSecretViewSet, @@ -25,6 +28,7 @@ from api.v1.views import ( RoleViewSet, SAMLConfigurationViewSet, SAMLInitiateAPIView, + SAMLTokenValidateView, ScanViewSet, ScheduleViewSet, SchemaView, @@ -53,6 +57,7 @@ router.register( router.register(r"overviews", OverviewViewSet, basename="overview") router.register(r"schedules", ScheduleViewSet, basename="schedule") router.register(r"integrations", IntegrationViewSet, basename="integration") +router.register(r"processors", ProcessorViewSet, basename="processor") router.register(r"saml-config", SAMLConfigurationViewSet, basename="saml-config") router.register( r"lighthouse-configurations", @@ -126,13 +131,32 @@ urlpatterns = [ path( "auth/saml/initiate/", SAMLInitiateAPIView.as_view(), name="api_saml_initiate" ), - # Allauth SAML endpoints for tenants - path("accounts/", include("allauth.urls")), path( - "api/v1/accounts/saml//acs/finish/", + "accounts/saml//login/", + CustomSAMLLoginView.as_view(), + name="saml_login", + ), + path( + "accounts/saml//acs/", + ACSView.as_view(), + name="saml_acs", + ), + path( + "accounts/saml//acs/finish/", TenantFinishACSView.as_view(), name="saml_finish_acs", ), + path( + "accounts/saml//sls/", + SLSView.as_view(), + name="saml_sls", + ), + path( + "accounts/saml//metadata/", + MetadataView.as_view(), + name="saml_metadata", + ), + path("tokens/saml", SAMLTokenValidateView.as_view(), name="token-saml"), path("tokens/google", GoogleSocialLoginView.as_view(), name="token-google"), path("tokens/github", GithubSocialLoginView.as_view(), name="token-github"), path("", include(router.urls)), diff --git a/api/src/backend/api/v1/views.py b/api/src/backend/api/v1/views.py index 139ba61928..4f7075ff78 100644 --- a/api/src/backend/api/v1/views.py +++ b/api/src/backend/api/v1/views.py @@ -1,12 +1,13 @@ import glob import os from datetime import datetime, timedelta, timezone +from urllib.parse import urljoin import sentry_sdk from allauth.socialaccount.models import SocialAccount, SocialApp from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter -from allauth.socialaccount.providers.saml.views import FinishACSView +from allauth.socialaccount.providers.saml.views import FinishACSView, LoginView from botocore.exceptions import ClientError, NoCredentialsError, ParamValidationError from celery.result import AsyncResult from config.env import env @@ -21,7 +22,7 @@ from django.contrib.postgres.search import SearchQuery from django.db import transaction from django.db.models import Count, Exists, F, OuterRef, Prefetch, Q, Sum from django.db.models.functions import Coalesce -from django.http import HttpResponse, JsonResponse +from django.http import HttpResponse from django.shortcuts import redirect from django.urls import reverse from django.utils.dateparse import parse_date @@ -76,6 +77,7 @@ from api.filters import ( InvitationFilter, LatestFindingFilter, MembershipFilter, + ProcessorFilter, ProviderFilter, ProviderGroupFilter, ProviderSecretFilter, @@ -96,6 +98,7 @@ from api.models import ( Invitation, LighthouseConfiguration, Membership, + Processor, Provider, ProviderGroup, ProviderGroupMembership, @@ -107,6 +110,7 @@ from api.models import ( RoleProviderGroupRelationship, SAMLConfiguration, SAMLDomainIndex, + SAMLToken, Scan, ScanSummary, SeverityChoices, @@ -148,6 +152,9 @@ from api.v1.serializers import ( OverviewProviderSerializer, OverviewServiceSerializer, OverviewSeveritySerializer, + ProcessorCreateSerializer, + ProcessorSerializer, + ProcessorUpdateSerializer, ProviderCreateSerializer, ProviderGroupCreateSerializer, ProviderGroupMembershipSerializer, @@ -345,6 +352,11 @@ class SchemaView(SpectacularAPIView): "description": "Endpoints for managing Lighthouse configurations, including creation, retrieval, " "updating, and deletion of configurations such as OpenAI keys, models, and business context.", }, + { + "name": "Processor", + "description": "Endpoints for managing post-processors used to process Prowler findings, including " + "registration, configuration, and deletion of post-processing actions.", + }, ] return super().get(request, *args, **kwargs) @@ -401,17 +413,68 @@ class GithubSocialLoginView(SocialLoginView): return original_response +@extend_schema(exclude=True) +class SAMLTokenValidateView(GenericAPIView): + resource_name = "tokens" + http_method_names = ["post"] + + def post(self, request): + token_id = request.query_params.get("id", "invalid") + try: + saml_token = SAMLToken.objects.using(MainRouter.admin_db).get(id=token_id) + except SAMLToken.DoesNotExist: + return Response({"detail": "Invalid token ID."}, status=404) + + if saml_token.is_expired(): + return Response({"detail": "Token expired."}, status=400) + + token_data = saml_token.token + # Currently we don't store the tokens in the database, so we delete the token after use + saml_token.delete() + + return Response(token_data, status=200) + + +@extend_schema(exclude=True) +class CustomSAMLLoginView(LoginView): + def dispatch(self, request, *args, **kwargs): + """ + Convert GET requests to POST to bypass allauth's confirmation screen. + + Why this is necessary: + - django-allauth requires POST for social logins to prevent open redirect attacks + - SAML login links typically use GET requests (e.g., ) + - This conversion allows seamless login without user-facing confirmation + + Security considerations: + 1. Preserves CSRF protection: Original POST handling remains intact + 2. Avoids global SOCIALACCOUNT_LOGIN_ON_GET=True which would: + - Enable GET logins for ALL providers (security risk) + - Potentially expose open redirect vulnerabilities + 3. SAML payloads remain signed/encrypted regardless of HTTP method + 4. No sensitive parameters are exposed in URLs (copied to POST body) + + This approach maintains security while providing better UX. + """ + if request.method == "GET": + # Convert GET to POST while preserving parameters + request.method = "POST" + return super().dispatch(request, *args, **kwargs) + + @extend_schema(exclude=True) class SAMLInitiateAPIView(GenericAPIView): serializer_class = SamlInitiateSerializer permission_classes = [] def post(self, request, *args, **kwargs): + # Validate the input payload and extract the domain serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) email = serializer.validated_data["email_domain"] domain = email.split("@", 1)[-1].lower() + # Retrieve the SAML configuration for the given email domain try: check = SAMLDomainIndex.objects.get(email_domain=domain) with rls_transaction(str(check.tenant_id)): @@ -421,20 +484,24 @@ class SAMLInitiateAPIView(GenericAPIView): {"detail": "Unauthorized domain."}, status=status.HTTP_403_FORBIDDEN ) - # Check certificates are not empty - saml_public_cert = os.getenv("SAML_PUBLIC_CERT", "").strip() - saml_private_key = os.getenv("SAML_PRIVATE_KEY", "").strip() + # Check certificates are not empty (TODO: Validate certificates) + # saml_public_cert = os.getenv("SAML_PUBLIC_CERT", "").strip() + # saml_private_key = os.getenv("SAML_PRIVATE_KEY", "").strip() - if not saml_public_cert or not saml_private_key: - return Response( - {"detail": "SAML configuration is invalid: missing certificates."}, - status=status.HTTP_403_FORBIDDEN, - ) + # if not saml_public_cert or not saml_private_key: + # return Response( + # {"detail": "SAML configuration is invalid: missing certificates."}, + # status=status.HTTP_403_FORBIDDEN, + # ) - saml_login_url = reverse( + # Build the SAML login URL using the configured API host + api_host = os.getenv("API_BASE_URL") + login_path = reverse( "saml_login", kwargs={"organization_slug": config.email_domain} ) - return redirect(f"{saml_login_url}?email={email}") + login_url = urljoin(api_host, login_path) + + return redirect(login_url) @extend_schema_view( @@ -493,26 +560,46 @@ class SAMLConfigurationViewSet(BaseRLSViewSet): class TenantFinishACSView(FinishACSView): def dispatch(self, request, organization_slug): - response = super().dispatch(request, organization_slug) + super().dispatch(request, organization_slug) user = getattr(request, "user", None) if not user or not user.is_authenticated: - return response + callback_url = env.str("AUTH_URL") + return redirect(f"{callback_url}?sso_saml_failed=true") + # Defensive check to avoid edge case failures due to inconsistent or incomplete data in the database + # This handles scenarios like partially deleted or missing related objects try: + check = SAMLDomainIndex.objects.get(email_domain=organization_slug) + with rls_transaction(str(check.tenant_id)): + SAMLConfiguration.objects.get(tenant_id=str(check.tenant_id)) social_app = SocialApp.objects.get( provider="saml", client_id=organization_slug ) + user_id = User.objects.get(email=str(user)).id social_account = SocialAccount.objects.get( - user=user, provider=social_app.provider + user=str(user_id), provider=social_app.provider_id ) - except (SocialApp.DoesNotExist, SocialAccount.DoesNotExist): - return response + except ( + SAMLDomainIndex.DoesNotExist, + SAMLConfiguration.DoesNotExist, + SocialApp.DoesNotExist, + SocialAccount.DoesNotExist, + User.DoesNotExist, + ): + callback_url = env.str("AUTH_URL") + return redirect(f"{callback_url}?sso_saml_failed=true") extra = social_account.extra_data - user.first_name = extra.get("firstName", [""])[0] - user.last_name = extra.get("lastName", [""])[0] - user.company_name = extra.get("organization", [""])[0] + user.first_name = ( + extra.get("firstName", [""])[0] if extra.get("firstName") else "" + ) + user.last_name = extra.get("lastName", [""])[0] if extra.get("lastName") else "" + user.company_name = ( + extra.get("organization", [""])[0] if extra.get("organization") else "" + ) user.name = f"{user.first_name} {user.last_name}".strip() + if user.name == "": + user.name = "N/A" user.save() email_domain = user.email.split("@")[-1] @@ -521,7 +608,11 @@ class TenantFinishACSView(FinishACSView): .get(email_domain=email_domain) .tenant ) - role_name = extra.get("userType", ["saml_default_role"])[0].strip() + role_name = ( + extra.get("userType", ["no_permissions"])[0].strip() + if extra.get("userType") + else "no_permissions" + ) try: role = Role.objects.using(MainRouter.admin_db).get( name=role_name, tenant=tenant @@ -547,16 +638,30 @@ class TenantFinishACSView(FinishACSView): role=role, tenant_id=tenant.id, ) - - serializer = TokenSocialLoginSerializer(data={"email": user.email}) - serializer.is_valid(raise_exception=True) - return JsonResponse( - { - "type": "saml-social-tokens", - "attributes": serializer.validated_data, - } + membership, _ = Membership.objects.using(MainRouter.admin_db).get_or_create( + user=user, + tenant=tenant, + defaults={ + "user": user, + "tenant": tenant, + "role": Membership.RoleChoices.MEMBER, + }, ) + serializer = TokenSocialLoginSerializer( + data={"email": user.email, "tenant_id": str(tenant.id)} + ) + serializer.is_valid(raise_exception=True) + + token_data = serializer.validated_data + saml_token = SAMLToken.objects.using(MainRouter.admin_db).create( + token=token_data, user=user + ) + callback_url = env.str("SAML_SSO_CALLBACK_URL") + redirect_url = f"{callback_url}?id={saml_token.id}" + + return redirect(redirect_url) + @extend_schema_view( list=extend_schema( @@ -2056,9 +2161,12 @@ class FindingViewSet(PaginateByPkMixin, BaseRLSViewSet): # ToRemove: Temporary fallback mechanism if not queryset.exists(): - scan_ids = Scan.objects.filter( + raw_scans_ids = Scan.objects.filter( tenant_id=tenant_id, **scan_based_filters - ).values_list("id", flat=True) + ).values_list("id", "unique_resource_count") + scan_ids = [ + scan_id for scan_id, count in raw_scans_ids if count and count > 0 + ] for scan_id in scan_ids: backfill_scan_resource_summaries_task.apply_async( kwargs={"tenant_id": tenant_id, "scan_id": scan_id} @@ -2144,7 +2252,12 @@ class FindingViewSet(PaginateByPkMixin, BaseRLSViewSet): .order_by("provider_id", "-inserted_at") .distinct("provider_id") ) - latest_scans_ids = list(latest_scans_queryset.values_list("id", flat=True)) + raw_latest_scans_ids = list( + latest_scans_queryset.values_list("id", "unique_resource_count") + ) + latest_scans_ids = [ + scan_id for scan_id, count in raw_latest_scans_ids if count and count > 0 + ] queryset = ResourceScanSummary.objects.filter( tenant_id=tenant_id, @@ -3038,9 +3151,9 @@ class ComplianceOverviewViewSet(BaseRLSViewSet, TaskManagementMixin): return Response(serializer.data, status=status.HTTP_200_OK) +@extend_schema(tags=["Overview"]) @extend_schema_view( - list=extend_schema( - tags=["Overview"], + providers=extend_schema( summary="Get aggregated provider data", description=( "Retrieve an aggregated overview of findings and resources grouped by providers. " @@ -3287,6 +3400,7 @@ class OverviewViewSet(BaseRLSViewSet): return Response(serializer.data, status=status.HTTP_200_OK) +@extend_schema(tags=["Schedule"]) @extend_schema_view( daily=extend_schema( summary="Create a daily schedule scan for a given provider", @@ -3481,3 +3595,54 @@ class LighthouseConfigViewSet(BaseRLSViewSet): ) }, ) + + +@extend_schema_view( + list=extend_schema( + tags=["Processor"], + summary="List all processors", + description="Retrieve a list of all configured processors with options for filtering by various criteria.", + ), + retrieve=extend_schema( + tags=["Processor"], + summary="Retrieve processor details", + description="Fetch detailed information about a specific processor by its ID.", + ), + create=extend_schema( + tags=["Processor"], + summary="Create a new processor", + description="Register a new processor with the system, providing necessary configuration details. There can " + "only be one processor of each type per tenant.", + ), + partial_update=extend_schema( + tags=["Processor"], + summary="Partially update a processor", + description="Modify certain fields of an existing processor without affecting other settings.", + ), + destroy=extend_schema( + tags=["Processor"], + summary="Delete a processor", + description="Remove a processor from the system by its ID.", + ), +) +@method_decorator(CACHE_DECORATOR, name="list") +@method_decorator(CACHE_DECORATOR, name="retrieve") +class ProcessorViewSet(BaseRLSViewSet): + queryset = Processor.objects.all() + serializer_class = ProcessorSerializer + http_method_names = ["get", "post", "patch", "delete"] + filterset_class = ProcessorFilter + ordering = ["processor_type", "-inserted_at"] + # RBAC required permissions + required_permissions = [Permissions.MANAGE_ACCOUNT] + + def get_queryset(self): + queryset = Processor.objects.filter(tenant_id=self.request.tenant_id) + return queryset + + def get_serializer_class(self): + if self.action == "create": + return ProcessorCreateSerializer + elif self.action == "partial_update": + return ProcessorUpdateSerializer + return super().get_serializer_class() diff --git a/api/src/backend/api/validators.py b/api/src/backend/api/validators.py index 135a0fd6c0..543406f202 100644 --- a/api/src/backend/api/validators.py +++ b/api/src/backend/api/validators.py @@ -1,3 +1,5 @@ +import string + from django.core.exceptions import ValidationError from django.utils.translation import gettext as _ @@ -20,3 +22,89 @@ class MaximumLengthValidator: return _( f"Your password must contain no more than {self.max_length} characters." ) + + +class SpecialCharactersValidator: + def __init__(self, special_characters=None, min_special_characters=1): + # Use string.punctuation if no custom characters provided + self.special_characters = special_characters or string.punctuation + self.min_special_characters = min_special_characters + + def validate(self, password, user=None): + if ( + sum(1 for char in password if char in self.special_characters) + < self.min_special_characters + ): + raise ValidationError( + _("This password must contain at least one special character."), + code="password_no_special_characters", + params={ + "special_characters": self.special_characters, + "min_special_characters": self.min_special_characters, + }, + ) + + def get_help_text(self): + return _( + f"Your password must contain at least one special character from: {self.special_characters}" + ) + + +class UppercaseValidator: + def __init__(self, min_uppercase=1): + self.min_uppercase = min_uppercase + + def validate(self, password, user=None): + if sum(1 for char in password if char.isupper()) < self.min_uppercase: + raise ValidationError( + _( + "This password must contain at least %(min_uppercase)d uppercase letter." + ), + code="password_no_uppercase_letters", + params={"min_uppercase": self.min_uppercase}, + ) + + def get_help_text(self): + return _( + f"Your password must contain at least {self.min_uppercase} uppercase letter." + ) + + +class LowercaseValidator: + def __init__(self, min_lowercase=1): + self.min_lowercase = min_lowercase + + def validate(self, password, user=None): + if sum(1 for char in password if char.islower()) < self.min_lowercase: + raise ValidationError( + _( + "This password must contain at least %(min_lowercase)d lowercase letter." + ), + code="password_no_lowercase_letters", + params={"min_lowercase": self.min_lowercase}, + ) + + def get_help_text(self): + return _( + f"Your password must contain at least {self.min_lowercase} lowercase letter." + ) + + +class NumericValidator: + def __init__(self, min_numeric=1): + self.min_numeric = min_numeric + + def validate(self, password, user=None): + if sum(1 for char in password if char.isdigit()) < self.min_numeric: + raise ValidationError( + _( + "This password must contain at least %(min_numeric)d numeric character." + ), + code="password_no_numeric_characters", + params={"min_numeric": self.min_numeric}, + ) + + def get_help_text(self): + return _( + f"Your password must contain at least {self.min_numeric} numeric character." + ) diff --git a/api/src/backend/config/django/base.py b/api/src/backend/config/django/base.py index 47c6320ce1..514dcfc6ff 100644 --- a/api/src/backend/config/django/base.py +++ b/api/src/backend/config/django/base.py @@ -11,6 +11,7 @@ SECRET_KEY = env("SECRET_KEY", default="secret") DEBUG = env.bool("DJANGO_DEBUG", default=False) ALLOWED_HOSTS = ["localhost", "127.0.0.1"] SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") +USE_X_FORWARDED_HOST = True # Application definition @@ -158,6 +159,30 @@ AUTH_PASSWORD_VALIDATORS = [ { "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, + { + "NAME": "api.validators.SpecialCharactersValidator", + "OPTIONS": { + "min_special_characters": 1, + }, + }, + { + "NAME": "api.validators.UppercaseValidator", + "OPTIONS": { + "min_uppercase": 1, + }, + }, + { + "NAME": "api.validators.LowercaseValidator", + "OPTIONS": { + "min_lowercase": 1, + }, + }, + { + "NAME": "api.validators.NumericValidator", + "OPTIONS": { + "min_numeric": 1, + }, + }, ] SIMPLE_JWT = { @@ -248,3 +273,7 @@ X_FRAME_OPTIONS = "DENY" SECURE_REFERRER_POLICY = "strict-origin-when-cross-origin" DJANGO_DELETION_BATCH_SIZE = env.int("DJANGO_DELETION_BATCH_SIZE", 5000) + +# SAML requirement +CSRF_COOKIE_SECURE = True +SESSION_COOKIE_SECURE = True diff --git a/api/src/backend/config/settings/sentry.py b/api/src/backend/config/settings/sentry.py index 648324707f..f6752f0646 100644 --- a/api/src/backend/config/settings/sentry.py +++ b/api/src/backend/config/settings/sentry.py @@ -4,6 +4,7 @@ from config.env import env IGNORED_EXCEPTIONS = [ # Provider is not connected due to credentials errors "is not connected", + "ProviderConnectionError", # Authentication Errors from AWS "InvalidToken", "AccessDeniedException", @@ -16,7 +17,7 @@ IGNORED_EXCEPTIONS = [ "InternalServerErrorException", "AccessDenied", "No Shodan API Key", # Shodan Check - "RequestLimitExceeded", # For now we don't want to log the RequestLimitExceeded errors + "RequestLimitExceeded", # For now, we don't want to log the RequestLimitExceeded errors "ThrottlingException", "Rate exceeded", "SubscriptionRequiredException", @@ -42,7 +43,9 @@ IGNORED_EXCEPTIONS = [ "AWSAccessKeyIDInvalidError", "AWSSessionTokenExpiredError", "EndpointConnectionError", # AWS Service is not available in a region - "Pool is closed", # The following comes from urllib3: eu-west-1 -- HTTPClientError[126]: An HTTP Client raised an unhandled exception: AWSHTTPSConnectionPool(host='hostname.s3.eu-west-1.amazonaws.com', port=443): Pool is closed. + # The following comes from urllib3: eu-west-1 -- HTTPClientError[126]: An HTTP Client raised an + # unhandled exception: AWSHTTPSConnectionPool(host='hostname.s3.eu-west-1.amazonaws.com', port=443): Pool is closed. + "Pool is closed", # Authentication Errors from GCP "ClientAuthenticationError", "AuthorizationFailed", @@ -71,7 +74,7 @@ IGNORED_EXCEPTIONS = [ def before_send(event, hint): """ - before_send handles the Sentry events in order to sent them or not + before_send handles the Sentry events in order to send them or not """ # Ignore logs with the ignored_exceptions # https://docs.python.org/3/library/logging.html#logrecord-objects diff --git a/api/src/backend/config/settings/social_login.py b/api/src/backend/config/settings/social_login.py index cf4e21d75b..d6b5b53df2 100644 --- a/api/src/backend/config/settings/social_login.py +++ b/api/src/backend/config/settings/social_login.py @@ -25,9 +25,18 @@ SOCIALACCOUNT_EMAIL_AUTHENTICATION = True SOCIALACCOUNT_EMAIL_AUTHENTICATION_AUTO_CONNECT = True SOCIALACCOUNT_ADAPTER = "api.adapters.ProwlerSocialAccountAdapter" -# SAML keys -SAML_PUBLIC_CERT = env("SAML_PUBLIC_CERT", default="") -SAML_PRIVATE_KEY = env("SAML_PRIVATE_KEY", default="") + +# def inline(pem: str) -> str: +# return "".join( +# line.strip() +# for line in pem.splitlines() +# if "CERTIFICATE" not in line and "KEY" not in line +# ) + + +# # SAML keys (TODO: Validate certificates) +# SAML_PUBLIC_CERT = inline(env("SAML_PUBLIC_CERT", default="")) +# SAML_PRIVATE_KEY = inline(env("SAML_PRIVATE_KEY", default="")) SOCIALACCOUNT_PROVIDERS = { "google": { @@ -60,12 +69,14 @@ SOCIALACCOUNT_PROVIDERS = { "entity_id": "urn:prowler.com:sp", }, "advanced": { - "x509cert": SAML_PUBLIC_CERT, - "private_key": SAML_PRIVATE_KEY, + # TODO: Validate certificates + # "x509cert": SAML_PUBLIC_CERT, + # "private_key": SAML_PRIVATE_KEY, + # "authn_request_signed": True, + # "want_message_signed": True, + # "want_assertion_signed": True, + "reject_idp_initiated_sso": False, "name_id_format": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", - "authn_request_signed": True, - "want_assertion_signed": True, - "want_message_signed": True, }, }, } diff --git a/api/src/backend/conftest.py b/api/src/backend/conftest.py index c601e07b80..6024fb3474 100644 --- a/api/src/backend/conftest.py +++ b/api/src/backend/conftest.py @@ -23,6 +23,7 @@ from api.models import ( Invitation, LighthouseConfiguration, Membership, + Processor, Provider, ProviderGroup, ProviderSecret, @@ -381,8 +382,27 @@ def providers_fixture(tenants_fixture): tenant_id=tenant.id, scanner_args={"key1": "value1", "key2": {"key21": "value21"}}, ) + provider6 = Provider.objects.create( + provider="m365", + uid="m365.test.com", + alias="m365_testing", + tenant_id=tenant.id, + ) - return provider1, provider2, provider3, provider4, provider5 + return provider1, provider2, provider3, provider4, provider5, provider6 + + +@pytest.fixture +def processor_fixture(tenants_fixture): + tenant, *_ = tenants_fixture + processor = Processor.objects.create( + tenant_id=tenant.id, + processor_type="mutelist", + configuration="Mutelist:\n Accounts:\n *:\n Checks:\n iam_user_hardware_mfa_enabled:\n " + " Regions:\n - *\n Resources:\n - *", + ) + + return processor @pytest.fixture @@ -1118,7 +1138,7 @@ def latest_scan_finding(authenticated_client, providers_fixture, resources_fixtu @pytest.fixture def saml_setup(tenants_fixture): tenant_id = tenants_fixture[0].id - domain = "example.com" + domain = "prowler.com" SAMLDomainIndex.objects.create(email_domain=domain, tenant_id=tenant_id) diff --git a/api/src/backend/tasks/jobs/export.py b/api/src/backend/tasks/jobs/export.py index 3525da2425..f4b389d8f0 100644 --- a/api/src/backend/tasks/jobs/export.py +++ b/api/src/backend/tasks/jobs/export.py @@ -31,6 +31,7 @@ from prowler.lib.outputs.compliance.iso27001.iso27001_gcp import GCPISO27001 from prowler.lib.outputs.compliance.iso27001.iso27001_kubernetes import ( KubernetesISO27001, ) +from prowler.lib.outputs.compliance.iso27001.iso27001_m365 import M365ISO27001 from prowler.lib.outputs.compliance.kisa_ismsp.kisa_ismsp_aws import AWSKISAISMSP from prowler.lib.outputs.compliance.mitre_attack.mitre_attack_aws import AWSMitreAttack from prowler.lib.outputs.compliance.mitre_attack.mitre_attack_azure import ( @@ -90,6 +91,7 @@ COMPLIANCE_CLASS_MAP = { "m365": [ (lambda name: name.startswith("cis_"), M365CIS), (lambda name: name == "prowler_threatscore_m365", ProwlerThreatScoreM365), + (lambda name: name.startswith("iso27001_"), M365ISO27001), ], } diff --git a/api/src/backend/tasks/jobs/scan.py b/api/src/backend/tasks/jobs/scan.py index 684fbfd759..118d62693c 100644 --- a/api/src/backend/tasks/jobs/scan.py +++ b/api/src/backend/tasks/jobs/scan.py @@ -14,9 +14,11 @@ from api.compliance import ( generate_scan_compliance, ) from api.db_utils import create_objects_in_batches, rls_transaction +from api.exceptions import ProviderConnectionError from api.models import ( ComplianceRequirementOverview, Finding, + Processor, Provider, Resource, ResourceScanSummary, @@ -26,7 +28,7 @@ from api.models import ( StateChoices, ) from api.models import StatusChoices as FindingStatus -from api.utils import initialize_prowler_provider +from api.utils import initialize_prowler_provider, return_prowler_provider from api.v1.serializers import ScanTaskSerializer from prowler.lib.outputs.finding import Finding as ProwlerFinding from prowler.lib.scan.scan import Scan as ProwlerScan @@ -132,14 +134,28 @@ def perform_prowler_scan( scan_instance.started_at = datetime.now(tz=timezone.utc) scan_instance.save() + # Find the mutelist processor if it exists + with rls_transaction(tenant_id): + try: + mutelist_processor = Processor.objects.get( + tenant_id=tenant_id, processor_type=Processor.ProcessorChoices.MUTELIST + ) + except Processor.DoesNotExist: + mutelist_processor = None + except Exception as e: + logger.error(f"Error processing mutelist rules: {e}") + mutelist_processor = None + try: with rls_transaction(tenant_id): try: - prowler_provider = initialize_prowler_provider(provider_instance) + prowler_provider = initialize_prowler_provider( + provider_instance, mutelist_processor + ) provider_instance.connected = True except Exception as e: provider_instance.connected = False - exc = ValueError( + exc = ProviderConnectionError( f"Provider {provider_instance.provider} is not connected: {e}" ) finally: @@ -149,7 +165,8 @@ def perform_prowler_scan( provider_instance.save() # 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 as not connected. + # If raised within the transaction, the transaction will be rolled back and the provider will not be marked + # as not connected. if exc: raise exc @@ -273,6 +290,9 @@ def perform_prowler_scan( if not last_first_seen_at: last_first_seen_at = datetime.now(tz=timezone.utc) + # If the finding is muted at this time the reason must be the configured Mutelist + muted_reason = "Muted by mutelist" if finding.muted else None + # Create the finding finding_instance = Finding.objects.create( tenant_id=tenant_id, @@ -288,6 +308,7 @@ def perform_prowler_scan( scan=scan_instance, first_seen_at=last_first_seen_at, muted=finding.muted, + muted_reason=muted_reason, compliance=finding.compliance, ) finding_instance.add_resources([resource_instance]) @@ -526,7 +547,7 @@ def create_compliance_requirements(tenant_id: str, scan_id: str): with rls_transaction(tenant_id): scan_instance = Scan.objects.get(pk=scan_id) provider_instance = scan_instance.provider - prowler_provider = initialize_prowler_provider(provider_instance) + prowler_provider = return_prowler_provider(provider_instance) # Get check status data by region from findings check_status_by_region = {} diff --git a/api/src/backend/tasks/tasks.py b/api/src/backend/tasks/tasks.py index 998e6773be..addb2c247e 100644 --- a/api/src/backend/tasks/tasks.py +++ b/api/src/backend/tasks/tasks.py @@ -37,6 +37,26 @@ from prowler.lib.outputs.finding import Finding as FindingOutput logger = get_task_logger(__name__) +def _perform_scan_complete_tasks(tenant_id: str, scan_id: str, provider_id: str): + """ + Helper function to perform tasks after a scan is completed. + + Args: + tenant_id (str): The tenant ID under which the scan was performed. + scan_id (str): The ID of the scan that was performed. + provider_id (str): The primary key of the Provider instance that was scanned. + """ + create_compliance_requirements_task.apply_async( + kwargs={"tenant_id": tenant_id, "scan_id": scan_id} + ) + chain( + perform_scan_summary_task.si(tenant_id=tenant_id, scan_id=scan_id), + generate_outputs_task.si( + scan_id=scan_id, provider_id=provider_id, tenant_id=tenant_id + ), + ).apply_async() + + @shared_task(base=RLSTask, name="provider-connection-check") @set_tenant def check_provider_connection_task(provider_id: str): @@ -103,13 +123,7 @@ def perform_scan_task( checks_to_execute=checks_to_execute, ) - chain( - perform_scan_summary_task.si(tenant_id, scan_id), - create_compliance_requirements_task.si(tenant_id=tenant_id, scan_id=scan_id), - generate_outputs.si( - scan_id=scan_id, provider_id=provider_id, tenant_id=tenant_id - ), - ).apply_async() + _perform_scan_complete_tasks(tenant_id, scan_id, provider_id) return result @@ -214,20 +228,12 @@ def perform_scheduled_scan_task(self, tenant_id: str, provider_id: str): scheduler_task_id=periodic_task_instance.id, ) - chain( - perform_scan_summary_task.si(tenant_id, scan_instance.id), - create_compliance_requirements_task.si( - tenant_id=tenant_id, scan_id=str(scan_instance.id) - ), - generate_outputs.si( - scan_id=str(scan_instance.id), provider_id=provider_id, tenant_id=tenant_id - ), - ).apply_async() + _perform_scan_complete_tasks(tenant_id, str(scan_instance.id), provider_id) return result -@shared_task(name="scan-summary") +@shared_task(name="scan-summary", queue="overview") def perform_scan_summary_task(tenant_id: str, scan_id: str): return aggregate_findings(tenant_id=tenant_id, scan_id=scan_id) @@ -243,7 +249,7 @@ def delete_tenant_task(tenant_id: str): queue="scan-reports", ) @set_tenant(keep_tenant=True) -def generate_outputs(scan_id: str, provider_id: str, tenant_id: str): +def generate_outputs_task(scan_id: str, provider_id: str, tenant_id: str): """ Process findings in batches and generate output files in multiple formats. @@ -381,7 +387,7 @@ def backfill_scan_resource_summaries_task(tenant_id: str, scan_id: str): return backfill_resource_scan_summaries(tenant_id=tenant_id, scan_id=scan_id) -@shared_task(base=RLSTask, name="scan-compliance-overviews") +@shared_task(base=RLSTask, name="scan-compliance-overviews", queue="overview") def create_compliance_requirements_task(tenant_id: str, scan_id: str): """ Creates detailed compliance requirement records for a scan. diff --git a/api/src/backend/tasks/tests/test_beat.py b/api/src/backend/tasks/tests/test_beat.py index 6e5c6fdf91..01ce0249e6 100644 --- a/api/src/backend/tasks/tests/test_beat.py +++ b/api/src/backend/tasks/tests/test_beat.py @@ -55,3 +55,22 @@ class TestScheduleProviderScan: assert "There is already a scheduled scan for this provider." in str( exc_info.value ) + + def test_remove_periodic_task(self, providers_fixture): + provider_instance = providers_fixture[0] + + assert Scan.objects.count() == 0 + with patch("tasks.tasks.perform_scheduled_scan_task.apply_async"): + schedule_provider_scan(provider_instance) + + assert Scan.objects.count() == 1 + scan = Scan.objects.first() + periodic_task = scan.scheduler_task + assert periodic_task is not None + + periodic_task.delete() + + scan.refresh_from_db() + # Assert the scan still exists but its scheduler_task is set to None + # Otherwise, Scan.DoesNotExist would be raised + assert Scan.objects.get(id=scan.id).scheduler_task is None diff --git a/api/src/backend/tasks/tests/test_scan.py b/api/src/backend/tasks/tests/test_scan.py index e4ec0d4d41..474543a82b 100644 --- a/api/src/backend/tasks/tests/test_scan.py +++ b/api/src/backend/tasks/tests/test_scan.py @@ -12,6 +12,7 @@ from tasks.jobs.scan import ( ) from tasks.utils import CustomEncoder +from api.exceptions import ProviderConnectionError from api.models import ( ComplianceRequirementOverview, Finding, @@ -158,6 +159,7 @@ class TestPerformScan: assert scan_finding.raw_result == finding.raw assert scan_finding.muted assert scan_finding.compliance == finding.compliance + assert scan_finding.muted_reason == "Muted by mutelist" assert scan_resource.tenant == tenant assert scan_resource.uid == finding.resource_uid @@ -203,7 +205,7 @@ class TestPerformScan: provider_id = str(provider.id) checks_to_execute = ["check1", "check2"] - with pytest.raises(ValueError): + with pytest.raises(ProviderConnectionError): perform_prowler_scan(tenant_id, scan_id, provider_id, checks_to_execute) scan.refresh_from_db() @@ -399,9 +401,7 @@ class TestCreateComplianceRequirements: ): with ( patch("api.db_utils.rls_transaction"), - patch( - "tasks.jobs.scan.initialize_prowler_provider" - ) as mock_initialize_prowler_provider, + patch("tasks.jobs.scan.return_prowler_provider") as mock_prowler_provider, patch( "tasks.jobs.scan.PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE" ) as mock_compliance_template, @@ -427,9 +427,7 @@ class TestCreateComplianceRequirements: "us-east-1", "us-west-2", ] - mock_initialize_prowler_provider.return_value = ( - mock_prowler_provider_instance - ) + mock_prowler_provider.return_value = mock_prowler_provider_instance mock_compliance_template.__getitem__.return_value = { "cis_1.4_aws": { @@ -512,9 +510,7 @@ class TestCreateComplianceRequirements: ): with ( patch("api.db_utils.rls_transaction"), - patch( - "tasks.jobs.scan.initialize_prowler_provider" - ) as mock_initialize_prowler_provider, + patch("tasks.jobs.scan.return_prowler_provider") as mock_prowler_provider, patch( "tasks.jobs.scan.PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE" ) as mock_compliance_template, @@ -557,9 +553,7 @@ class TestCreateComplianceRequirements: "us-east-1", "us-west-2", ] - mock_initialize_prowler_provider.return_value = ( - mock_prowler_provider_instance - ) + mock_prowler_provider.return_value = mock_prowler_provider_instance mock_compliance_template.__getitem__.return_value = { "test_compliance": { @@ -607,9 +601,7 @@ class TestCreateComplianceRequirements: ): with ( patch("api.db_utils.rls_transaction"), - patch( - "tasks.jobs.scan.initialize_prowler_provider" - ) as mock_initialize_prowler_provider, + patch("tasks.jobs.scan.return_prowler_provider") as mock_prowler_provider, patch( "tasks.jobs.scan.PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE" ) as mock_compliance_template, @@ -641,9 +633,7 @@ class TestCreateComplianceRequirements: mock_prowler_provider_instance.get_regions.side_effect = AttributeError( "No get_regions method" ) - mock_initialize_prowler_provider.return_value = ( - mock_prowler_provider_instance - ) + mock_prowler_provider.return_value = mock_prowler_provider_instance mock_compliance_template.__getitem__.return_value = { "kubernetes_cis": { @@ -676,9 +666,7 @@ class TestCreateComplianceRequirements: ): with ( patch("api.db_utils.rls_transaction"), - patch( - "tasks.jobs.scan.initialize_prowler_provider" - ) as mock_initialize_prowler_provider, + patch("tasks.jobs.scan.return_prowler_provider") as mock_prowler_provider, patch( "tasks.jobs.scan.PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE" ) as mock_compliance_template, @@ -704,9 +692,7 @@ class TestCreateComplianceRequirements: mock_prowler_provider_instance = MagicMock() mock_prowler_provider_instance.get_regions.return_value = ["us-east-1"] - mock_initialize_prowler_provider.return_value = ( - mock_prowler_provider_instance - ) + mock_prowler_provider.return_value = mock_prowler_provider_instance mock_compliance_template.__getitem__.return_value = { "cis_1.4_aws": { @@ -743,9 +729,7 @@ class TestCreateComplianceRequirements: ): with ( patch("api.db_utils.rls_transaction"), - patch( - "tasks.jobs.scan.initialize_prowler_provider" - ) as mock_initialize_prowler_provider, + patch("tasks.jobs.scan.return_prowler_provider") as mock_prowler_provider, ): tenant = tenants_fixture[0] scan = scans_fixture[0] @@ -759,7 +743,7 @@ class TestCreateComplianceRequirements: tenant_id = str(tenant.id) scan_id = str(scan.id) - mock_initialize_prowler_provider.side_effect = Exception( + mock_prowler_provider.side_effect = Exception( "Provider initialization failed" ) @@ -774,9 +758,7 @@ class TestCreateComplianceRequirements: ): with ( patch("api.db_utils.rls_transaction"), - patch( - "tasks.jobs.scan.initialize_prowler_provider" - ) as mock_initialize_prowler_provider, + patch("tasks.jobs.scan.return_prowler_provider") as mock_prowler_provider, patch( "tasks.jobs.scan.PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE" ) as mock_compliance_template, @@ -800,9 +782,7 @@ class TestCreateComplianceRequirements: mock_prowler_provider_instance = MagicMock() mock_prowler_provider_instance.get_regions.return_value = ["us-east-1"] - mock_initialize_prowler_provider.return_value = ( - mock_prowler_provider_instance - ) + mock_prowler_provider.return_value = mock_prowler_provider_instance mock_compliance_template.__getitem__.return_value = {} @@ -821,8 +801,8 @@ class TestCreateComplianceRequirements: with ( patch("api.db_utils.rls_transaction"), patch( - "tasks.jobs.scan.initialize_prowler_provider" - ) as mock_initialize_prowler_provider, + "tasks.jobs.scan.return_prowler_provider" + ) as mock_return_prowler_provider, patch( "tasks.jobs.scan.PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE" ) as mock_compliance_template, @@ -862,9 +842,7 @@ class TestCreateComplianceRequirements: mock_prowler_provider_instance = MagicMock() mock_prowler_provider_instance.get_regions.return_value = ["us-east-1"] - mock_initialize_prowler_provider.return_value = ( - mock_prowler_provider_instance - ) + mock_return_prowler_provider.return_value = mock_prowler_provider_instance mock_compliance_template.__getitem__.return_value = { "cis_1.4_aws": { @@ -898,8 +876,8 @@ class TestCreateComplianceRequirements: with ( patch("api.db_utils.rls_transaction"), patch( - "tasks.jobs.scan.initialize_prowler_provider" - ) as mock_initialize_prowler_provider, + "tasks.jobs.scan.return_prowler_provider" + ) as mock_return_prowler_provider, patch( "tasks.jobs.scan.PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE" ) as mock_compliance_template, @@ -911,7 +889,6 @@ class TestCreateComplianceRequirements: ): tenant = tenants_fixture[0] scan = scans_fixture[0] - providers_fixture[0] mock_findings_filter.return_value = [] @@ -921,7 +898,7 @@ class TestCreateComplianceRequirements: "us-west-2", "eu-west-1", ] - mock_initialize_prowler_provider.return_value = mock_prowler_provider + mock_return_prowler_provider.return_value = mock_prowler_provider mock_compliance_template.__getitem__.return_value = { "test_compliance": { @@ -990,8 +967,8 @@ class TestCreateComplianceRequirements: with ( patch("api.db_utils.rls_transaction"), patch( - "tasks.jobs.scan.initialize_prowler_provider" - ) as mock_initialize_prowler_provider, + "tasks.jobs.scan.return_prowler_provider" + ) as mock_return_prowler_provider, patch( "tasks.jobs.scan.PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE" ) as mock_compliance_template, @@ -1009,7 +986,7 @@ class TestCreateComplianceRequirements: mock_prowler_provider = MagicMock() mock_prowler_provider.get_regions.return_value = ["us-east-1", "us-west-2"] - mock_initialize_prowler_provider.return_value = mock_prowler_provider + mock_return_prowler_provider.return_value = mock_prowler_provider mock_compliance_template.__getitem__.return_value = { "test_compliance": { @@ -1077,8 +1054,8 @@ class TestCreateComplianceRequirements: with ( patch("api.db_utils.rls_transaction"), patch( - "tasks.jobs.scan.initialize_prowler_provider" - ) as mock_initialize_prowler_provider, + "tasks.jobs.scan.return_prowler_provider" + ) as mock_return_prowler_provider, patch( "tasks.jobs.scan.PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE" ) as mock_compliance_template, @@ -1090,13 +1067,12 @@ class TestCreateComplianceRequirements: ): tenant = tenants_fixture[0] scan = scans_fixture[0] - providers_fixture[0] mock_findings_filter.return_value = [] mock_prowler_provider = MagicMock() mock_prowler_provider.get_regions.return_value = ["us-east-1", "us-west-2"] - mock_initialize_prowler_provider.return_value = mock_prowler_provider + mock_return_prowler_provider.return_value = mock_prowler_provider mock_compliance_template.__getitem__.return_value = { "test_compliance": { diff --git a/api/src/backend/tasks/tests/test_tasks.py b/api/src/backend/tasks/tests/test_tasks.py index 5f7c3fd94f..def57e11b1 100644 --- a/api/src/backend/tasks/tests/test_tasks.py +++ b/api/src/backend/tasks/tests/test_tasks.py @@ -3,9 +3,10 @@ from pathlib import Path from unittest.mock import MagicMock, patch import pytest -from tasks.tasks import generate_outputs +from tasks.tasks import _perform_scan_complete_tasks, generate_outputs_task +# TODO Move this to outputs/reports jobs @pytest.mark.django_db class TestGenerateOutputs: def setup_method(self): @@ -17,7 +18,7 @@ class TestGenerateOutputs: with patch("tasks.tasks.ScanSummary.objects.filter") as mock_filter: mock_filter.return_value.exists.return_value = False - result = generate_outputs( + result = generate_outputs_task( scan_id=self.scan_id, provider_id=self.provider_id, tenant_id=self.tenant_id, @@ -99,7 +100,7 @@ class TestGenerateOutputs: mock_compress.return_value = "/tmp/zipped.zip" mock_upload.return_value = "s3://bucket/zipped.zip" - result = generate_outputs( + result = generate_outputs_task( scan_id=self.scan_id, provider_id=self.provider_id, tenant_id=self.tenant_id, @@ -150,7 +151,7 @@ class TestGenerateOutputs: True, ] - result = generate_outputs( + result = generate_outputs_task( scan_id="scan", provider_id="provider", tenant_id=self.tenant_id, @@ -208,7 +209,7 @@ class TestGenerateOutputs: {"aws": [(lambda x: True, MagicMock())]}, ), ): - generate_outputs( + generate_outputs_task( scan_id=self.scan_id, provider_id=self.provider_id, tenant_id=self.tenant_id, @@ -276,7 +277,7 @@ class TestGenerateOutputs: } }, ): - result = generate_outputs( + result = generate_outputs_task( scan_id=self.scan_id, provider_id=self.provider_id, tenant_id=self.tenant_id, @@ -346,7 +347,7 @@ class TestGenerateOutputs: ): mock_summary.return_value.exists.return_value = True - result = generate_outputs( + result = generate_outputs_task( scan_id=self.scan_id, provider_id=self.provider_id, tenant_id=self.tenant_id, @@ -407,9 +408,31 @@ class TestGenerateOutputs: ), ): with caplog.at_level("ERROR"): - generate_outputs( + generate_outputs_task( scan_id=self.scan_id, provider_id=self.provider_id, tenant_id=self.tenant_id, ) assert "Error deleting output files" in caplog.text + + +class TestScanCompleteTasks: + @patch("tasks.tasks.create_compliance_requirements_task.apply_async") + @patch("tasks.tasks.perform_scan_summary_task.si") + @patch("tasks.tasks.generate_outputs_task.si") + def test_scan_complete_tasks( + self, mock_outputs_task, mock_scan_summary_task, mock_compliance_tasks + ): + _perform_scan_complete_tasks("tenant-id", "scan-id", "provider-id") + mock_compliance_tasks.assert_called_once_with( + kwargs={"tenant_id": "tenant-id", "scan_id": "scan-id"}, + ) + mock_scan_summary_task.assert_called_once_with( + scan_id="scan-id", + tenant_id="tenant-id", + ) + mock_outputs_task.assert_called_once_with( + scan_id="scan-id", + provider_id="provider-id", + tenant_id="tenant-id", + ) diff --git a/api/tests/performance/scenarios/compliance.py b/api/tests/performance/scenarios/compliance.py new file mode 100644 index 0000000000..ae4e030a59 --- /dev/null +++ b/api/tests/performance/scenarios/compliance.py @@ -0,0 +1,128 @@ +import random +from collections import defaultdict + +import requests +from locust import events, task +from utils.helpers import APIUserBase, get_api_token, get_auth_headers + +GLOBAL = { + "token": None, + "available_scans_info": {}, +} +SUPPORTED_COMPLIANCE_IDS = { + "aws": ["ens_rd2022", "cis_2.0", "prowler_threatscore", "soc2"], + "gcp": ["ens_rd2022", "cis_2.0", "prowler_threatscore", "soc2"], + "azure": ["ens_rd2022", "cis_2.0", "prowler_threatscore", "soc2"], + "m365": ["cis_4.0", "iso27001_2022", "prowler_threatscore"], +} + + +def _get_random_scan() -> tuple: + provider_type = random.choice(list(GLOBAL["available_scans_info"].keys())) + scan_info = random.choice(GLOBAL["available_scans_info"][provider_type]) + return provider_type, scan_info + + +def _get_random_compliance_id(provider: str) -> str: + return f"{random.choice(SUPPORTED_COMPLIANCE_IDS[provider])}_{provider}" + + +def _get_compliance_available_scans_by_provider_type(host: str, token: str) -> dict: + excluded_providers = ["kubernetes"] + + response_dict = defaultdict(list) + provider_response = requests.get( + f"{host}/providers?fields[providers]=id,provider&filter[connected]=true", + headers=get_auth_headers(token), + ) + for provider in provider_response.json()["data"]: + provider_id = provider["id"] + provider_type = provider["attributes"]["provider"] + if provider_type in excluded_providers: + continue + + scan_response = requests.get( + f"{host}/scans?fields[scans]=id&filter[provider]={provider_id}&filter[state]=completed", + headers=get_auth_headers(token), + ) + scan_data = scan_response.json()["data"] + if not scan_data: + continue + scan_id = scan_data[0]["id"] + response_dict[provider_type].append(scan_id) + return response_dict + + +def _get_compliance_regions_from_scan(host: str, token: str, scan_id: str) -> list: + response = requests.get( + f"{host}/compliance-overviews/metadata?filter[scan_id]={scan_id}", + headers=get_auth_headers(token), + ) + assert response.status_code == 200, f"Failed to get scan: {response.text}" + return response.json()["data"]["attributes"]["regions"] + + +@events.test_start.add_listener +def on_test_start(environment, **kwargs): + GLOBAL["token"] = get_api_token(environment.host) + scans_by_provider = _get_compliance_available_scans_by_provider_type( + environment.host, GLOBAL["token"] + ) + scan_info = defaultdict(list) + for provider, scans in scans_by_provider.items(): + for scan in scans: + scan_info[provider].append( + { + "scan_id": scan, + "regions": _get_compliance_regions_from_scan( + environment.host, GLOBAL["token"], scan + ), + } + ) + GLOBAL["available_scans_info"] = scan_info + + +class APIUser(APIUserBase): + def on_start(self): + self.token = GLOBAL["token"] + + @task(3) + def compliance_overviews_default(self): + provider_type, scan_info = _get_random_scan() + name = f"/compliance-overviews ({provider_type})" + endpoint = f"/compliance-overviews?" f"filter[scan_id]={scan_info['scan_id']}" + self.client.get(endpoint, headers=get_auth_headers(self.token), name=name) + + @task(2) + def compliance_overviews_region(self): + provider_type, scan_info = _get_random_scan() + name = f"/compliance-overviews?filter[region] ({provider_type})" + endpoint = ( + f"/compliance-overviews" + f"?filter[scan_id]={scan_info['scan_id']}" + f"&filter[region]={random.choice(scan_info['regions'])}" + ) + self.client.get(endpoint, headers=get_auth_headers(self.token), name=name) + + @task(2) + def compliance_overviews_requirements(self): + provider_type, scan_info = _get_random_scan() + compliance_id = _get_random_compliance_id(provider_type) + name = f"/compliance-overviews/requirements ({compliance_id})" + endpoint = ( + f"/compliance-overviews/requirements" + f"?filter[scan_id]={scan_info['scan_id']}" + f"&filter[compliance_id]={compliance_id}" + ) + self.client.get(endpoint, headers=get_auth_headers(self.token), name=name) + + @task + def compliance_overviews_attributes(self): + provider_type, _ = _get_random_scan() + compliance_id = _get_random_compliance_id(provider_type) + name = f"/compliance-overviews/attributes ({compliance_id})" + endpoint = ( + f"/compliance-overviews/attributes" + f"?filter[compliance_id]={compliance_id}" + ) + self.client.get(endpoint, headers=get_auth_headers(self.token), name=name) diff --git a/dashboard/lib/cards.py b/dashboard/lib/cards.py index 0df0ff5794..ba3e73be11 100644 --- a/dashboard/lib/cards.py +++ b/dashboard/lib/cards.py @@ -4,7 +4,10 @@ from dash import html def create_provider_card( - provider: str, provider_logo: str, account_type: str, filtered_data + provider: str, + provider_logo: str, + account_type: str, + filtered_data, ) -> List[html.Div]: """ Card to display the provider's name and icon. diff --git a/dashboard/lib/dropdowns.py b/dashboard/lib/dropdowns.py index 2aaab1ab31..3e92759042 100644 --- a/dashboard/lib/dropdowns.py +++ b/dashboard/lib/dropdowns.py @@ -245,6 +245,31 @@ def create_service_dropdown(services: list) -> html.Div: ) +def create_provider_dropdown(providers: list) -> html.Div: + """ + Dropdown to select the provider. + Args: + providers (list): List of providers. + Returns: + html.Div: Dropdown to select the provider. + """ + return html.Div( + [ + html.Label( + "Provider:", className="text-prowler-stone-900 font-bold text-sm" + ), + dcc.Dropdown( + id="provider-filter", + options=[{"label": i, "value": i} for i in providers], + value=["All"], + clearable=False, + multi=True, + style={"color": "#000000"}, + ), + ], + ) + + def create_status_dropdown(status: list) -> html.Div: """ Dropdown to select the status. diff --git a/dashboard/lib/layouts.py b/dashboard/lib/layouts.py index 144036f183..e054731684 100644 --- a/dashboard/lib/layouts.py +++ b/dashboard/lib/layouts.py @@ -9,9 +9,11 @@ def create_layout_overview( download_button_xlsx: html.Button, severity_dropdown: html.Div, service_dropdown: html.Div, + provider_dropdown: html.Div, table_row_dropdown: html.Div, status_dropdown: html.Div, table_div_header: html.Div, + amount_providers: int, ) -> html.Div: """ Create the layout of the dashboard. @@ -47,9 +49,10 @@ def create_layout_overview( [ html.Div([severity_dropdown], className=""), html.Div([service_dropdown], className=""), + html.Div([provider_dropdown], className=""), html.Div([status_dropdown], className=""), ], - className="grid gap-x-4 mb-[30px] sm:grid-cols-2 lg:grid-cols-3", + className="grid gap-x-4 mb-[30px] sm:grid-cols-2 lg:grid-cols-4", ), html.Div( [ @@ -59,7 +62,7 @@ def create_layout_overview( html.Div(className="flex", id="k8s_card", n_clicks=0), html.Div(className="flex", id="m365_card", n_clicks=0), ], - className="grid gap-x-4 mb-[30px] sm:grid-cols-2 lg:grid-cols-5", + className=f"grid gap-x-4 mb-[30px] sm:grid-cols-2 lg:grid-cols-{amount_providers}", ), html.H4( "Count of Findings by severity", diff --git a/dashboard/pages/compliance.py b/dashboard/pages/compliance.py index 93c0eeb2c1..33a2b0c268 100644 --- a/dashboard/pages/compliance.py +++ b/dashboard/pages/compliance.py @@ -346,34 +346,27 @@ def display_data( if item == "nan" or item.__class__.__name__ != "str": region_filter_options.remove(item) + # Convert ASSESSMENTDATE to datetime data["ASSESSMENTDATE"] = pd.to_datetime(data["ASSESSMENTDATE"], errors="coerce") - data["ASSESSMENTDATE"] = data["ASSESSMENTDATE"].dt.strftime("%Y-%m-%d %H:%M:%S") + data["ASSESSMENTDAY"] = data["ASSESSMENTDATE"].dt.date - # Choosing the date that is the most recent - data_values = data["ASSESSMENTDATE"].unique() - data_values.sort() - data_values = data_values[::-1] - aux = [] + # Find the latest timestamp per account per day + latest_per_account_day = data.groupby(["ACCOUNTID", "ASSESSMENTDAY"])[ + "ASSESSMENTDATE" + ].transform("max") - data_values = [str(i) for i in data_values] - for value in data_values: - if value.split(" ")[0] not in [aux[i].split(" ")[0] for i in range(len(aux))]: - aux.append(value) - data_values = [str(i) for i in aux] + # Keep only rows with the latest timestamp for each account and day + data = data[data["ASSESSMENTDATE"] == latest_per_account_day] - data = data[data["ASSESSMENTDATE"].isin(data_values)] - data["ASSESSMENTDATE"] = data["ASSESSMENTDATE"].apply(lambda x: x.split(" ")[0]) + # Prepare the date filter options (unique days, as strings) + options_date = sorted(data["ASSESSMENTDAY"].astype(str).unique(), reverse=True) - options_date = data["ASSESSMENTDATE"].unique() - options_date.sort() - options_date = options_date[::-1] - - # Filter DATE + # Filter by selected date (as string) if date_filter_analytics in options_date: - data = data[data["ASSESSMENTDATE"] == date_filter_analytics] + data = data[data["ASSESSMENTDAY"].astype(str) == date_filter_analytics] else: date_filter_analytics = options_date[0] - data = data[data["ASSESSMENTDATE"] == date_filter_analytics] + data = data[data["ASSESSMENTDAY"].astype(str) == date_filter_analytics] if data.empty: fig = px.pie() diff --git a/dashboard/pages/overview.py b/dashboard/pages/overview.py index 85f07b816e..c51d33aa47 100644 --- a/dashboard/pages/overview.py +++ b/dashboard/pages/overview.py @@ -38,6 +38,7 @@ from dashboard.lib.cards import create_provider_card from dashboard.lib.dropdowns import ( create_account_dropdown, create_date_dropdown, + create_provider_dropdown, create_region_dropdown, create_service_dropdown, create_severity_dropdown, @@ -298,6 +299,13 @@ else: service_dropdown = create_service_dropdown(services) + # Provider Dropdown + providers = ["All"] + list(data["PROVIDER"].unique()) + providers = [ + x for x in providers if str(x) != "nan" and x.__class__.__name__ == "str" + ] + provider_dropdown = create_provider_dropdown(providers) + # Create the download button download_button_csv = html.Button( "Download this table as CSV", @@ -479,9 +487,11 @@ else: download_button_xlsx, severity_dropdown, service_dropdown, + provider_dropdown, table_row_dropdown, status_dropdown, table_div_header, + len(data["PROVIDER"].unique()), ) @@ -508,6 +518,8 @@ else: Output("severity-filter", "value"), Output("severity-filter", "options"), Output("service-filter", "value"), + Output("provider-filter", "value"), + Output("provider-filter", "options"), Output("service-filter", "options"), Output("table-rows", "value"), Output("table-rows", "options"), @@ -526,6 +538,7 @@ else: Input("download_link_xlsx", "n_clicks"), Input("severity-filter", "value"), Input("service-filter", "value"), + Input("provider-filter", "value"), Input("table-rows", "value"), Input("status-filter", "value"), Input("search-input", "value"), @@ -549,6 +562,7 @@ def filter_data( n_clicks_xlsx, severity_values, service_values, + provider_values, table_row_values, status_values, search_value, @@ -874,6 +888,25 @@ def filter_data( filtered_data["SERVICE_NAME"].isin(updated_service_values) ] + provider_filter_options = ["All"] + list(filtered_data["PROVIDER"].unique()) + + # Filter Provider + if provider_values == ["All"]: + updated_provider_values = filtered_data["PROVIDER"].unique() + elif "All" in provider_values and len(provider_values) > 1: + # Remove 'All' from the list + provider_values.remove("All") + updated_provider_values = provider_values + elif len(provider_values) == 0: + updated_provider_values = filtered_data["PROVIDER"].unique() + provider_values = ["All"] + else: + updated_provider_values = provider_values + + filtered_data = filtered_data[ + filtered_data["PROVIDER"].isin(updated_provider_values) + ] + # Filter Status if status_values == ["All"]: updated_status_values = filtered_data["STATUS"].unique() @@ -1094,25 +1127,17 @@ def filter_data( table_row_options = [] - # Take the values from the table_row_values + # Calculate table row options as percentages + percentages = [0.05, 0.10, 0.25, 0.50, 0.75, 1.0] + total_rows = len(filtered_data) + for pct in percentages: + value = max(1, int(total_rows * pct)) + label = f"{int(pct * 100)}%" + table_row_options.append({"label": label, "value": value}) + + # Default to 25% if not set if table_row_values is None or table_row_values == -1: - if len(filtered_data) < 25: - table_row_values = len(filtered_data) - else: - table_row_values = 25 - - if len(filtered_data) < 25: - table_row_values = len(filtered_data) - - if len(filtered_data) >= 25: - table_row_options.append(25) - if len(filtered_data) >= 50: - table_row_options.append(50) - if len(filtered_data) >= 75: - table_row_options.append(75) - if len(filtered_data) >= 100: - table_row_options.append(100) - table_row_options.append(len(filtered_data)) + table_row_values = table_row_options[0]["value"] # For the values that are nan or none, replace them with "" filtered_data = filtered_data.replace({np.nan: ""}) @@ -1347,21 +1372,36 @@ def filter_data( ] # Create Provider Cards - aws_card = create_provider_card( - "aws", aws_provider_logo, "Accounts", full_filtered_data - ) - azure_card = create_provider_card( - "azure", azure_provider_logo, "Subscriptions", full_filtered_data - ) - gcp_card = create_provider_card( - "gcp", gcp_provider_logo, "Projects", full_filtered_data - ) - k8s_card = create_provider_card( - "kubernetes", ks8_provider_logo, "Clusters", full_filtered_data - ) - m365_card = create_provider_card( - "m365", m365_provider_logo, "Accounts", full_filtered_data - ) + if "aws" in list(data["PROVIDER"].unique()): + aws_card = create_provider_card( + "aws", aws_provider_logo, "Accounts", full_filtered_data + ) + else: + aws_card = None + if "azure" in list(data["PROVIDER"].unique()): + azure_card = create_provider_card( + "azure", azure_provider_logo, "Subscriptions", full_filtered_data + ) + else: + azure_card = None + if "gcp" in list(data["PROVIDER"].unique()): + gcp_card = create_provider_card( + "gcp", gcp_provider_logo, "Projects", full_filtered_data + ) + else: + gcp_card = None + if "kubernetes" in list(data["PROVIDER"].unique()): + k8s_card = create_provider_card( + "kubernetes", ks8_provider_logo, "Clusters", full_filtered_data + ) + else: + k8s_card = None + if "m365" in list(data["PROVIDER"].unique()): + m365_card = create_provider_card( + "m365", m365_provider_logo, "Accounts", full_filtered_data + ) + else: + m365_card = None # Subscribe to Prowler Cloud card subscribe_card = [ @@ -1445,6 +1485,8 @@ def filter_data( severity_values, severity_filter_options, service_values, + provider_values, + provider_filter_options, service_filter_options, table_row_values, table_row_options, diff --git a/docs/developer-guide/checks.md b/docs/developer-guide/checks.md index 400da7134d..e5dba4ba9c 100644 --- a/docs/developer-guide/checks.md +++ b/docs/developer-guide/checks.md @@ -212,34 +212,9 @@ Each check **must** populate the report with an unique identifier for the audite - Resource Name — `report.resource_name`. - The name of the Github resource. In the case of a repository, this is just the repository name. For full repository names use the resource `full_name`. -### Using the Audit Configuration +### Configurable Checks in Prowler -Prowler has a [configuration file](../tutorials/configuration_file.md) which is used to pass certain configuration values to the checks. For example: - -```python title="ec2_securitygroup_with_many_ingress_egress_rules.py" -class ec2_securitygroup_with_many_ingress_egress_rules(Check): - def execute(self): - findings = [] - - max_security_group_rules = ec2_client.audit_config.get( - "max_security_group_rules", 50 - ) - for security_group_arn, security_group in ec2_client.security_groups.items(): -``` - -We use the `audit_config` object to retrieve the value of `max_security_group_rules`, which is the default value of 50 if the configuration value is not present. - -The configuration file is located at [`prowler/config/config.yaml`](https://github.com/prowler-cloud/prowler/blob/master/prowler/config/config.yaml) and is used to pass certain configuration values to the checks. For example: - -```yaml title="config.yaml" - aws: - max_security_group_rules: 50 -``` - -This `audit_config` object is a Python dictionary that stores values read from the configuration file. It can be accessed by the check using the `audit_config` attribute of the service client. - -???+ note - Always use the `dictionary.get(value, default)` syntax to ensure a default value is set when the configuration value is not present. +See [Configurable Checks](./configurable-checks.md) for detailed information on making checks configurable using the `audit_config` object and configuration file. ## Metadata Structure for Prowler Checks diff --git a/docs/developer-guide/configurable-checks.md b/docs/developer-guide/configurable-checks.md new file mode 100644 index 0000000000..aad072ddd5 --- /dev/null +++ b/docs/developer-guide/configurable-checks.md @@ -0,0 +1,46 @@ +# Configurable Checks in Prowler + +Prowler empowers users to extend and adapt cloud security coverage by making checks configurable through the use of the `audit_config` object. This approach enables customization of checks to meet specific requirements through a configuration file. + +## Understanding the `audit_config` Object + +The `audit_config` object is a dictionary attached to each provider's service client (for example, `_client.audit_config`). This object loads configuration values from the main configuration file (`prowler/config/config.yaml`). Use `audit_config` to make checks flexible and user-configurable. + +## Using `audit_config` to Configure Checks + +Retrieve configuration values in a check by using the `.get()` method on the `audit_config` object. For example, to get the minimum number of Availability Zones for Lambda from the configuration file, use the following code. If the value is not set in the configuration, the check defaults to 2: + +```python +LAMBDA_MIN_AZS = awslambda_client.audit_config.get("lambda_min_azs", 2) +``` + +Always provide a default value in `.get()` to ensure the check works even if the configuration is missing the variable. + +### Example: Security Group Rule Limit + +```python title="ec2_securitygroup_with_many_ingress_egress_rules.py" +class ec2_securitygroup_with_many_ingress_egress_rules(Check): + def execute(self): + findings = [] + max_security_group_rules = ec2_client.audit_config.get( + "max_security_group_rules", 50 + ) + for security_group_arn, security_group in ec2_client.security_groups.items(): + # ... check logic ... +``` + +## Required File Updates for Configurable Variables + +When adding a new configurable check to Prowler, update the following files: + +- **Configuration File:** Add the new variable under the relevant provider or service section in `prowler/config/config.yaml`. + ```yaml + # aws.awslambda_function_vpc_multi_az + lambda_min_azs: 2 + ``` +- **Test Fixtures:** If tests depend on this configuration, add the variable to `tests/config/fixtures/config.yaml`. +- **Documentation:** Document the new variable in the list of configurable checks in `docs/tutorials/configuration_file.md`. + +For a complete list of checks that already support configuration, see the [Configuration File Tutorial](../tutorials/configuration_file.md). + +This approach ensures that checks are easily configurable, making Prowler highly adaptable to different environments and requirements. diff --git a/docs/developer-guide/introduction.md b/docs/developer-guide/introduction.md index dc18fd4bbc..abc83fb682 100644 --- a/docs/developer-guide/introduction.md +++ b/docs/developer-guide/introduction.md @@ -164,3 +164,13 @@ If you enjoy swag, we’d love to thank you for your contribution with laptop st To request swag: Share your pull request details in our [Slack workspace](https://goto.prowler.com/slack). You can also reach out to Toni de la Fuente on [Twitter](https://twitter.com/ToniBlyx)—his DMs are open! + +# Testing a Pull Request from a Specific Branch + +To test Prowler from a specific branch (for example, to try out changes from a pull request before it is merged), you can use `pipx` to install directly from GitHub: + +```sh +pipx install "git+https://github.com/prowler-cloud/prowler.git@branch-name" +``` + +Replace `branch-name` with the name of the branch you want to test. This will install Prowler in an isolated environment, allowing you to try out the changes safely. \ No newline at end of file diff --git a/docs/developer-guide/lighthouse.md b/docs/developer-guide/lighthouse.md new file mode 100644 index 0000000000..e0a8d939dc --- /dev/null +++ b/docs/developer-guide/lighthouse.md @@ -0,0 +1,134 @@ +# Extending Prowler Lighthouse + +This guide helps developers customize and extend Prowler Lighthouse by adding or modifying AI agents. + +## Understanding AI Agents + +AI agents combine Large Language Models (LLMs) with specialized tools that provide environmental context. These tools can include API calls, system command execution, or any function-wrapped capability. + +### Types of AI Agents + +AI agents fall into two main categories: + +- **Autonomous Agents**: Freely chooses from available tools to complete tasks, adapting their approach based on context. They decide which tools to use and when. +- **Workflow Agents**: Follows structured paths with predefined logic. They execute specific tool sequences and can include conditional logic. + +Prowler Lighthouse is an autonomous agent - selecting the right tool(s) based on the users query. + +???+ note + To learn more about AI agents, read [Anthropic's blog post on building effective agents](https://www.anthropic.com/engineering/building-effective-agents). + +### LLM Dependency + +The autonomous nature of agents depends on the underlying LLM. Autonomous agents using identical system prompts and tools but powered by different LLM providers might approach user queries differently. Agent with one LLM might solve a problem efficiently, while with another it might take a different route or fail entirely. + +After evaluating multiple LLM providers (OpenAI, Gemini, Claude, LLama) based on tool calling features and response accuracy, we recommend using the `gpt-4o` model. + +## Prowler Lighthouse Architecture + +Prowler Lighthouse uses a multi-agent architecture orchestrated by the [Langgraph-Supervisor](https://www.npmjs.com/package/@langchain/langgraph-supervisor) library. + +### Architecture Components + +Prowler Lighthouse architecture + +Prowler Lighthouse integrates with the NextJS application: + +- The [Langgraph-Supervisor](https://www.npmjs.com/package/@langchain/langgraph-supervisor) library integrates directly with NextJS +- The system uses the authenticated user session to interact with the Prowler API server +- Agents only access data the current user is authorized to view +- Session management operates automatically, ensuring Role-Based Access Control (RBAC) is maintained + +## Available Prowler AI Agents + +The following specialized AI agents are available in Prowler: + +### Agent Overview + +- **provider_agent**: Fetches information about cloud providers connected to Prowler +- **user_info_agent**: Retrieves information about Prowler users +- **scans_agent**: Fetches information about Prowler scans +- **compliance_agent**: Retrieves compliance overviews across scans +- **findings_agent**: Fetches information about individual findings across scans +- **overview_agent**: Retrieves overview information (providers, findings by status and severity, etc.) + +## How to Add New Capabilities + +### Updating the Supervisor Prompt + +The supervisor agent controls system behavior, tone, and capabilities. You can find the supervisor prompt at: [https://github.com/prowler-cloud/prowler/blob/master/ui/lib/lighthouse/prompts.ts](https://github.com/prowler-cloud/prowler/blob/master/ui/lib/lighthouse/prompts.ts) + +#### Supervisor Prompt Modifications + +Modifying the supervisor prompt allows you to: + +- Change personality or response style +- Add new high-level capabilities +- Modify task delegation to specialized agents +- Set up guardrails (query types to answer or decline) + +???+ note + The supervisor agent should not have its own tools. This design keeps the system modular and maintainable. + +### How to Create New Specialized Agents + +The supervisor agent and all specialized agents are defined in the `route.ts` file. The supervisor agent uses [langgraph-supervisor](https://www.npmjs.com/package/@langchain/langgraph-supervisor), while other agents use the prebuilt [create-react-agent](https://langchain-ai.github.io/langgraphjs/how-tos/create-react-agent/). + +To add new capabilities or all Lighthouse to interact with other APIs, create additional specialized agents: + +1. First determine what the new agent would do. Create a detailed prompt defining the agent's purpose and capabilities. You can see an example from [here](https://github.com/prowler-cloud/prowler/blob/master/ui/lib/lighthouse/prompts.ts#L359-L385). +???+ note + Ensure that the new agent's capabilities don't collide with existing agents. For example, if there's already a *findings_agent* that talks to findings APIs don't create a new agent to do the same. + +2. Create necessary tools for the agents to access specific data or perform actions. A tool is a specialized function that extends the capabilities of LLM by allowing it to access external data or APIs. A tool is triggered by LLM based on the description of the tool and the user's query. +For example, the description of `getScanTool` is "Fetches detailed information about a specific scan by its ID." If the description doesn't convey what the tool is capable of doing, LLM will not invoke the function. If the description of `getScanTool` was set to something random or not set at all, LLM will not answer queries like "Give me the critical issues from the scan ID xxxxxxxxxxxxxxx" +???+ note + Ensure that one tool is added to one agent only. Adding tools is optional. There can be agents with no tools at all. + +3. Use the `createReactAgent` function to define a new agent. For example, the rolesAgent name is "roles_agent" and has access to call tools "*getRolesTool*" and "*getRoleTool*" +```js +const rolesAgent = createReactAgent({ + llm: llm, + tools: [getRolesTool, getRoleTool], + name: "roles_agent", + prompt: rolesAgentPrompt, +}); +``` + +4. Create a detailed prompt defining the agent's purpose and capabilities. + +5. Add the new agent to the available agents list: +```js +const agents = [ + userInfoAgent, + providerAgent, + overviewAgent, + scansAgent, + complianceAgent, + findingsAgent, + rolesAgent, // New agent added here +]; +// Create supervisor workflow +const workflow = createSupervisor({ + agents: agents, + llm: supervisorllm, + prompt: supervisorPrompt, + outputMode: "last_message", +}); +``` + +6. Update the supervisor's system prompt to summarize the new agent's capabilities. + +### Best Practices for Agent Development + +When developing new agents or capabilities: + +- **Clear Responsibility Boundaries**: Each agent should have a defined purpose with minimal overlap. No two agents should access the same tools or different tools accessing the same Prowler APIs. +- **Minimal Data Access**: Agents should only request the data they need, keeping requests specific to minimize context window usage, cost, and response time. +- **Thorough Prompting:** Ensure agent prompts include clear instructions about: + - The agent's purpose and limitations + - How to use its tools + - How to format responses for the supervisor + - Error handling procedures (Optional) +- **Security Considerations:** Agents should never modify data or access sensitive information like secrets or credentials. +- **Testing:** Thoroughly test new agents with various queries before deploying to production. diff --git a/docs/getting-started/requirements.md b/docs/getting-started/requirements.md index 37b8ab5663..1405477533 100644 --- a/docs/getting-started/requirements.md +++ b/docs/getting-started/requirements.md @@ -139,7 +139,7 @@ Prowler for M365 currently supports the following authentication types: ???+ warning For Prowler App only the Service Principal with User Credentials authentication method is supported. -### Service Principal authentication +### Service Principal authentication (recommended) Authentication flag: `--sp-env-auth` @@ -154,9 +154,11 @@ export AZURE_TENANT_ID="XXXXXXXXX" If you try to execute Prowler with the `--sp-env-auth` flag and those variables are empty or not exported, the execution is going to fail. Follow the instructions in the [Create Prowler Service Principal](../tutorials/microsoft365/getting-started-m365.md#create-the-service-principal-app) section to create a service principal. -With this credentials you will only be able to run the checks that work through MS Graph, this means that you won't run all the provider. If you want to scan all the checks from M365 you will need to use the recommended authentication method. +If you don't add the external API permissions described in the mentioned section above you will only be able to run the checks that work through MS Graph. This means that you won't run all the provider. -### Service Principal and User Credentials authentication (recommended) +If you want to scan all the checks from M365 you will need to add the required permissions to the service principal application. Refer to the [Needed permissions](/docs/tutorials/microsoft365/getting-started-m365.md#needed-permissions) section for more information. + +### Service Principal and User Credentials authentication Authentication flag: `--env-auth` @@ -170,9 +172,10 @@ export M365_USER="your_email@example.com" export M365_PASSWORD="examplepassword" ``` -These two new environment variables are **required** to execute the PowerShell modules needed to retrieve information from M365 services. Prowler uses Service Principal authentication to access Microsoft Graph and user credentials to authenticate to Microsoft PowerShell modules. +These two new environment variables are **required** in this authentication method to execute the PowerShell modules needed to retrieve information from M365 services. Prowler uses Service Principal authentication to access Microsoft Graph and user credentials to authenticate to Microsoft PowerShell modules. - `M365_USER` should be your Microsoft account email using the **assigned domain in the tenant**. This means it must look like `example@YourCompany.onmicrosoft.com` or `example@YourCompany.com`, but it must be the exact domain assigned to that user in the tenant. + ???+ warning If the user is newly created, you need to sign in with that account first, as Microsoft will prompt you to change the password. If you don’t complete this step, user authentication will fail because Microsoft marks the initial password as expired. @@ -205,27 +208,56 @@ Since this is a delegated permission authentication method, necessary permission ### Needed permissions -Prowler for M365 requires two types of permission scopes to be set (if you want to run the full provider including PowerShell checks). Both must be configured using Microsoft Entra ID: +Prowler for M365 requires different permission scopes depending on the authentication method you choose. The permissions must be configured using Microsoft Entra ID: -- **Service Principal Application Permissions**: These are set at the **application** level and are used to retrieve data from the identity being assessed: - - `AuditLog.Read.All`: Required for Entra service. - - `Directory.Read.All`: Required for all services. - - `Policy.Read.All`: Required for all services. - - `SharePointTenantSettings.Read.All`: Required for SharePoint service. - - `User.Read` (IMPORTANT: this must be set as **delegated**): Required for the sign-in. +#### For Service Principal Authentication (`--sp-env-auth`) - Recommended - ???+ note - You can replace `Directory.Read.All` with `Domain.Read.All` is a more restrictive permission but you won't be able to run the Entra checks related with DirectoryRoles and GetUsers. +When using service principal authentication, you need to add the following **Application Permissions** configured to: - > If you do this you will need to add also the `Organization.Read.All` permission to the service principal application in order to authenticate. +**Microsoft Graph API Permissions:** +- `AuditLog.Read.All`: Required for Entra service. +- `Directory.Read.All`: Required for all services. +- `Policy.Read.All`: Required for all services. +- `SharePointTenantSettings.Read.All`: Required for SharePoint service. +- `User.Read` (IMPORTANT: this must be set as **delegated**): Required for the sign-in. +**External API Permissions:** +- `Exchange.ManageAsApp` from external API `Office 365 Exchange Online`: Required for Exchange PowerShell module app authentication. You also need to assign the `Exchange Administrator` role to the app. +- `application_access` from external API `Skype and Teams Tenant Admin API`: Required for Teams PowerShell module app authentication. +???+ note + You can replace `Directory.Read.All` with `Domain.Read.All` is a more restrictive permission but you won't be able to run the Entra checks related with DirectoryRoles and GetUsers. -- **Powershell Modules Permissions**: These are set at the `M365_USER` level, so the user used to run Prowler must have one of the following roles: - - `Global Reader` (recommended): this allows you to read all roles needed. - - `Exchange Administrator` and `Teams Administrator`: user needs both roles but with this [roles](https://learn.microsoft.com/en-us/exchange/permissions-exo/permissions-exo#microsoft-365-permissions-in-exchange-online) you can access to the same information as a Global Reader (since only read access is needed, Global Reader is recommended). + > If you do this you will need to add also the `Organization.Read.All` permission to the service principal application in order to authenticate. -In order to know how to assign those permissions and roles follow the instructions in the Microsoft Entra ID [permissions](../tutorials/microsoft365/getting-started-m365.md#grant-required-api-permissions) and [roles](../tutorials/microsoft365/getting-started-m365.md#assign-required-roles-to-your-user) section. +???+ warning + With service principal only authentication, you can only run checks that work through MS Graph API. Some checks that require PowerShell modules will not be executed. + +#### For Service Principal + User Credentials Authentication (`--env-auth`) + +When using service principal with user credentials authentication, you need **both** sets of permissions: + +**1. Service Principal Application Permissions**: +- You **will need** all the Microsoft Graph API permissions listed above. +- You **won't need** the External API permissions listed above. + +**2. User-Level Permissions**: These are set at the `M365_USER` level, so the user used to run Prowler must have one of the following roles: +- `Global Reader` (recommended): this allows you to read all roles needed. +- `Exchange Administrator` and `Teams Administrator`: user needs both roles but with this [roles](https://learn.microsoft.com/en-us/exchange/permissions-exo/permissions-exo#microsoft-365-permissions-in-exchange-online) you can access to the same information as a Global Reader (since only read access is needed, Global Reader is recommended). + +???+ note + This is the **recommended authentication method** because it allows you to run the full M365 provider including PowerShell checks, providing complete coverage of all available security checks. + +#### For Browser Authentication (`--browser-auth`) + +When using browser authentication, permissions are delegated to the user, so the user must have the appropriate permissions rather than the application. + +???+ warning + With browser authentication, you will only be able to run checks that work through MS Graph API. PowerShell module checks will not be executed. + +--- + +**To assign these permissions and roles**, follow the instructions in the Microsoft Entra ID [permissions](../tutorials/microsoft365/getting-started-m365.md#grant-required-api-permissions) and [roles](../tutorials/microsoft365/getting-started-m365.md#assign-required-roles-to-your-user) section. ### Supported PowerShell versions @@ -439,6 +471,7 @@ The required modules are: - [ExchangeOnlineManagement](https://www.powershellgallery.com/packages/ExchangeOnlineManagement/3.6.0): Minimum version 3.6.0. Required for several checks across Exchange, Defender, and Purview. - [MicrosoftTeams](https://www.powershellgallery.com/packages/MicrosoftTeams/6.6.0): Minimum version 6.6.0. Required for all Teams checks. +- [MSAL.PS](https://www.powershellgallery.com/packages/MSAL.PS/4.32.0): Required for Exchange module via application authentication. ## GitHub ### Authentication @@ -455,3 +488,54 @@ The provided credentials must have the appropriate permissions to perform all th ???+ note GitHub App Credentials support less checks than other authentication methods. + +## Infrastructure as Code (IaC) + +Prowler's Infrastructure as Code (IaC) provider enables you to scan local infrastructure code for security and compliance issues using [Checkov](https://www.checkov.io/). This provider supports a wide range of IaC frameworks and requires no cloud authentication. + +### Authentication + +The IaC provider does not require any authentication or credentials since it scans local files directly. This makes it ideal for CI/CD pipelines and local development environments. + +### Supported Frameworks + +The IaC provider leverages Checkov to support multiple frameworks, including: + +- Terraform +- CloudFormation +- Kubernetes +- ARM (Azure Resource Manager) +- Serverless +- Dockerfile +- YAML/JSON (generic IaC) +- Bicep +- Helm +- GitHub Actions, GitLab CI, Bitbucket Pipelines, Azure Pipelines, CircleCI, Argo Workflows +- Ansible +- Kustomize +- OpenAPI +- SAST, SCA (Software Composition Analysis) + +### Usage + +To run Prowler with the IaC provider, use the `iac` flag. You can specify the directory to scan, frameworks to include, and paths to exclude. + +#### Basic Example + +```console +prowler iac --scan-path ./my-iac-directory +``` + +#### Specify Frameworks + +Scan only Terraform and Kubernetes files: + +```console +prowler iac --scan-path ./my-iac-directory --frameworks terraform kubernetes +``` + +#### Exclude Paths + +```console +prowler iac --scan-path ./my-iac-directory --exclude-path ./my-iac-directory/test,./my-iac-directory/examples +``` diff --git a/docs/img/m365-credentials.png b/docs/img/m365-credentials.png index 87191751a8..ab912f9d8e 100644 Binary files a/docs/img/m365-credentials.png and b/docs/img/m365-credentials.png differ diff --git a/docs/index.md b/docs/index.md index ff0f3237a2..fb8e1d734e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -572,12 +572,12 @@ With M365 you need to specify which auth method is going to be used: ```console +# To use service principal authentication for MSGraph and PowerShell modules +prowler m365 --sp-env-auth + # To use both service principal (for MSGraph) and user credentials (for PowerShell modules) prowler m365 --env-auth -# To use service principal authentication -prowler m365 --sp-env-auth - # To use az cli authentication prowler m365 --az-cli-auth @@ -612,5 +612,27 @@ prowler github --github-app-id app_id --github-app-key app_key 2. `OAUTH_APP_TOKEN` 3. `GITHUB_APP_ID` and `GITHUB_APP_KEY` +#### Infrastructure as Code (IaC) + +Prowler's Infrastructure as Code (IaC) provider enables you to scan local infrastructure code for security and compliance issues using [Checkov](https://www.checkov.io/). This provider supports a wide range of IaC frameworks, allowing you to assess your code before deployment. + +```console +# Scan a directory for IaC files +prowler iac --scan-path ./my-iac-directory + +# Specify frameworks to scan (default: all) +prowler iac --scan-path ./my-iac-directory --frameworks terraform kubernetes + +# Exclude specific paths +prowler iac --scan-path ./my-iac-directory --exclude-path ./my-iac-directory/test,./my-iac-directory/examples +``` + +???+ note + - The IaC provider does not require cloud authentication + - It is ideal for CI/CD pipelines and local development environments + - For more details on supported frameworks and rules, see the [Checkov documentation](https://www.checkov.io/1.Welcome/Quick%20Start.html) + +See more details about IaC scanning in the [IaC Tutorial](tutorials/iac/getting-started-iac.md) section. + ## Prowler v2 Documentation For **Prowler v2 Documentation**, please check it out [here](https://github.com/prowler-cloud/prowler/blob/8818f47333a0c1c1a457453c87af0ea5b89a385f/README.md). diff --git a/docs/tutorials/github/authentication.md b/docs/tutorials/github/authentication.md index 29f7795aba..5b589921ec 100644 --- a/docs/tutorials/github/authentication.md +++ b/docs/tutorials/github/authentication.md @@ -37,7 +37,7 @@ prowler github --github-app-id app_id --github-app-key app_key If no login method is explicitly provided, Prowler will automatically attempt to authenticate using environment variables in the following order of precedence: 1. `GITHUB_PERSONAL_ACCESS_TOKEN` -2. `OAUTH_APP_TOKEN` +2. `GITHUB_OAUTH_APP_TOKEN` 3. `GITHUB_APP_ID` and `GITHUB_APP_KEY` ???+ note diff --git a/docs/tutorials/iac/getting-started-iac.md b/docs/tutorials/iac/getting-started-iac.md new file mode 100644 index 0000000000..63e0e9f78c --- /dev/null +++ b/docs/tutorials/iac/getting-started-iac.md @@ -0,0 +1,67 @@ +# Getting Started with the IaC Provider + +Prowler's Infrastructure as Code (IaC) provider enables you to scan local infrastructure code for security and compliance issues using [Checkov](https://www.checkov.io/). This provider supports a wide range of IaC frameworks, allowing you to assess your code before deployment. + +## Supported Frameworks + +The IaC provider leverages Checkov to support multiple frameworks, including: + +- Terraform +- CloudFormation +- Kubernetes +- ARM (Azure Resource Manager) +- Serverless +- Dockerfile +- YAML/JSON (generic IaC) +- Bicep +- Helm +- GitHub Actions, GitLab CI, Bitbucket Pipelines, Azure Pipelines, CircleCI, Argo Workflows +- Ansible +- Kustomize +- OpenAPI +- SAST, SCA (Software Composition Analysis) + +## How It Works + +- The IaC provider scans your local directory (or a specified path) for supported IaC files. +- No cloud credentials or authentication are required. +- Mutelist logic is handled by Checkov, not Prowler. +- Results are output in the same formats as other Prowler providers (CSV, JSON, HTML, etc.). + +## Usage + +To run Prowler with the IaC provider, use the `iac` argument. You can specify the directory to scan, frameworks to include, and paths to exclude. + +### Basic Example + +```sh +prowler iac --scan-path ./my-iac-directory +``` + +### Specify Frameworks + +Scan only Terraform and Kubernetes files: + +```sh +prowler iac --scan-path ./my-iac-directory --frameworks terraform kubernetes +``` + +### Exclude Paths + +```sh +prowler iac --scan-path ./my-iac-directory --exclude-path ./my-iac-directory/test,./my-iac-directory/examples +``` + +## Output + +You can use the standard Prowler output options, for example: + +```sh +prowler iac --scan-path ./iac --output-formats csv json html +``` + +## Notes + +- The IaC provider does not require cloud authentication. +- It is ideal for CI/CD pipelines and local development environments. +- For more details on supported frameworks and rules, see the [Checkov documentation](https://www.checkov.io/1.Welcome/Quick%20Start.html). diff --git a/docs/tutorials/img/gcp-auth-methods.png b/docs/tutorials/img/gcp-auth-methods.png new file mode 100644 index 0000000000..dc8681396e Binary files /dev/null and b/docs/tutorials/img/gcp-auth-methods.png differ diff --git a/docs/tutorials/img/gcp-service-account-creds.png b/docs/tutorials/img/gcp-service-account-creds.png new file mode 100644 index 0000000000..af09776ab1 Binary files /dev/null and b/docs/tutorials/img/gcp-service-account-creds.png differ diff --git a/docs/tutorials/img/lighthouse-architecture.png b/docs/tutorials/img/lighthouse-architecture.png new file mode 100644 index 0000000000..63202ce7c7 Binary files /dev/null and b/docs/tutorials/img/lighthouse-architecture.png differ diff --git a/docs/tutorials/img/lighthouse-config.png b/docs/tutorials/img/lighthouse-config.png new file mode 100644 index 0000000000..8bae46f51b Binary files /dev/null and b/docs/tutorials/img/lighthouse-config.png differ diff --git a/docs/tutorials/img/lighthouse-feature1.png b/docs/tutorials/img/lighthouse-feature1.png new file mode 100644 index 0000000000..4568f5752a Binary files /dev/null and b/docs/tutorials/img/lighthouse-feature1.png differ diff --git a/docs/tutorials/img/lighthouse-feature2.png b/docs/tutorials/img/lighthouse-feature2.png new file mode 100644 index 0000000000..01e72bbaf4 Binary files /dev/null and b/docs/tutorials/img/lighthouse-feature2.png differ diff --git a/docs/tutorials/img/lighthouse-feature3.png b/docs/tutorials/img/lighthouse-feature3.png new file mode 100644 index 0000000000..a40177cf18 Binary files /dev/null and b/docs/tutorials/img/lighthouse-feature3.png differ diff --git a/docs/tutorials/img/lighthouse-intro.png b/docs/tutorials/img/lighthouse-intro.png new file mode 100644 index 0000000000..38d3f0819c Binary files /dev/null and b/docs/tutorials/img/lighthouse-intro.png differ diff --git a/docs/tutorials/microsoft365/authentication.md b/docs/tutorials/microsoft365/authentication.md index f8bd9d13d4..c63b8d734c 100644 --- a/docs/tutorials/microsoft365/authentication.md +++ b/docs/tutorials/microsoft365/authentication.md @@ -12,7 +12,7 @@ This allows Prowler to authenticate against Microsoft 365 using the following me To launch the tool first you need to specify which method is used through the following flags: ```console -# To use service principal (app) authentication and Microsoft user credentials (to use PowerShell) +# To use service principal (app) authentication and Microsoft user credentials prowler m365 --env-auth # To use service principal authentication @@ -25,4 +25,4 @@ prowler m365 --az-cli-auth prowler m365 --browser-auth --tenant-id "XXXXXXXX" ``` -To use Prowler you need to set up also the permissions required to access your resources in your Microsoft 365 account, to more details refer to [Requirements](../../getting-started/requirements.md#microsoft-365) +To use Prowler you need to set up also the permissions required to access your resources in your Microsoft 365 account, to more details refer to [Requirements](../../getting-started/requirements.md#needed-permissions-2) diff --git a/docs/tutorials/microsoft365/getting-started-m365.md b/docs/tutorials/microsoft365/getting-started-m365.md index 46184d84e2..935e45ad5c 100644 --- a/docs/tutorials/microsoft365/getting-started-m365.md +++ b/docs/tutorials/microsoft365/getting-started-m365.md @@ -30,7 +30,7 @@ Go to the Entra ID portal, then you can search for `Domain` or go to Identity > ![Custom Domain Names](./img/custom-domain-names.png) -Once you are there just select the domain you want to use. +Once you are there just select the domain you want to use as unique identifier for your M365 account in Prowler Cloud/App. --- @@ -92,7 +92,7 @@ With this done you will have all the needed keys, summarized in the following ta --- -### Grant required API permissions +### Grant required Graph API permissions Assign the following Microsoft Graph permissions: @@ -100,7 +100,7 @@ Assign the following Microsoft Graph permissions: - `Directory.Read.All`: Required for all services. - `Policy.Read.All`: Required for all services. - `SharePointTenantSettings.Read.All`: Required for SharePoint service. -- `User.Read` (IMPORTANT: this is set as **delegated**): Required for the sign-in. +- `User.Read` (IMPORTANT: this is set as **delegated**): Required for the sign-in only if using user authentication. ???+ note You can replace `Directory.Read.All` with `Domain.Read.All` is a more restrictive permission but you won't be able to run the Entra checks related with DirectoryRoles and GetUsers. @@ -128,18 +128,83 @@ Follow these steps to assign the permissions: ![Application Permissions](./img/app-permissions.png) +--- -4. Click `+ Add a permission` > `Microsoft Graph` > `Delegated permissions` - ![Add API Permission](./img/add-delegated-api-permission.png) +### Grant PowerShell modules permissions -5. Search and select: +The permissions you need to grant depends on whether you are using user credentials or service principal to authenticate to the M365 modules. + +???+ warning "Warning" + Make sure you add the correct set of permissions for the authentication method you are using. + + +#### If using application(service principal) authentication (Recommended) + +To grant the permissions for the PowerShell modules via application authentication, you need to add the necessary APIs to your app registration. + +???+ warning "Warning" + You need to have a license that allows you to use the APIs. + +1. Add Exchange API: + + - Search and select`Office 365 Exchange Online` API in **APIs my organization uses**. + + ![Office 365 Exchange Online API](./img/search-exchange-api.png) + + - Select `Exchange.ManageAsApp` permission and click on `Add permissions`. + + ![Exchange.ManageAsApp Permission](./img/exchange-permission.png) + + You also need to assign the `Exchange Administrator` role to the app. For that go to `Roles and administrators` and in the `Administrative roles` section click `here` to go to the directory level assignment: + + ![Roles and administrators](./img/here.png) + + Once in the directory level assignment, search for `Exchange Administrator` and click on it to open the assginments page of that role. + + ![Exchange Administrator Role](./img/exchange-administrator-role.png) + + Click on `Add assignments`, search for your app and click on `Assign`. + + You have to select it as `Active` and click on `Assign` to assign the role to the app. + + ![Assign Exchange Administrator Role](./img/assign-exchange-administrator-role.png) + +2. Add Teams API: + + - Search and select `Skype and Teams Tenant Admin API` API in **APIs my organization uses**. + + ![Skype and Teams Tenant Admin API](./img/search-skype-teams-tenant-admin-api.png) + + - Select `application_access` permission and click on `Add permissions`. + + ![application_access Permission](./img/teams-permission.png) + +3. Click on `Grant admin consent for ` to grant admin consent. + + ![Grant Admin Consent](./img/grant-external-api-permissions.png) + + The final result of permission assignment should be this: + + ![Final Permission Assignment](./img/final-permissions.png) + +--- + +#### If using user authentication + +This method is not recommended because it requires a user with MFA enabled and Microsoft will not allow MFA capable users to authenticate programmatically after 1st September 2025. See [Microsoft documentation](https://learn.microsoft.com/en-us/entra/identity/authentication/concept-mandatory-multifactor-authentication?tabs=dotnet) for more information. + +???+ warning + Remember that if the user is newly created, you need to sign in with that account first, as Microsoft will prompt you to change the password. If you don’t complete this step, user authentication will fail because Microsoft marks the initial password as expired. + + +1. Search and select: - `User.Read` ![Permission Screenshots](./img/directory-permission-delegated.png) -6. After adding all the permissions, click on `Grant admin consent` +2. Click `Add permissions`, then **grant admin consent** ![Grant Admin Consent](./img/grant-admin-consent.png) @@ -147,37 +212,32 @@ Follow these steps to assign the permissions: ![Final Permission Assignment](./img/final-permissions-m365.png) ---- +3. Assign **required roles** to your **user** -### Assign required roles to your user + Assign one of the following roles to your User: -Assign one of the following roles to your User: + - `Global Reader` (recommended): this allows you to read all roles needed. + - `Exchange Administrator` and `Teams Administrator`: user needs both roles but with this [roles](https://learn.microsoft.com/en-us/exchange/permissions-exo/permissions-exo#microsoft-365-permissions-in-exchange-online) you can access to the same information as a Global Reader (here you only read so that's why we recomend that role). -- `Global Reader` (recommended): this allows you to read all roles needed. -- `Exchange Administrator` and `Teams Administrator`: user needs both roles but with this [roles](https://learn.microsoft.com/en-us/exchange/permissions-exo/permissions-exo#microsoft-365-permissions-in-exchange-online) you can access to the same information as a Global Reader (here you only read so that's why we recomend that role). + Follow these steps to assign the role: -Follow these steps to assign the role: + 1. Go to Users > All Users > Click on the email for the user you will use -1. Go to Users > All Users > Click on the email for the user you will use + ![User Overview](./img/user-info-page.png) - ![User Overview](./img/user-info-page.png) + 2. Click `Assigned Roles` -2. Click `Assigned Roles` + ![User Roles](./img/user-role-page.png) - ![User Roles](./img/user-role-page.png) + 3. Click on `Add assignments`, then search and select: -3. Click on `Add assignments`, then search and select: + - `Global Reader` This is the recommended, if you want to use the others just search for them - - `Global Reader` This is the recommended, if you want to use the others just search for them + ![Global Reader Screenshots](./img/global-reader.png) - ![Global Reader Screenshots](./img/global-reader.png) + 4. Click on next, then assign the role as `Active`, and click on `Assign` to grant admin consent -4. Click on next, then assign the role as `Active`, and click on `Assign` to grant admin consent - - ![Grant Admin Consent for Role](./img/grant-admin-consent-for-role.png) - -???+ warning - Remember that if the user is newly created, you need to sign in with that account first, as Microsoft will prompt you to change the password. If you don’t complete this step, user authentication will fail because Microsoft marks the initial password as expired. + ![Grant Admin Consent for Role](./img/grant-admin-consent-for-role.png) --- @@ -193,6 +253,8 @@ Follow these steps to assign the role: - `Client ID` - `Tenant ID` - `AZURE_CLIENT_SECRET` from earlier + + If you are using user authentication, also add: - `M365_USER` the user using the correct assigned domain, more info [here](../../getting-started/requirements.md#service-principal-and-user-credentials-authentication-recommended) - `M365_PASSWORD` the password of the user diff --git a/docs/tutorials/microsoft365/img/add-assginments.png b/docs/tutorials/microsoft365/img/add-assginments.png new file mode 100644 index 0000000000..af073a1d99 Binary files /dev/null and b/docs/tutorials/microsoft365/img/add-assginments.png differ diff --git a/docs/tutorials/microsoft365/img/assign-exchange-administrator-role.png b/docs/tutorials/microsoft365/img/assign-exchange-administrator-role.png new file mode 100644 index 0000000000..10e5752e8a Binary files /dev/null and b/docs/tutorials/microsoft365/img/assign-exchange-administrator-role.png differ diff --git a/docs/tutorials/microsoft365/img/exchange-administrator-role.png b/docs/tutorials/microsoft365/img/exchange-administrator-role.png new file mode 100644 index 0000000000..08a05dd2d9 Binary files /dev/null and b/docs/tutorials/microsoft365/img/exchange-administrator-role.png differ diff --git a/docs/tutorials/microsoft365/img/exchange-permission.png b/docs/tutorials/microsoft365/img/exchange-permission.png new file mode 100644 index 0000000000..29084600c2 Binary files /dev/null and b/docs/tutorials/microsoft365/img/exchange-permission.png differ diff --git a/docs/tutorials/microsoft365/img/final-permissions.png b/docs/tutorials/microsoft365/img/final-permissions.png new file mode 100644 index 0000000000..f3bed7981e Binary files /dev/null and b/docs/tutorials/microsoft365/img/final-permissions.png differ diff --git a/docs/tutorials/microsoft365/img/grant-external-api-permissions.png b/docs/tutorials/microsoft365/img/grant-external-api-permissions.png new file mode 100644 index 0000000000..0492e25dc3 Binary files /dev/null and b/docs/tutorials/microsoft365/img/grant-external-api-permissions.png differ diff --git a/docs/tutorials/microsoft365/img/here.png b/docs/tutorials/microsoft365/img/here.png new file mode 100644 index 0000000000..9c0f947c07 Binary files /dev/null and b/docs/tutorials/microsoft365/img/here.png differ diff --git a/docs/tutorials/microsoft365/img/m365-credentials.png b/docs/tutorials/microsoft365/img/m365-credentials.png index 87191751a8..ab912f9d8e 100644 Binary files a/docs/tutorials/microsoft365/img/m365-credentials.png and b/docs/tutorials/microsoft365/img/m365-credentials.png differ diff --git a/docs/tutorials/microsoft365/img/search-exchange-api.png b/docs/tutorials/microsoft365/img/search-exchange-api.png new file mode 100644 index 0000000000..5c4514c168 Binary files /dev/null and b/docs/tutorials/microsoft365/img/search-exchange-api.png differ diff --git a/docs/tutorials/microsoft365/img/search-skype-teams-tenant-admin-api.png b/docs/tutorials/microsoft365/img/search-skype-teams-tenant-admin-api.png new file mode 100644 index 0000000000..0009f3c05c Binary files /dev/null and b/docs/tutorials/microsoft365/img/search-skype-teams-tenant-admin-api.png differ diff --git a/docs/tutorials/microsoft365/img/teams-permission.png b/docs/tutorials/microsoft365/img/teams-permission.png new file mode 100644 index 0000000000..734a24f184 Binary files /dev/null and b/docs/tutorials/microsoft365/img/teams-permission.png differ diff --git a/docs/tutorials/prowler-app-lighthouse.md b/docs/tutorials/prowler-app-lighthouse.md new file mode 100644 index 0000000000..52e94cfe9d --- /dev/null +++ b/docs/tutorials/prowler-app-lighthouse.md @@ -0,0 +1,204 @@ +# Prowler Lighthouse + +Prowler Lighthouse is an AI Cloud Security Analyst chatbot that helps you understand, prioritize, and remediate security findings in your cloud environments. It's designed to provide security expertise for teams without dedicated resources, acting as your 24/7 virtual cloud security analyst. + +Prowler Lighthouse + +## How It Works + +Prowler Lighthouse uses OpenAI's language models and integrates with your Prowler security findings data. + +Here's what's happening behind the scenes: + +- The system uses a multi-agent architecture built with [LanggraphJS](https://github.com/langchain-ai/langgraphjs) for LLM logic and [Vercel AI SDK UI](https://sdk.vercel.ai/docs/ai-sdk-ui/overview) for frontend chatbot. +- It uses a ["supervisor" architecture](https://langchain-ai.lang.chat/langgraphjs/tutorials/multi_agent/agent_supervisor/) that interacts with different agents for specialized tasks. For example, `findings_agent` can analyze detected security findings, while `overview_agent` provides a summary of connected cloud accounts. +- The system connects to OpenAI models to understand, fetch the right data, and respond to the user's query. +???+ note + Lighthouse is tested against `gpt-4o` and `gpt-4o-mini` OpenAI models. +- The supervisor agent is the main contact point. It is what users interact with directly from the chat interface. It coordinates with other agents to answer users' questions comprehensively. + +Lighthouse Architecture + +???+ note + All agents can only read relevant security data. They cannot modify your data or access sensitive information like configured secrets or tenant details. + +## Set up + +Getting started with Prowler Lighthouse is easy: + +1. Go to the configuration page in your Prowler dashboard. +2. Enter your OpenAI API key. +3. Select your preferred model. The recommended one for best results is `gpt-4o`. +4. (Optional) Add business context to improve response quality and prioritization. + +Lighthouse Configuration + +### Adding Business Context + +The optional business context field lets you provide additional information to help Lighthouse understand your environment and priorities, including: + +- Your organization's cloud security goals +- Information about account owners or responsible teams +- Compliance requirements for your organization +- Current security initiatives or focus areas + +Better context leads to more relevant responses and prioritization that aligns with your needs. + +## Capabilities + +Prowler Lighthouse is designed to be your AI security team member, with capabilities including: + +### Natural Language Querying + +Ask questions in plain English about your security findings. Examples: + +- "What are my highest risk findings?" +- "Show me all S3 buckets with public access." +- "What security issues were found in my production accounts?" + +Natural language querying + +### Detailed Remediation Guidance + +Get tailored step-by-step instructions for fixing security issues: + +- Clear explanations of the problem and its impact +- Commands or console steps to implement fixes +- Alternative approaches with different solutions + +Detailed Remediation + +### Enhanced Context and Analysis + +Lighthouse can provide additional context to help you understand the findings: + +- Explain security concepts related to findings in simple terms +- Provide risk assessments based on your environment and context +- Connect related findings to show broader security patterns + +Business Context + +Contextual Responses + +## Important Notes + +Prowler Lighthouse is powerful, but there are limitations: + +- **Continuous improvement**: Please report any issues, as the feature may make mistakes or encounter errors, despite extensive testing. +- **Access limitations**: Lighthouse can only access data the logged-in user can view. If you can't see certain information, Lighthouse can't see it either. +- **NextJS session dependence**: If your Prowler application session expires or logs out, Lighthouse will error out. Refresh and log back in to continue. +- **Response quality**: The response quality depends on the selected OpenAI model. For best results, use gpt-4o. + +### Getting Help + +If you encounter issues with Prowler Lighthouse or have suggestions for improvements, please [reach out through our Slack channel](https://goto.prowler.com/slack). + +### What Data Is Shared to OpenAI? + +The following API endpoints are accessible to Prowler Lighthouse. Data from the following API endpoints could be shared with OpenAI depending on the scope of user's query: + +#### Accessible API Endpoints + +**User Management:** + +- List all users - `/api/v1/users` +- Retrieve the current user's information - `/api/v1/users/me` + +**Provider Management:** + +- List all providers - `/api/v1/providers` +- Retrieve data from a provider - `/api/v1/providers/{id}` + +**Scan Management:** + +- List all scans - `/api/v1/scans` +- Retrieve data from a specific scan - `/api/v1/scans/{id}` + +**Resource Management:** + +- List all resources - `/api/v1/resources` +- Retrieve data for a resource - `/api/v1/resources/{id}` + +**Findings Management:** + +- List all findings - `/api/v1/findings` +- Retrieve data from a specific finding - `/api/v1/findings/{id}` +- Retrieve metadata values from findings - `/api/v1/findings/metadata` + +**Overview Data:** + +- Get aggregated findings data - `/api/v1/overviews/findings` +- Get findings data by severity - `/api/v1/overviews/findings_severity` +- Get aggregated provider data - `/api/v1/overviews/providers` +- Get findings data by service - `/api/v1/overviews/services` + +**Compliance Management:** + +- List compliance overviews for a scan - `/api/v1/compliance-overviews` +- Retrieve data from a specific compliance overview - `/api/v1/compliance-overviews/{id}` + +#### Excluded API Endpoints + +Not all Prowler API endpoints are integrated with Lighthouse. They are intentionally excluded for the following reasons: + +- OpenAI/other LLM providers shouldn't have access to sensitive data (like fetching provider secrets and other sensitive config) +- Users queries don't need responses from those API endpoints (ex: tasks, tenant details, downloading zip file, etc.) + +**Excluded Endpoints:** + +**User Management:** + +- List specific users information - `/api/v1/users/{id}` +- List user memberships - `/api/v1/users/{user_pk}/memberships` +- Retrieve membership data from the user - `/api/v1/users/{user_pk}/memberships/{id}` + +**Tenant Management:** + +- List all tenants - `/api/v1/tenants` +- Retrieve data from a tenant - `/api/v1/tenants/{id}` +- List tenant memberships - `/api/v1/tenants/{tenant_pk}/memberships` +- List all invitations - `/api/v1/tenants/invitations` +- Retrieve data from tenant invitation - `/api/v1/tenants/invitations/{id}` + +**Security and Configuration:** + +- List all secrets - `/api/v1/providers/secrets` +- Retrieve data from a secret - `/api/v1/providers/secrets/{id}` +- List all provider groups - `/api/v1/provider-groups` +- Retrieve data from a provider group - `/api/v1/provider-groups/{id}` + +**Reports and Tasks:** + +- Download zip report - `/api/v1/scans/{v1}/report` +- List all tasks - `/api/v1/tasks` +- Retrieve data from a specific task - `/api/v1/tasks/{id}` + +**Lighthouse Configuration:** + +- List OpenAI configuration - `/api/v1/lighthouse-config` +- Retrieve OpenAI key and configuration - `/api/v1/lighthouse-config/{id}` + +???+ note + Agents only have access to hit GET endpoints. They don't have access to other HTTP methods. + +## FAQs + +**1. Why only OpenAI models?** + +During feature development, we evaluated other LLM models. + +- **Claude AI** - Claude models have [tier-based ratelimits](https://docs.anthropic.com/en/api/rate-limits#requirements-to-advance-tier). For Lighthouse to answer slightly complex questions, there are a handful of API calls to the LLM provider within few seconds. With Claude's tiering system, users must purchase $400 credits or convert their subscription to monthly invoicing after talking to their sales team. This pricing may not suit all Prowler users. +- **Gemini Models** - Gemini lacks a solid tool calling feature like OpenAI. It calls functions recursively until exceeding limits. Gemini-2.5-Pro-Experimental is better than previous models regarding tool calling and responding, but it's still experimental. +- **Deepseek V3** - Doesn't support system prompt messages. + +**2. Why a multi-agent supervisor model?** + +Context windows are limited. While demo data fits inside the context window, querying real-world data often exceeds it. A multi-agent architecture is used so different agents fetch different sizes of data and respond with the minimum required data to the supervisor. This spreads the context window usage across agents. + +**3. Is my security data shared with OpenAI?** + +Minimal data is shared to generate useful responses. Agents can access security findings and remediation details when needed. Provider secrets are protected by design and cannot be read. The Lighthouse key is only accessible to our NextJS server and is never sent to LLMs. Resource metadata (names, tags, account/project IDs, etc) may be shared with OpenAI based on your query requirements. + +**4. Can the Lighthouse change my cloud environment?** + +No. The agent doesn't have the tools to make the changes, even if the configured cloud provider API keys contain permissions to modify resources. diff --git a/docs/tutorials/prowler-app-sso.md b/docs/tutorials/prowler-app-sso.md index a82971c8a9..2ca898beb4 100644 --- a/docs/tutorials/prowler-app-sso.md +++ b/docs/tutorials/prowler-app-sso.md @@ -1,6 +1,6 @@ # Configuring SAML Single Sign-On (SSO) in Prowler -This guide explains how to enable and test SAML SSO integration in Prowler. It includes environment setup, certificate configuration, API endpoints, and how to configure Okta as your Identity Provider (IdP). +This guide explains how to enable and test SAML SSO integration in Prowler. It includes environment setup, API endpoints, and how to configure Okta as your Identity Provider (IdP). --- @@ -20,26 +20,6 @@ Update this variable to specify which domains Django should accept incoming requ DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,prowler-api,mycompany.prowler ``` -# SAML Certificates - -To enable SAML support, you must provide a public certificate and private key to allow Prowler to sign SAML requests and validate responses. - -### Why is this necessary? - -SAML relies on digital signatures to verify trust between the Identity Provider (IdP) and the Service Provider (SP). Prowler acts as the SP and must use a certificate to sign outbound authentication requests. - -### Add to your .env file: - -```env -SAML_PUBLIC_CERT="-----BEGIN CERTIFICATE----- -...your certificate here... ------END CERTIFICATE-----" - -SAML_PRIVATE_KEY="-----BEGIN PRIVATE KEY----- -...your private key here... ------END PRIVATE KEY-----" -``` - # SAML Configuration API You can manage SAML settings via the API. Prowler provides full CRUD support for tenant-specific SAML configuration. @@ -60,7 +40,7 @@ You can manage SAML settings via the API. Prowler provides full CRUD support for ### Description -This endpoint receives an email and checks if there is an active SAML configuration for the associated domain (i.e., the part after the @). If a configuration exists and the required certificates are present, it responds with an HTTP 302 redirect to the appropriate saml_login endpoint for the organization. +This endpoint receives an email and checks if there is an active SAML configuration for the associated domain (i.e., the part after the @). If a configuration exists it responds with an HTTP 302 redirect to the appropriate saml_login endpoint for the organization. - POST /api/v1/accounts/saml/initiate/ @@ -78,7 +58,7 @@ This endpoint receives an email and checks if there is an active SAML configurat • 302 FOUND: Redirects to the SAML login URL associated with the organization. - • 403 FORBIDDEN: The domain is not authorized or SAML certificates are missing from the configuration. + • 403 FORBIDDEN: The domain is not authorized. ### Validation logic @@ -86,8 +66,6 @@ This endpoint receives an email and checks if there is an active SAML configurat • Retrieves the related SAMLConfiguration object via tenant_id. - • Verifies that SAML_PUBLIC_CERT and SAML_PRIVATE_KEY environment variables are set. - # SAML Integration: Testing Guide @@ -95,41 +73,22 @@ This document outlines the process for testing the SAML integration functionalit --- -## 1. Generate Self-Signed Certificate and Private Key - -First, generate a self-signed certificate and corresponding private key using OpenSSL: - -```bash -openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \ - -keyout saml_private_key.pem \ - -out saml_public_cert.pem \ - -subj "/C=US/ST=Test/L=Test/O=Test/OU=Test/CN=localhost" -``` - -## 2. Add Certificate Values to .env - -Paste the generated values into your .env file: -``` -SAML_PUBLIC_CERT= -SAML_PRIVATE_KEY= -``` - -## 3. Start Ngrok and Update ALLOWED_HOSTS +## 1. Start Ngrok and Update ALLOWED_HOSTS Start ngrok on port 8080: ``` ngrok http 8080 ``` -Then, copy the generated ngrok URL and include it in the ALLOWED_HOSTS setting. If you’re using the development environment, it usually defaults to *, but in some cases this may not work properly, like in my tests (investigate): +Then, copy the generated ngrok URL and include it in the ALLOWED_HOSTS setting. If you're using the development environment, it usually defaults to *, but in some cases this may not work properly, like in my tests (investigate): ``` ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["*"]) ``` -## 4. Configure the Identity Provider (IdP) +## 2. Configure the Identity Provider (IdP) -Start your environment and configure your IdP. You will need to download the IdP’s metadata XML file. +Start your environment and configure your IdP. You will need to download the IdP's metadata XML file. Your Assertion Consumer Service (ACS) URL must follow this format: @@ -137,7 +96,7 @@ Your Assertion Consumer Service (ACS) URL must follow this format: https:///api/v1/accounts/saml//acs/ ``` -## 5. IdP Attribute Mapping +## 3. IdP Attribute Mapping The following fields are expected from the IdP: @@ -147,11 +106,11 @@ The following fields are expected from the IdP: - userType (this is the name of the role the user should be assigned) -- companyName (this is filled automatically if the IdP includes an “organization” field) +- companyName (this is filled automatically if the IdP includes an "organization" field) These values are dynamic. If the values change in the IdP, they will be updated on the next login. -## 6. SAML Configuration API (POST) +## 4. SAML Configuration API (POST) SAML configuration is managed via a CRUD API. Use the following POST request to create a new configuration: @@ -171,7 +130,37 @@ curl --location 'http://localhost:8080/api/v1/saml-config' \ }' ``` -## 7. Start SAML Login Flow +## 5. SAML SSO Callback Configuration + +### Environment Variable Configuration + +The SAML authentication flow requires proper callback URL configuration to handle post-authentication redirects. Configure the following environment variables: + +#### `SAML_SSO_CALLBACK_URL` + +Specifies the callback endpoint that will be invoked upon successful SAML authentication completion. This URL directs users back to the web application interface. + +```env +SAML_SSO_CALLBACK_URL="${AUTH_URL}/api/auth/callback/saml" +``` + +#### `AUTH_URL` + +Defines the base URL of the web user interface application that serves as the authentication callback destination. + +```env +AUTH_URL="" +``` + +### Configuration Notes + +- The `SAML_SSO_CALLBACK_URL` dynamically references the `AUTH_URL` variable to construct the complete callback endpoint +- Ensure the `AUTH_URL` points to the correct web UI deployment (development, staging, or production) +- The callback endpoint `/api/auth/callback/saml` must be accessible and properly configured to handle SAML authentication responses +- Both environment variables are required for proper SAML SSO functionality +- Verify that the `NEXT_PUBLIC_API_BASE_URL` environment variable is properly configured to reference the correct API server base URL corresponding to your target deployment environment. This ensures proper routing of SAML callback requests to the appropriate backend services. + +## 6. Start SAML Login Flow Once everything is configured, start the SAML login process by visiting the following URL: @@ -181,6 +170,6 @@ https:///api/v1/accounts/saml//login/?email= + +#### **Step 4.3.1: Service Account Authentication** + +First of all, in the same project that you selected in the previous step, you need to create a service account and then generate a key in JSON format for it. For more information about this, you can follow the next Google Cloud documentation tutorials: + +- [Create a service account](https://cloud.google.com/iam/docs/creating-managing-service-accounts) +- [Generate a key for a service account](https://cloud.google.com/iam/docs/creating-managing-service-account-keys) + +GCP Service Account Credentials + +#### **Step 4.3.2: Application Default Credentials** 1. Run the following command in your terminal to authenticate with GCP: ```bash @@ -151,7 +170,15 @@ By default, the `kubeconfig` file is located at `~/.kube/config`. --- ### **Step 4.5: M365 Credentials** -For M365, Prowler App uses a service principal application with user and password to authenticate, for more information about the requirements needed for this provider check this [section](../getting-started/requirements.md#microsoft-365). Also, the detailed steps of how to add this provider to Prowler Cloud and start using it are [here](./microsoft365/getting-started-m365.md). +For M365, you must enter your Domain ID and choose the authentication method you want to use: + +- Service Principal Authentication (Recommended) +- User Authentication (only works if the user does not have MFA enabled) + +???+ note + User authentication with M365_USER and M365_PASSWORD is optional and will only work if the account does not enforce MFA. + +For full setup instructions and requirements, check the [Microsoft 365 provider requirements](./microsoft365/getting-started-m365.md). Prowler Cloud M365 Credentials diff --git a/mkdocs.yml b/mkdocs.yml index bf597ce494..c0adae11db 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -54,6 +54,7 @@ nav: - Role-Based Access Control: tutorials/prowler-app-rbac.md - Social Login: tutorials/prowler-app-social-login.md - SSO with SAML: tutorials/prowler-app-sso.md + - Lighthouse: tutorials/prowler-app-lighthouse.md - CLI: - Miscellaneous: tutorials/misc.md - Reporting: tutorials/reporting.md @@ -106,6 +107,10 @@ nav: - Getting Started: tutorials/microsoft365/getting-started-m365.md - Authentication: tutorials/microsoft365/authentication.md - Use of PowerShell: tutorials/microsoft365/use-of-powershell.md + - GitHub: + - Authentication: tutorials/github/authentication.md + - IaC: + - Getting Started: tutorials/iac/getting-started-iac.md - Developer Guide: - General Concepts: - Introduction: developer-guide/introduction.md @@ -115,6 +120,7 @@ nav: - Outputs: developer-guide/outputs.md - Integrations: developer-guide/integrations.md - Compliance: developer-guide/security-compliance-framework.md + - Lighthouse: developer-guide/lighthouse.md - Provider Specific Details: - AWS: developer-guide/aws-details.md - Azure: developer-guide/azure-details.md @@ -128,6 +134,7 @@ nav: - Unit Tests: developer-guide/unit-testing.md - Integration Tests: developer-guide/integration-testing.md - Debugging: developer-guide/debugging.md + - Configurable Checks: developer-guide/configurable-checks.md - Security: security.md - Contact Us: contact.md - Troubleshooting: troubleshooting.md diff --git a/poetry.lock b/poetry.lock index 58abe3af1c..6977137741 100644 --- a/poetry.lock +++ b/poetry.lock @@ -41,97 +41,102 @@ files = [ [[package]] name = "aiohttp" -version = "3.11.18" +version = "3.12.13" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "aiohttp-3.11.18-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:96264854fedbea933a9ca4b7e0c745728f01380691687b7365d18d9e977179c4"}, - {file = "aiohttp-3.11.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9602044ff047043430452bc3a2089743fa85da829e6fc9ee0025351d66c332b6"}, - {file = "aiohttp-3.11.18-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5691dc38750fcb96a33ceef89642f139aa315c8a193bbd42a0c33476fd4a1609"}, - {file = "aiohttp-3.11.18-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:554c918ec43f8480b47a5ca758e10e793bd7410b83701676a4782672d670da55"}, - {file = "aiohttp-3.11.18-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a4076a2b3ba5b004b8cffca6afe18a3b2c5c9ef679b4d1e9859cf76295f8d4f"}, - {file = "aiohttp-3.11.18-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:767a97e6900edd11c762be96d82d13a1d7c4fc4b329f054e88b57cdc21fded94"}, - {file = "aiohttp-3.11.18-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0ddc9337a0fb0e727785ad4f41163cc314376e82b31846d3835673786420ef1"}, - {file = "aiohttp-3.11.18-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f414f37b244f2a97e79b98d48c5ff0789a0b4b4609b17d64fa81771ad780e415"}, - {file = "aiohttp-3.11.18-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fdb239f47328581e2ec7744ab5911f97afb10752332a6dd3d98e14e429e1a9e7"}, - {file = "aiohttp-3.11.18-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f2c50bad73ed629cc326cc0f75aed8ecfb013f88c5af116f33df556ed47143eb"}, - {file = "aiohttp-3.11.18-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a8d8f20c39d3fa84d1c28cdb97f3111387e48209e224408e75f29c6f8e0861d"}, - {file = "aiohttp-3.11.18-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:106032eaf9e62fd6bc6578c8b9e6dc4f5ed9a5c1c7fb2231010a1b4304393421"}, - {file = "aiohttp-3.11.18-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:b491e42183e8fcc9901d8dcd8ae644ff785590f1727f76ca86e731c61bfe6643"}, - {file = "aiohttp-3.11.18-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ad8c745ff9460a16b710e58e06a9dec11ebc0d8f4dd82091cefb579844d69868"}, - {file = "aiohttp-3.11.18-cp310-cp310-win32.whl", hash = "sha256:8e57da93e24303a883146510a434f0faf2f1e7e659f3041abc4e3fb3f6702a9f"}, - {file = "aiohttp-3.11.18-cp310-cp310-win_amd64.whl", hash = "sha256:cc93a4121d87d9f12739fc8fab0a95f78444e571ed63e40bfc78cd5abe700ac9"}, - {file = "aiohttp-3.11.18-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:427fdc56ccb6901ff8088544bde47084845ea81591deb16f957897f0f0ba1be9"}, - {file = "aiohttp-3.11.18-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c828b6d23b984255b85b9b04a5b963a74278b7356a7de84fda5e3b76866597b"}, - {file = "aiohttp-3.11.18-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5c2eaa145bb36b33af1ff2860820ba0589e165be4ab63a49aebfd0981c173b66"}, - {file = "aiohttp-3.11.18-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d518ce32179f7e2096bf4e3e8438cf445f05fedd597f252de9f54c728574756"}, - {file = "aiohttp-3.11.18-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0700055a6e05c2f4711011a44364020d7a10fbbcd02fbf3e30e8f7e7fddc8717"}, - {file = "aiohttp-3.11.18-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bd1cde83e4684324e6ee19adfc25fd649d04078179890be7b29f76b501de8e4"}, - {file = "aiohttp-3.11.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73b8870fe1c9a201b8c0d12c94fe781b918664766728783241a79e0468427e4f"}, - {file = "aiohttp-3.11.18-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25557982dd36b9e32c0a3357f30804e80790ec2c4d20ac6bcc598533e04c6361"}, - {file = "aiohttp-3.11.18-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7e889c9df381a2433802991288a61e5a19ceb4f61bd14f5c9fa165655dcb1fd1"}, - {file = "aiohttp-3.11.18-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9ea345fda05bae217b6cce2acf3682ce3b13d0d16dd47d0de7080e5e21362421"}, - {file = "aiohttp-3.11.18-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9f26545b9940c4b46f0a9388fd04ee3ad7064c4017b5a334dd450f616396590e"}, - {file = "aiohttp-3.11.18-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3a621d85e85dccabd700294494d7179ed1590b6d07a35709bb9bd608c7f5dd1d"}, - {file = "aiohttp-3.11.18-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9c23fd8d08eb9c2af3faeedc8c56e134acdaf36e2117ee059d7defa655130e5f"}, - {file = "aiohttp-3.11.18-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9e6b0e519067caa4fd7fb72e3e8002d16a68e84e62e7291092a5433763dc0dd"}, - {file = "aiohttp-3.11.18-cp311-cp311-win32.whl", hash = "sha256:122f3e739f6607e5e4c6a2f8562a6f476192a682a52bda8b4c6d4254e1138f4d"}, - {file = "aiohttp-3.11.18-cp311-cp311-win_amd64.whl", hash = "sha256:e6f3c0a3a1e73e88af384b2e8a0b9f4fb73245afd47589df2afcab6b638fa0e6"}, - {file = "aiohttp-3.11.18-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:63d71eceb9cad35d47d71f78edac41fcd01ff10cacaa64e473d1aec13fa02df2"}, - {file = "aiohttp-3.11.18-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d1929da615840969929e8878d7951b31afe0bac883d84418f92e5755d7b49508"}, - {file = "aiohttp-3.11.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d0aebeb2392f19b184e3fdd9e651b0e39cd0f195cdb93328bd124a1d455cd0e"}, - {file = "aiohttp-3.11.18-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3849ead845e8444f7331c284132ab314b4dac43bfae1e3cf350906d4fff4620f"}, - {file = "aiohttp-3.11.18-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e8452ad6b2863709f8b3d615955aa0807bc093c34b8e25b3b52097fe421cb7f"}, - {file = "aiohttp-3.11.18-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b8d2b42073611c860a37f718b3d61ae8b4c2b124b2e776e2c10619d920350ec"}, - {file = "aiohttp-3.11.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fbf91f6a0ac317c0a07eb328a1384941872f6761f2e6f7208b63c4cc0a7ff6"}, - {file = "aiohttp-3.11.18-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ff5625413fec55216da5eaa011cf6b0a2ed67a565914a212a51aa3755b0009"}, - {file = "aiohttp-3.11.18-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7f33a92a2fde08e8c6b0c61815521324fc1612f397abf96eed86b8e31618fdb4"}, - {file = "aiohttp-3.11.18-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:11d5391946605f445ddafda5eab11caf310f90cdda1fd99865564e3164f5cff9"}, - {file = "aiohttp-3.11.18-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3cc314245deb311364884e44242e00c18b5896e4fe6d5f942e7ad7e4cb640adb"}, - {file = "aiohttp-3.11.18-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0f421843b0f70740772228b9e8093289924359d306530bcd3926f39acbe1adda"}, - {file = "aiohttp-3.11.18-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e220e7562467dc8d589e31c1acd13438d82c03d7f385c9cd41a3f6d1d15807c1"}, - {file = "aiohttp-3.11.18-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ab2ef72f8605046115bc9aa8e9d14fd49086d405855f40b79ed9e5c1f9f4faea"}, - {file = "aiohttp-3.11.18-cp312-cp312-win32.whl", hash = "sha256:12a62691eb5aac58d65200c7ae94d73e8a65c331c3a86a2e9670927e94339ee8"}, - {file = "aiohttp-3.11.18-cp312-cp312-win_amd64.whl", hash = "sha256:364329f319c499128fd5cd2d1c31c44f234c58f9b96cc57f743d16ec4f3238c8"}, - {file = "aiohttp-3.11.18-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:474215ec618974054cf5dc465497ae9708543cbfc312c65212325d4212525811"}, - {file = "aiohttp-3.11.18-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ced70adf03920d4e67c373fd692123e34d3ac81dfa1c27e45904a628567d804"}, - {file = "aiohttp-3.11.18-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d9f6c0152f8d71361905aaf9ed979259537981f47ad099c8b3d81e0319814bd"}, - {file = "aiohttp-3.11.18-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a35197013ed929c0aed5c9096de1fc5a9d336914d73ab3f9df14741668c0616c"}, - {file = "aiohttp-3.11.18-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:540b8a1f3a424f1af63e0af2d2853a759242a1769f9f1ab053996a392bd70118"}, - {file = "aiohttp-3.11.18-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9e6710ebebfce2ba21cee6d91e7452d1125100f41b906fb5af3da8c78b764c1"}, - {file = "aiohttp-3.11.18-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8af2ef3b4b652ff109f98087242e2ab974b2b2b496304063585e3d78de0b000"}, - {file = "aiohttp-3.11.18-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:28c3f975e5ae3dbcbe95b7e3dcd30e51da561a0a0f2cfbcdea30fc1308d72137"}, - {file = "aiohttp-3.11.18-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c28875e316c7b4c3e745172d882d8a5c835b11018e33432d281211af35794a93"}, - {file = "aiohttp-3.11.18-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:13cd38515568ae230e1ef6919e2e33da5d0f46862943fcda74e7e915096815f3"}, - {file = "aiohttp-3.11.18-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0e2a92101efb9f4c2942252c69c63ddb26d20f46f540c239ccfa5af865197bb8"}, - {file = "aiohttp-3.11.18-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e6d3e32b8753c8d45ac550b11a1090dd66d110d4ef805ffe60fa61495360b3b2"}, - {file = "aiohttp-3.11.18-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ea4cf2488156e0f281f93cc2fd365025efcba3e2d217cbe3df2840f8c73db261"}, - {file = "aiohttp-3.11.18-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d4df95ad522c53f2b9ebc07f12ccd2cb15550941e11a5bbc5ddca2ca56316d7"}, - {file = "aiohttp-3.11.18-cp313-cp313-win32.whl", hash = "sha256:cdd1bbaf1e61f0d94aced116d6e95fe25942f7a5f42382195fd9501089db5d78"}, - {file = "aiohttp-3.11.18-cp313-cp313-win_amd64.whl", hash = "sha256:bdd619c27e44382cf642223f11cfd4d795161362a5a1fc1fa3940397bc89db01"}, - {file = "aiohttp-3.11.18-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:469ac32375d9a716da49817cd26f1916ec787fc82b151c1c832f58420e6d3533"}, - {file = "aiohttp-3.11.18-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3cec21dd68924179258ae14af9f5418c1ebdbba60b98c667815891293902e5e0"}, - {file = "aiohttp-3.11.18-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b426495fb9140e75719b3ae70a5e8dd3a79def0ae3c6c27e012fc59f16544a4a"}, - {file = "aiohttp-3.11.18-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad2f41203e2808616292db5d7170cccf0c9f9c982d02544443c7eb0296e8b0c7"}, - {file = "aiohttp-3.11.18-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bc0ae0a5e9939e423e065a3e5b00b24b8379f1db46046d7ab71753dfc7dd0e1"}, - {file = "aiohttp-3.11.18-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe7cdd3f7d1df43200e1c80f1aed86bb36033bf65e3c7cf46a2b97a253ef8798"}, - {file = "aiohttp-3.11.18-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5199be2a2f01ffdfa8c3a6f5981205242986b9e63eb8ae03fd18f736e4840721"}, - {file = "aiohttp-3.11.18-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ccec9e72660b10f8e283e91aa0295975c7bd85c204011d9f5eb69310555cf30"}, - {file = "aiohttp-3.11.18-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1596ebf17e42e293cbacc7a24c3e0dc0f8f755b40aff0402cb74c1ff6baec1d3"}, - {file = "aiohttp-3.11.18-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:eab7b040a8a873020113ba814b7db7fa935235e4cbaf8f3da17671baa1024863"}, - {file = "aiohttp-3.11.18-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:5d61df4a05476ff891cff0030329fee4088d40e4dc9b013fac01bc3c745542c2"}, - {file = "aiohttp-3.11.18-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:46533e6792e1410f9801d09fd40cbbff3f3518d1b501d6c3c5b218f427f6ff08"}, - {file = "aiohttp-3.11.18-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c1b90407ced992331dd6d4f1355819ea1c274cc1ee4d5b7046c6761f9ec11829"}, - {file = "aiohttp-3.11.18-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a2fd04ae4971b914e54fe459dd7edbbd3f2ba875d69e057d5e3c8e8cac094935"}, - {file = "aiohttp-3.11.18-cp39-cp39-win32.whl", hash = "sha256:b2f317d1678002eee6fe85670039fb34a757972284614638f82b903a03feacdc"}, - {file = "aiohttp-3.11.18-cp39-cp39-win_amd64.whl", hash = "sha256:5e7007b8d1d09bce37b54111f593d173691c530b80f27c6493b928dabed9e6ef"}, - {file = "aiohttp-3.11.18.tar.gz", hash = "sha256:ae856e1138612b7e412db63b7708735cff4d38d0399f6a5435d3dac2669f558a"}, + {file = "aiohttp-3.12.13-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5421af8f22a98f640261ee48aae3a37f0c41371e99412d55eaf2f8a46d5dad29"}, + {file = "aiohttp-3.12.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fcda86f6cb318ba36ed8f1396a6a4a3fd8f856f84d426584392083d10da4de0"}, + {file = "aiohttp-3.12.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cd71c9fb92aceb5a23c4c39d8ecc80389c178eba9feab77f19274843eb9412d"}, + {file = "aiohttp-3.12.13-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34ebf1aca12845066c963016655dac897651e1544f22a34c9b461ac3b4b1d3aa"}, + {file = "aiohttp-3.12.13-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:893a4639694c5b7edd4bdd8141be296042b6806e27cc1d794e585c43010cc294"}, + {file = "aiohttp-3.12.13-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:663d8ee3ffb3494502ebcccb49078faddbb84c1d870f9c1dd5a29e85d1f747ce"}, + {file = "aiohttp-3.12.13-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0f8f6a85a0006ae2709aa4ce05749ba2cdcb4b43d6c21a16c8517c16593aabe"}, + {file = "aiohttp-3.12.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1582745eb63df267c92d8b61ca655a0ce62105ef62542c00a74590f306be8cb5"}, + {file = "aiohttp-3.12.13-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d59227776ee2aa64226f7e086638baa645f4b044f2947dbf85c76ab11dcba073"}, + {file = "aiohttp-3.12.13-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06b07c418bde1c8e737d8fa67741072bd3f5b0fb66cf8c0655172188c17e5fa6"}, + {file = "aiohttp-3.12.13-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:9445c1842680efac0f81d272fd8db7163acfcc2b1436e3f420f4c9a9c5a50795"}, + {file = "aiohttp-3.12.13-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:09c4767af0b0b98c724f5d47f2bf33395c8986995b0a9dab0575ca81a554a8c0"}, + {file = "aiohttp-3.12.13-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f3854fbde7a465318ad8d3fc5bef8f059e6d0a87e71a0d3360bb56c0bf87b18a"}, + {file = "aiohttp-3.12.13-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2332b4c361c05ecd381edb99e2a33733f3db906739a83a483974b3df70a51b40"}, + {file = "aiohttp-3.12.13-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1561db63fa1b658cd94325d303933553ea7d89ae09ff21cc3bcd41b8521fbbb6"}, + {file = "aiohttp-3.12.13-cp310-cp310-win32.whl", hash = "sha256:a0be857f0b35177ba09d7c472825d1b711d11c6d0e8a2052804e3b93166de1ad"}, + {file = "aiohttp-3.12.13-cp310-cp310-win_amd64.whl", hash = "sha256:fcc30ad4fb5cb41a33953292d45f54ef4066746d625992aeac33b8c681173178"}, + {file = "aiohttp-3.12.13-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7c229b1437aa2576b99384e4be668af1db84b31a45305d02f61f5497cfa6f60c"}, + {file = "aiohttp-3.12.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:04076d8c63471e51e3689c93940775dc3d12d855c0c80d18ac5a1c68f0904358"}, + {file = "aiohttp-3.12.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:55683615813ce3601640cfaa1041174dc956d28ba0511c8cbd75273eb0587014"}, + {file = "aiohttp-3.12.13-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:921bc91e602d7506d37643e77819cb0b840d4ebb5f8d6408423af3d3bf79a7b7"}, + {file = "aiohttp-3.12.13-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e72d17fe0974ddeae8ed86db297e23dba39c7ac36d84acdbb53df2e18505a013"}, + {file = "aiohttp-3.12.13-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0653d15587909a52e024a261943cf1c5bdc69acb71f411b0dd5966d065a51a47"}, + {file = "aiohttp-3.12.13-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a77b48997c66722c65e157c06c74332cdf9c7ad00494b85ec43f324e5c5a9b9a"}, + {file = "aiohttp-3.12.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6946bae55fd36cfb8e4092c921075cde029c71c7cb571d72f1079d1e4e013bc"}, + {file = "aiohttp-3.12.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f95db8c8b219bcf294a53742c7bda49b80ceb9d577c8e7aa075612b7f39ffb7"}, + {file = "aiohttp-3.12.13-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:03d5eb3cfb4949ab4c74822fb3326cd9655c2b9fe22e4257e2100d44215b2e2b"}, + {file = "aiohttp-3.12.13-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6383dd0ffa15515283c26cbf41ac8e6705aab54b4cbb77bdb8935a713a89bee9"}, + {file = "aiohttp-3.12.13-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6548a411bc8219b45ba2577716493aa63b12803d1e5dc70508c539d0db8dbf5a"}, + {file = "aiohttp-3.12.13-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:81b0fcbfe59a4ca41dc8f635c2a4a71e63f75168cc91026c61be665945739e2d"}, + {file = "aiohttp-3.12.13-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6a83797a0174e7995e5edce9dcecc517c642eb43bc3cba296d4512edf346eee2"}, + {file = "aiohttp-3.12.13-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5734d8469a5633a4e9ffdf9983ff7cdb512524645c7a3d4bc8a3de45b935ac3"}, + {file = "aiohttp-3.12.13-cp311-cp311-win32.whl", hash = "sha256:fef8d50dfa482925bb6b4c208b40d8e9fa54cecba923dc65b825a72eed9a5dbd"}, + {file = "aiohttp-3.12.13-cp311-cp311-win_amd64.whl", hash = "sha256:9a27da9c3b5ed9d04c36ad2df65b38a96a37e9cfba6f1381b842d05d98e6afe9"}, + {file = "aiohttp-3.12.13-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0aa580cf80558557285b49452151b9c69f2fa3ad94c5c9e76e684719a8791b73"}, + {file = "aiohttp-3.12.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b103a7e414b57e6939cc4dece8e282cfb22043efd0c7298044f6594cf83ab347"}, + {file = "aiohttp-3.12.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f64e748e9e741d2eccff9597d09fb3cd962210e5b5716047cbb646dc8fe06f"}, + {file = "aiohttp-3.12.13-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c955989bf4c696d2ededc6b0ccb85a73623ae6e112439398935362bacfaaf6"}, + {file = "aiohttp-3.12.13-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d640191016763fab76072c87d8854a19e8e65d7a6fcfcbf017926bdbbb30a7e5"}, + {file = "aiohttp-3.12.13-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4dc507481266b410dede95dd9f26c8d6f5a14315372cc48a6e43eac652237d9b"}, + {file = "aiohttp-3.12.13-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8a94daa873465d518db073bd95d75f14302e0208a08e8c942b2f3f1c07288a75"}, + {file = "aiohttp-3.12.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f52420cde4ce0bb9425a375d95577fe082cb5721ecb61da3049b55189e4e6"}, + {file = "aiohttp-3.12.13-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f7df1f620ec40f1a7fbcb99ea17d7326ea6996715e78f71a1c9a021e31b96b8"}, + {file = "aiohttp-3.12.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3062d4ad53b36e17796dce1c0d6da0ad27a015c321e663657ba1cc7659cfc710"}, + {file = "aiohttp-3.12.13-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:8605e22d2a86b8e51ffb5253d9045ea73683d92d47c0b1438e11a359bdb94462"}, + {file = "aiohttp-3.12.13-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:54fbbe6beafc2820de71ece2198458a711e224e116efefa01b7969f3e2b3ddae"}, + {file = "aiohttp-3.12.13-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:050bd277dfc3768b606fd4eae79dd58ceda67d8b0b3c565656a89ae34525d15e"}, + {file = "aiohttp-3.12.13-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2637a60910b58f50f22379b6797466c3aa6ae28a6ab6404e09175ce4955b4e6a"}, + {file = "aiohttp-3.12.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e986067357550d1aaa21cfe9897fa19e680110551518a5a7cf44e6c5638cb8b5"}, + {file = "aiohttp-3.12.13-cp312-cp312-win32.whl", hash = "sha256:ac941a80aeea2aaae2875c9500861a3ba356f9ff17b9cb2dbfb5cbf91baaf5bf"}, + {file = "aiohttp-3.12.13-cp312-cp312-win_amd64.whl", hash = "sha256:671f41e6146a749b6c81cb7fd07f5a8356d46febdaaaf07b0e774ff04830461e"}, + {file = "aiohttp-3.12.13-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d4a18e61f271127465bdb0e8ff36e8f02ac4a32a80d8927aa52371e93cd87938"}, + {file = "aiohttp-3.12.13-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:532542cb48691179455fab429cdb0d558b5e5290b033b87478f2aa6af5d20ace"}, + {file = "aiohttp-3.12.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d7eea18b52f23c050ae9db5d01f3d264ab08f09e7356d6f68e3f3ac2de9dfabb"}, + {file = "aiohttp-3.12.13-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad7c8e5c25f2a26842a7c239de3f7b6bfb92304593ef997c04ac49fb703ff4d7"}, + {file = "aiohttp-3.12.13-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6af355b483e3fe9d7336d84539fef460120c2f6e50e06c658fe2907c69262d6b"}, + {file = "aiohttp-3.12.13-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a95cf9f097498f35c88e3609f55bb47b28a5ef67f6888f4390b3d73e2bac6177"}, + {file = "aiohttp-3.12.13-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8ed8c38a1c584fe99a475a8f60eefc0b682ea413a84c6ce769bb19a7ff1c5ef"}, + {file = "aiohttp-3.12.13-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a0b9170d5d800126b5bc89d3053a2363406d6e327afb6afaeda2d19ee8bb103"}, + {file = "aiohttp-3.12.13-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:372feeace612ef8eb41f05ae014a92121a512bd5067db8f25101dd88a8db11da"}, + {file = "aiohttp-3.12.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a946d3702f7965d81f7af7ea8fb03bb33fe53d311df48a46eeca17e9e0beed2d"}, + {file = "aiohttp-3.12.13-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a0c4725fae86555bbb1d4082129e21de7264f4ab14baf735278c974785cd2041"}, + {file = "aiohttp-3.12.13-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b28ea2f708234f0a5c44eb6c7d9eb63a148ce3252ba0140d050b091b6e842d1"}, + {file = "aiohttp-3.12.13-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d4f5becd2a5791829f79608c6f3dc745388162376f310eb9c142c985f9441cc1"}, + {file = "aiohttp-3.12.13-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:60f2ce6b944e97649051d5f5cc0f439360690b73909230e107fd45a359d3e911"}, + {file = "aiohttp-3.12.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69fc1909857401b67bf599c793f2183fbc4804717388b0b888f27f9929aa41f3"}, + {file = "aiohttp-3.12.13-cp313-cp313-win32.whl", hash = "sha256:7d7e68787a2046b0e44ba5587aa723ce05d711e3a3665b6b7545328ac8e3c0dd"}, + {file = "aiohttp-3.12.13-cp313-cp313-win_amd64.whl", hash = "sha256:5a178390ca90419bfd41419a809688c368e63c86bd725e1186dd97f6b89c2706"}, + {file = "aiohttp-3.12.13-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:36f6c973e003dc9b0bb4e8492a643641ea8ef0e97ff7aaa5c0f53d68839357b4"}, + {file = "aiohttp-3.12.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6cbfc73179bd67c229eb171e2e3745d2afd5c711ccd1e40a68b90427f282eab1"}, + {file = "aiohttp-3.12.13-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1e8b27b2d414f7e3205aa23bb4a692e935ef877e3a71f40d1884f6e04fd7fa74"}, + {file = "aiohttp-3.12.13-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eabded0c2b2ef56243289112c48556c395d70150ce4220d9008e6b4b3dd15690"}, + {file = "aiohttp-3.12.13-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:003038e83f1a3ff97409999995ec02fe3008a1d675478949643281141f54751d"}, + {file = "aiohttp-3.12.13-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b6f46613031dbc92bdcaad9c4c22c7209236ec501f9c0c5f5f0b6a689bf50f3"}, + {file = "aiohttp-3.12.13-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c332c6bb04650d59fb94ed96491f43812549a3ba6e7a16a218e612f99f04145e"}, + {file = "aiohttp-3.12.13-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fea41a2c931fb582cb15dc86a3037329e7b941df52b487a9f8b5aa960153cbd"}, + {file = "aiohttp-3.12.13-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:846104f45d18fb390efd9b422b27d8f3cf8853f1218c537f36e71a385758c896"}, + {file = "aiohttp-3.12.13-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d6c85ac7dd350f8da2520bac8205ce99df4435b399fa7f4dc4a70407073e390"}, + {file = "aiohttp-3.12.13-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5a1ecce0ed281bec7da8550da052a6b89552db14d0a0a45554156f085a912f48"}, + {file = "aiohttp-3.12.13-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:5304d74867028cca8f64f1cc1215eb365388033c5a691ea7aa6b0dc47412f495"}, + {file = "aiohttp-3.12.13-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:64d1f24ee95a2d1e094a4cd7a9b7d34d08db1bbcb8aa9fb717046b0a884ac294"}, + {file = "aiohttp-3.12.13-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:119c79922a7001ca6a9e253228eb39b793ea994fd2eccb79481c64b5f9d2a055"}, + {file = "aiohttp-3.12.13-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:bb18f00396d22e2f10cd8825d671d9f9a3ba968d708a559c02a627536b36d91c"}, + {file = "aiohttp-3.12.13-cp39-cp39-win32.whl", hash = "sha256:0022de47ef63fd06b065d430ac79c6b0bd24cdae7feaf0e8c6bac23b805a23a8"}, + {file = "aiohttp-3.12.13-cp39-cp39-win_amd64.whl", hash = "sha256:29e08111ccf81b2734ae03f1ad1cb03b9615e7d8f616764f22f71209c094f122"}, + {file = "aiohttp-3.12.13.tar.gz", hash = "sha256:47e2da578528264a12e4e3dd8dd72a7289e5f812758fe086473fab037a10fcce"}, ] [package.dependencies] -aiohappyeyeballs = ">=2.3.0" +aiohappyeyeballs = ">=2.5.0" aiosignal = ">=1.1.2" async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""} attrs = ">=17.3.0" @@ -141,7 +146,7 @@ propcache = ">=0.2.0" yarl = ">=1.17.0,<2.0" [package.extras] -speedups = ["Brotli ; platform_python_implementation == \"CPython\"", "aiodns (>=3.2.0) ; sys_platform == \"linux\" or sys_platform == \"darwin\"", "brotlicffi ; platform_python_implementation != \"CPython\""] +speedups = ["Brotli ; platform_python_implementation == \"CPython\"", "aiodns (>=3.3.0)", "brotlicffi ; platform_python_implementation != \"CPython\""] [[package]] name = "aiomultiprocess" @@ -272,18 +277,18 @@ test = ["coverage", "pytest", "pytest-cov"] [[package]] name = "astroid" -version = "3.3.9" +version = "3.3.10" description = "An abstract syntax tree for Python with inference support." optional = false python-versions = ">=3.9.0" groups = ["dev"] files = [ - {file = "astroid-3.3.9-py3-none-any.whl", hash = "sha256:d05bfd0acba96a7bd43e222828b7d9bc1e138aaeb0649707908d3702a9831248"}, - {file = "astroid-3.3.9.tar.gz", hash = "sha256:622cc8e3048684aa42c820d9d218978021c3c3d174fb03a9f0d615921744f550"}, + {file = "astroid-3.3.10-py3-none-any.whl", hash = "sha256:104fb9cb9b27ea95e847a94c003be03a9e039334a8ebca5ee27dafaf5c5711eb"}, + {file = "astroid-3.3.10.tar.gz", hash = "sha256:c332157953060c6deb9caa57303ae0d20b0fbdb2e59b4a4f2a6ba49d0a7961ce"}, ] [package.dependencies] -typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} [[package]] name = "async-timeout" @@ -320,14 +325,14 @@ tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" a [[package]] name = "authlib" -version = "1.5.2" +version = "1.6.0" 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.5.2-py2.py3-none-any.whl", hash = "sha256:8804dd4402ac5e4a0435ac49e0b6e19e395357cfa632a3f624dcb4f6df13b4b1"}, - {file = "authlib-1.5.2.tar.gz", hash = "sha256:fe85ec7e50c5f86f1e2603518bb3b4f632985eb4a355e52256530790e326c512"}, + {file = "authlib-1.6.0-py2.py3-none-any.whl", hash = "sha256:91685589498f79e8655e8a8947431ad6288831d643f11c55c2143ffcc738048d"}, + {file = "authlib-1.6.0.tar.gz", hash = "sha256:4367d32031b7af175ad3a323d571dc7257b7099d55978087ceae4a0d88cd3210"}, ] [package.dependencies] @@ -335,24 +340,24 @@ cryptography = "*" [[package]] name = "aws-sam-translator" -version = "1.97.0" +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.97.0-py3-none-any.whl", hash = "sha256:305701ab49eb546fd720b3682e99cadcd43539f4ddb8395ea03c90c9e14d3325"}, - {file = "aws_sam_translator-1.97.0.tar.gz", hash = "sha256:6f7ec94de0a9b220dd1f1a0bf7e2df95dd44a85592301ee830744da2f209b7e6"}, + {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.19.5,<2.dev0" +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.23,<2)", "boto3-stubs[appconfig,serverlessrepo] (>=1.19.5,<2.dev0)", "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 (>=8.0,<9.0)", "types-PyYAML (>=6.0,<7.0)", "types-jsonschema (>=3.2,<4.0)"] +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" @@ -396,14 +401,14 @@ files = [ [[package]] name = "azure-core" -version = "1.33.0" +version = "1.34.0" description = "Microsoft Azure Core Library for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "azure_core-1.33.0-py3-none-any.whl", hash = "sha256:9b5b6d0223a1d38c37500e6971118c1e0f13f54951e6893968b38910bc9cda8f"}, - {file = "azure_core-1.33.0.tar.gz", hash = "sha256:f367aa07b5e3005fec2c1e184b882b0b039910733907d001c20fb08ebb8c0eb9"}, + {file = "azure_core-1.34.0-py3-none-any.whl", hash = "sha256:0615d3b756beccdb6624d1c0ae97284f38b78fb59a2a9839bf927c66fbbdddd6"}, + {file = "azure_core-1.34.0.tar.gz", hash = "sha256:bdb544989f246a0ad1c85d72eeb45f2f835afdcbc5b45e43f0dbde7461c81ece"}, ] [package.dependencies] @@ -661,6 +666,42 @@ 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 = "23.3.0" @@ -1084,14 +1125,14 @@ files = [ [[package]] name = "certifi" -version = "2025.1.31" +version = "2025.6.15" description = "Python package for providing Mozilla's CA Bundle." optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" groups = ["main", "dev", "docs"] files = [ - {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, - {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, + {file = "certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057"}, + {file = "certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b"}, ] [[package]] @@ -1189,14 +1230,14 @@ files = [ [[package]] name = "cfn-lint" -version = "1.34.1" +version = "1.36.1" 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.34.1-py3-none-any.whl", hash = "sha256:2c21a908fa5d9b0c1caac2081c10261eaae7fce88364e4edc607717892f36d68"}, - {file = "cfn_lint-1.34.1.tar.gz", hash = "sha256:58f4352c370a710b72b389eda0acd6762e62f926e534d2eb1609d5326ded4bed"}, + {file = "cfn_lint-1.36.1-py3-none-any.whl", hash = "sha256:1f5eae322cabc9b73a717b83bf3a73709e2193fbf7fd399339e016a0838de7a3"}, + {file = "cfn_lint-1.36.1.tar.gz", hash = "sha256:ccb85398fb1d8d8d87edafe207ab6a133c4ab9aefd3ff93591b6ad8c3ccab291"}, ] [package.dependencies] @@ -1216,116 +1257,116 @@ 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.1" +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", "docs"] files = [ - {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, - {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, - {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, + {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 = "checkov" -version = "3.2.442" +version = "3.2.445" description = "Infrastructure as code static analysis" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "checkov-3.2.442-py3-none-any.whl", hash = "sha256:e94a3283bff9b4a81e54e57b4a00b02259dec0d85b17bf17e00652d137bd1a6d"}, - {file = "checkov-3.2.442.tar.gz", hash = "sha256:e5206872de63d389cfb1b7c1212ce4b5a147986152e890461d87251726b4b0e7"}, + {file = "checkov-3.2.445-py3-none-any.whl", hash = "sha256:c0cd1109cbcf1c764198a7c64b911845b83776587a65e0677880ed67a39d1f73"}, + {file = "checkov-3.2.445.tar.gz", hash = "sha256:78705d34a9c7234bd7076c1970daedd18e823f79e7d83d2998bb84695cc33ebc"}, ] [package.dependencies] @@ -1384,6 +1425,7 @@ description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" groups = ["main", "dev", "docs"] +markers = "python_version < \"3.10\"" files = [ {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, @@ -1392,6 +1434,22 @@ files = [ [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} +[[package]] +name = "click" +version = "8.2.1" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +groups = ["main", "dev", "docs"] +markers = "python_version >= \"3.10\"" +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-option-group" version = "0.5.7" @@ -1415,14 +1473,14 @@ test-cov = ["pytest", "pytest-cov"] [[package]] name = "click-plugins" -version = "1.1.1" +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.tar.gz", hash = "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b"}, - {file = "click_plugins-1.1.1-py2.py3-none-any.whl", hash = "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8"}, + {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] @@ -1653,21 +1711,18 @@ xml-validation = ["lxml (>=4,<6)"] [[package]] name = "dash" -version = "2.18.2" +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-2.18.2-py3-none-any.whl", hash = "sha256:0ce0479d1bc958e934630e2de7023b8a4558f23ce1f9f5a4b34b65eb3903a869"}, - {file = "dash-2.18.2.tar.gz", hash = "sha256:20e8404f73d0fe88ce2eae33c25bbc513cbe52f30d23a401fa5f24dbb44296c8"}, + {file = "dash-3.1.1-py3-none-any.whl", hash = "sha256:66fff37e79c6aa114cd55aea13683d1e9afe0e3f96b35388baca95ff6cfdad23"}, + {file = "dash-3.1.1.tar.gz", hash = "sha256:916b31cec46da0a3339da0e9df9f446126aa7f293c0544e07adf9fe4ba060b18"}, ] [package.dependencies] -dash-core-components = "2.0.0" -dash-html-components = "2.0.0" -dash-table = "5.0.0" -Flask = ">=1.0.4,<3.1" +Flask = ">=1.0.4,<3.2" importlib-metadata = "*" nest-asyncio = "*" plotly = ">=5.0.0" @@ -1675,11 +1730,12 @@ requests = "*" retrying = "*" setuptools = "*" typing-extensions = ">=4.1.1" -Werkzeug = "<3.1" +Werkzeug = "<3.2" [package.extras] -celery = ["celery[redis] (>=5.1.2)", "redis (>=3.5.3)"] -ci = ["black (==22.3.0)", "dash-dangerously-set-inner-html", "dash-flow-example (==0.0.5)", "flake8 (==7.0.0)", "flaky (==3.8.1)", "flask-talisman (==1.0.0)", "jupyterlab (<4.0.0)", "mimesis (<=11.1.0)", "mock (==4.0.3)", "numpy (<=1.26.3)", "openpyxl", "orjson (==3.10.3)", "pandas (>=1.4.0)", "pyarrow", "pylint (==3.0.3)", "pytest-mock", "pytest-rerunfailures", "pytest-sugar (==0.9.6)", "pyzmq (==25.1.2)", "xlrd (>=2.0.1)"] +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)"] @@ -1687,57 +1743,21 @@ testing = ["beautifulsoup4 (>=4.8.2)", "cryptography", "dash-testing-stub (>=0.0 [[package]] name = "dash-bootstrap-components" -version = "1.6.0" +version = "2.0.3" description = "Bootstrap themed components for use in Plotly Dash" optional = false -python-versions = "<4,>=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "dash_bootstrap_components-1.6.0-py3-none-any.whl", hash = "sha256:97f0f47b38363f18863e1b247462229266ce12e1e171cfb34d3c9898e6e5cd1e"}, - {file = "dash_bootstrap_components-1.6.0.tar.gz", hash = "sha256:960a1ec9397574792f49a8241024fa3cecde0f5930c971a3fc81f016cbeb1095"}, + {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 = ">=2.0.0" +dash = ">=3.0.4" [package.extras] -pandas = ["numpy", "pandas"] - -[[package]] -name = "dash-core-components" -version = "2.0.0" -description = "Core component suite for Dash" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "dash_core_components-2.0.0-py3-none-any.whl", hash = "sha256:52b8e8cce13b18d0802ee3acbc5e888cb1248a04968f962d63d070400af2e346"}, - {file = "dash_core_components-2.0.0.tar.gz", hash = "sha256:c6733874af975e552f95a1398a16c2ee7df14ce43fa60bb3718a3c6e0b63ffee"}, -] - -[[package]] -name = "dash-html-components" -version = "2.0.0" -description = "Vanilla HTML components for Dash" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "dash_html_components-2.0.0-py3-none-any.whl", hash = "sha256:b42cc903713c9706af03b3f2548bda4be7307a7cf89b7d6eae3da872717d1b63"}, - {file = "dash_html_components-2.0.0.tar.gz", hash = "sha256:8703a601080f02619a6390998e0b3da4a5daabe97a1fd7a9cebc09d015f26e50"}, -] - -[[package]] -name = "dash-table" -version = "5.0.0" -description = "Dash table" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "dash_table-5.0.0-py3-none-any.whl", hash = "sha256:19036fa352bb1c11baf38068ec62d172f0515f73ca3276c79dee49b95ddc16c9"}, - {file = "dash_table-5.0.0.tar.gz", hash = "sha256:18624d693d4c8ef2ddec99a6f167593437a7ea0bf153aa20f318c170c5bc7308"}, -] +pandas = ["numpy (>=2.0.2)", "pandas (>=2.2.3)"] [[package]] name = "decorator" @@ -1921,14 +1941,14 @@ files = [ [[package]] name = "durationpy" -version = "0.9" +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.9-py3-none-any.whl", hash = "sha256:e65359a7af5cedad07fb77a2dd3f390f8eb0b74cb845589fa6c057086834dd38"}, - {file = "durationpy-0.9.tar.gz", hash = "sha256:fd3feb0a69a0057d582ef643c355c40d2fa1c942191f914d12203b1a01ac722a"}, + {file = "durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286"}, + {file = "durationpy-0.10.tar.gz", hash = "sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba"}, ] [[package]] @@ -1949,17 +1969,20 @@ idna = ">=2.0.0" [[package]] name = "exceptiongroup" -version = "1.2.2" +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.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, - {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, + {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)"] @@ -2014,23 +2037,24 @@ pyflakes = ">=3.2.0,<3.3.0" [[package]] name = "flask" -version = "3.0.3" +version = "3.1.1" description = "A simple framework for building complex web applications." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "flask-3.0.3-py3-none-any.whl", hash = "sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3"}, - {file = "flask-3.0.3.tar.gz", hash = "sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842"}, + {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.6.2" +blinker = ">=1.9.0" click = ">=8.1.3" importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""} -itsdangerous = ">=2.1.2" -Jinja2 = ">=3.1.2" -Werkzeug = ">=3.0.0" +itsdangerous = ">=2.2.0" +jinja2 = ">=3.1.2" +markupsafe = ">=2.1.1" +werkzeug = ">=3.1.0" [package.extras] async = ["asgiref (>=3.2)"] @@ -2053,116 +2077,116 @@ python-dateutil = ">=2.7" [[package]] name = "frozenlist" -version = "1.6.0" +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.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e6e558ea1e47fd6fa8ac9ccdad403e5dd5ecc6ed8dda94343056fa4277d5c65e"}, - {file = "frozenlist-1.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f4b3cd7334a4bbc0c472164f3744562cb72d05002cc6fcf58adb104630bbc352"}, - {file = "frozenlist-1.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9799257237d0479736e2b4c01ff26b5c7f7694ac9692a426cb717f3dc02fff9b"}, - {file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a7bb0fe1f7a70fb5c6f497dc32619db7d2cdd53164af30ade2f34673f8b1fc"}, - {file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:36d2fc099229f1e4237f563b2a3e0ff7ccebc3999f729067ce4e64a97a7f2869"}, - {file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f27a9f9a86dcf00708be82359db8de86b80d029814e6693259befe82bb58a106"}, - {file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75ecee69073312951244f11b8627e3700ec2bfe07ed24e3a685a5979f0412d24"}, - {file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2c7d5aa19714b1b01a0f515d078a629e445e667b9da869a3cd0e6fe7dec78bd"}, - {file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69bbd454f0fb23b51cadc9bdba616c9678e4114b6f9fa372d462ff2ed9323ec8"}, - {file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7daa508e75613809c7a57136dec4871a21bca3080b3a8fc347c50b187df4f00c"}, - {file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:89ffdb799154fd4d7b85c56d5fa9d9ad48946619e0eb95755723fffa11022d75"}, - {file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:920b6bd77d209931e4c263223381d63f76828bec574440f29eb497cf3394c249"}, - {file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d3ceb265249fb401702fce3792e6b44c1166b9319737d21495d3611028d95769"}, - {file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:52021b528f1571f98a7d4258c58aa8d4b1a96d4f01d00d51f1089f2e0323cb02"}, - {file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0f2ca7810b809ed0f1917293050163c7654cefc57a49f337d5cd9de717b8fad3"}, - {file = "frozenlist-1.6.0-cp310-cp310-win32.whl", hash = "sha256:0e6f8653acb82e15e5443dba415fb62a8732b68fe09936bb6d388c725b57f812"}, - {file = "frozenlist-1.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:f1a39819a5a3e84304cd286e3dc62a549fe60985415851b3337b6f5cc91907f1"}, - {file = "frozenlist-1.6.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ae8337990e7a45683548ffb2fee1af2f1ed08169284cd829cdd9a7fa7470530d"}, - {file = "frozenlist-1.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8c952f69dd524558694818a461855f35d36cc7f5c0adddce37e962c85d06eac0"}, - {file = "frozenlist-1.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8f5fef13136c4e2dee91bfb9a44e236fff78fc2cd9f838eddfc470c3d7d90afe"}, - {file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:716bbba09611b4663ecbb7cd022f640759af8259e12a6ca939c0a6acd49eedba"}, - {file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7b8c4dc422c1a3ffc550b465090e53b0bf4839047f3e436a34172ac67c45d595"}, - {file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b11534872256e1666116f6587a1592ef395a98b54476addb5e8d352925cb5d4a"}, - {file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c6eceb88aaf7221f75be6ab498dc622a151f5f88d536661af3ffc486245a626"}, - {file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62c828a5b195570eb4b37369fcbbd58e96c905768d53a44d13044355647838ff"}, - {file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1c6bd2c6399920c9622362ce95a7d74e7f9af9bfec05fff91b8ce4b9647845a"}, - {file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:49ba23817781e22fcbd45fd9ff2b9b8cdb7b16a42a4851ab8025cae7b22e96d0"}, - {file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:431ef6937ae0f853143e2ca67d6da76c083e8b1fe3df0e96f3802fd37626e606"}, - {file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9d124b38b3c299ca68433597ee26b7819209cb8a3a9ea761dfe9db3a04bba584"}, - {file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:118e97556306402e2b010da1ef21ea70cb6d6122e580da64c056b96f524fbd6a"}, - {file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fb3b309f1d4086b5533cf7bbcf3f956f0ae6469664522f1bde4feed26fba60f1"}, - {file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54dece0d21dce4fdb188a1ffc555926adf1d1c516e493c2914d7c370e454bc9e"}, - {file = "frozenlist-1.6.0-cp311-cp311-win32.whl", hash = "sha256:654e4ba1d0b2154ca2f096bed27461cf6160bc7f504a7f9a9ef447c293caf860"}, - {file = "frozenlist-1.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e911391bffdb806001002c1f860787542f45916c3baf764264a52765d5a5603"}, - {file = "frozenlist-1.6.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c5b9e42ace7d95bf41e19b87cec8f262c41d3510d8ad7514ab3862ea2197bfb1"}, - {file = "frozenlist-1.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ca9973735ce9f770d24d5484dcb42f68f135351c2fc81a7a9369e48cf2998a29"}, - {file = "frozenlist-1.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6ac40ec76041c67b928ca8aaffba15c2b2ee3f5ae8d0cb0617b5e63ec119ca25"}, - {file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b7a8a3180dfb280eb044fdec562f9b461614c0ef21669aea6f1d3dac6ee576"}, - {file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c444d824e22da6c9291886d80c7d00c444981a72686e2b59d38b285617cb52c8"}, - {file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb52c8166499a8150bfd38478248572c924c003cbb45fe3bcd348e5ac7c000f9"}, - {file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b35298b2db9c2468106278537ee529719228950a5fdda686582f68f247d1dc6e"}, - {file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d108e2d070034f9d57210f22fefd22ea0d04609fc97c5f7f5a686b3471028590"}, - {file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e1be9111cb6756868ac242b3c2bd1f09d9aea09846e4f5c23715e7afb647103"}, - {file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:94bb451c664415f02f07eef4ece976a2c65dcbab9c2f1705b7031a3a75349d8c"}, - {file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d1a686d0b0949182b8faddea596f3fc11f44768d1f74d4cad70213b2e139d821"}, - {file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ea8e59105d802c5a38bdbe7362822c522230b3faba2aa35c0fa1765239b7dd70"}, - {file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:abc4e880a9b920bc5020bf6a431a6bb40589d9bca3975c980495f63632e8382f"}, - {file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9a79713adfe28830f27a3c62f6b5406c37376c892b05ae070906f07ae4487046"}, - {file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a0318c2068e217a8f5e3b85e35899f5a19e97141a45bb925bb357cfe1daf770"}, - {file = "frozenlist-1.6.0-cp312-cp312-win32.whl", hash = "sha256:853ac025092a24bb3bf09ae87f9127de9fe6e0c345614ac92536577cf956dfcc"}, - {file = "frozenlist-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:2bdfe2d7e6c9281c6e55523acd6c2bf77963cb422fdc7d142fb0cb6621b66878"}, - {file = "frozenlist-1.6.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1d7fb014fe0fbfee3efd6a94fc635aeaa68e5e1720fe9e57357f2e2c6e1a647e"}, - {file = "frozenlist-1.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01bcaa305a0fdad12745502bfd16a1c75b14558dabae226852f9159364573117"}, - {file = "frozenlist-1.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b314faa3051a6d45da196a2c495e922f987dc848e967d8cfeaee8a0328b1cd4"}, - {file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da62fecac21a3ee10463d153549d8db87549a5e77eefb8c91ac84bb42bb1e4e3"}, - {file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1eb89bf3454e2132e046f9599fbcf0a4483ed43b40f545551a39316d0201cd1"}, - {file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18689b40cb3936acd971f663ccb8e2589c45db5e2c5f07e0ec6207664029a9c"}, - {file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e67ddb0749ed066b1a03fba812e2dcae791dd50e5da03be50b6a14d0c1a9ee45"}, - {file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc5e64626e6682638d6e44398c9baf1d6ce6bc236d40b4b57255c9d3f9761f1f"}, - {file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:437cfd39564744ae32ad5929e55b18ebd88817f9180e4cc05e7d53b75f79ce85"}, - {file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:62dd7df78e74d924952e2feb7357d826af8d2f307557a779d14ddf94d7311be8"}, - {file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a66781d7e4cddcbbcfd64de3d41a61d6bdde370fc2e38623f30b2bd539e84a9f"}, - {file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:482fe06e9a3fffbcd41950f9d890034b4a54395c60b5e61fae875d37a699813f"}, - {file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e4f9373c500dfc02feea39f7a56e4f543e670212102cc2eeb51d3a99c7ffbde6"}, - {file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e69bb81de06827147b7bfbaeb284d85219fa92d9f097e32cc73675f279d70188"}, - {file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7613d9977d2ab4a9141dde4a149f4357e4065949674c5649f920fec86ecb393e"}, - {file = "frozenlist-1.6.0-cp313-cp313-win32.whl", hash = "sha256:4def87ef6d90429f777c9d9de3961679abf938cb6b7b63d4a7eb8a268babfce4"}, - {file = "frozenlist-1.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:37a8a52c3dfff01515e9bbbee0e6063181362f9de3db2ccf9bc96189b557cbfd"}, - {file = "frozenlist-1.6.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:46138f5a0773d064ff663d273b309b696293d7a7c00a0994c5c13a5078134b64"}, - {file = "frozenlist-1.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f88bc0a2b9c2a835cb888b32246c27cdab5740059fb3688852bf91e915399b91"}, - {file = "frozenlist-1.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:777704c1d7655b802c7850255639672e90e81ad6fa42b99ce5ed3fbf45e338dd"}, - {file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85ef8d41764c7de0dcdaf64f733a27352248493a85a80661f3c678acd27e31f2"}, - {file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:da5cb36623f2b846fb25009d9d9215322318ff1c63403075f812b3b2876c8506"}, - {file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cbb56587a16cf0fb8acd19e90ff9924979ac1431baea8681712716a8337577b0"}, - {file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6154c3ba59cda3f954c6333025369e42c3acd0c6e8b6ce31eb5c5b8116c07e0"}, - {file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e8246877afa3f1ae5c979fe85f567d220f86a50dc6c493b9b7d8191181ae01e"}, - {file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b0f6cce16306d2e117cf9db71ab3a9e8878a28176aeaf0dbe35248d97b28d0c"}, - {file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1b8e8cd8032ba266f91136d7105706ad57770f3522eac4a111d77ac126a25a9b"}, - {file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:e2ada1d8515d3ea5378c018a5f6d14b4994d4036591a52ceaf1a1549dec8e1ad"}, - {file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:cdb2c7f071e4026c19a3e32b93a09e59b12000751fc9b0b7758da899e657d215"}, - {file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:03572933a1969a6d6ab509d509e5af82ef80d4a5d4e1e9f2e1cdd22c77a3f4d2"}, - {file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:77effc978947548b676c54bbd6a08992759ea6f410d4987d69feea9cd0919911"}, - {file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a2bda8be77660ad4089caf2223fdbd6db1858462c4b85b67fbfa22102021e497"}, - {file = "frozenlist-1.6.0-cp313-cp313t-win32.whl", hash = "sha256:a4d96dc5bcdbd834ec6b0f91027817214216b5b30316494d2b1aebffb87c534f"}, - {file = "frozenlist-1.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e18036cb4caa17ea151fd5f3d70be9d354c99eb8cf817a3ccde8a7873b074348"}, - {file = "frozenlist-1.6.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:536a1236065c29980c15c7229fbb830dedf809708c10e159b8136534233545f0"}, - {file = "frozenlist-1.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ed5e3a4462ff25ca84fb09e0fada8ea267df98a450340ead4c91b44857267d70"}, - {file = "frozenlist-1.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e19c0fc9f4f030fcae43b4cdec9e8ab83ffe30ec10c79a4a43a04d1af6c5e1ad"}, - {file = "frozenlist-1.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7c608f833897501dac548585312d73a7dca028bf3b8688f0d712b7acfaf7fb3"}, - {file = "frozenlist-1.6.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0dbae96c225d584f834b8d3cc688825911960f003a85cb0fd20b6e5512468c42"}, - {file = "frozenlist-1.6.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:625170a91dd7261a1d1c2a0c1a353c9e55d21cd67d0852185a5fef86587e6f5f"}, - {file = "frozenlist-1.6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1db8b2fc7ee8a940b547a14c10e56560ad3ea6499dc6875c354e2335812f739d"}, - {file = "frozenlist-1.6.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4da6fc43048b648275a220e3a61c33b7fff65d11bdd6dcb9d9c145ff708b804c"}, - {file = "frozenlist-1.6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef8e7e8f2f3820c5f175d70fdd199b79e417acf6c72c5d0aa8f63c9f721646f"}, - {file = "frozenlist-1.6.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aa733d123cc78245e9bb15f29b44ed9e5780dc6867cfc4e544717b91f980af3b"}, - {file = "frozenlist-1.6.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ba7f8d97152b61f22d7f59491a781ba9b177dd9f318486c5fbc52cde2db12189"}, - {file = "frozenlist-1.6.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:56a0b8dd6d0d3d971c91f1df75e824986667ccce91e20dca2023683814344791"}, - {file = "frozenlist-1.6.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:5c9e89bf19ca148efcc9e3c44fd4c09d5af85c8a7dd3dbd0da1cb83425ef4983"}, - {file = "frozenlist-1.6.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:1330f0a4376587face7637dfd245380a57fe21ae8f9d360c1c2ef8746c4195fa"}, - {file = "frozenlist-1.6.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2187248203b59625566cac53572ec8c2647a140ee2738b4e36772930377a533c"}, - {file = "frozenlist-1.6.0-cp39-cp39-win32.whl", hash = "sha256:2b8cf4cfea847d6c12af06091561a89740f1f67f331c3fa8623391905e878530"}, - {file = "frozenlist-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:1255d5d64328c5a0d066ecb0f02034d086537925f1f04b50b1ae60d37afbf572"}, - {file = "frozenlist-1.6.0-py3-none-any.whl", hash = "sha256:535eec9987adb04701266b92745d6cdcef2e77669299359c3009c3404dd5d191"}, - {file = "frozenlist-1.6.0.tar.gz", hash = "sha256:b99655c32c1c8e06d111e7f41c06c29a5318cb1835df23a45518e02a47c63b68"}, + {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]] @@ -2219,14 +2243,14 @@ test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock ; python_version < \"3. [[package]] name = "google-api-core" -version = "2.24.2" +version = "2.25.1" description = "Google API client core library" optional = false python-versions = ">=3.7" groups = ["main"] files = [ - {file = "google_api_core-2.24.2-py3-none-any.whl", hash = "sha256:810a63ac95f3c441b7c0e43d344e372887f62ce9071ba972eacf32672e072de9"}, - {file = "google_api_core-2.24.2.tar.gz", hash = "sha256:81718493daf06d96d6bc76a91c23874dbf2fac0adbbf542831b805ee6e974696"}, + {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] @@ -2237,10 +2261,10 @@ protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4 requests = ">=2.18.0,<3.0.0" [package.extras] -async-rest = ["google-auth[aiohttp] (>=2.35.0,<3.0.dev0)"] -grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio (>=1.49.1,<2.0dev) ; python_version >= \"3.11\"", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0) ; python_version >= \"3.11\""] -grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] -grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] +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" @@ -2263,14 +2287,14 @@ uritemplate = ">=3.0.1,<5" [[package]] name = "google-auth" -version = "2.39.0" +version = "2.40.3" description = "Google Authentication Library" optional = false python-versions = ">=3.7" groups = ["main"] files = [ - {file = "google_auth-2.39.0-py2.py3-none-any.whl", hash = "sha256:0150b6711e97fb9f52fe599f55648950cc4540015565d8fbb31be2ad6e1548a2"}, - {file = "google_auth-2.39.0.tar.gz", hash = "sha256:73222d43cdc35a3aeacbfdcaf73142a97839f10de930550d89ebfe1d0a00cde7"}, + {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] @@ -2468,14 +2492,14 @@ files = [ [[package]] name = "iamdata" -version = "0.1.202505091" +version = "0.1.202506261" 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.202505091-py3-none-any.whl", hash = "sha256:006536cb6ee32c0567e04ba7582586596c67b599ab0ed10166efae4153acf120"}, - {file = "iamdata-0.1.202505091.tar.gz", hash = "sha256:b8538dde82282e89c5ca6e98662008e3c50e21252e3a3a532f0c1b7387488036"}, + {file = "iamdata-0.1.202506261-py3-none-any.whl", hash = "sha256:800817bce22c50efdbc5556b4849dcef89b8d42a56ee0b175fd7ac56e397f2d0"}, + {file = "iamdata-0.1.202506261.tar.gz", hash = "sha256:22e23aebb3fa103951a60f1025fe201f3501327aa522a1a7ce33c3873473da28"}, ] [[package]] @@ -2613,14 +2637,14 @@ files = [ [[package]] name = "joserfc" -version = "1.0.4" +version = "1.1.0" 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.0.4-py3-none-any.whl", hash = "sha256:ecf3a5999f89d3a663485ab7c4f633541586d6f44e664ee760197299f39ed51b"}, - {file = "joserfc-1.0.4.tar.gz", hash = "sha256:dc3fc216cfcfc952d4c0d4b06c759a04711af0b667e5973adc47dbb1ba784127"}, + {file = "joserfc-1.1.0-py3-none-any.whl", hash = "sha256:9493512cfffb9bc3001e8f609fe0eb7e95b71f3d3b374ede93de94b4b6b520f5"}, + {file = "joserfc-1.1.0.tar.gz", hash = "sha256:a8f3442b04c233f742f7acde0d0dcd926414e9542a6337096b2b4e5f435f36c1"}, ] [package.dependencies] @@ -2714,14 +2738,14 @@ requests = ">=2.31.0,<3.0.0" [[package]] name = "jsonschema-specifications" -version = "2024.10.1" +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-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf"}, - {file = "jsonschema_specifications-2024.10.1.tar.gz", hash = "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272"}, + {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] @@ -2814,33 +2838,32 @@ files = [ [[package]] name = "license-expression" -version = "30.4.1" +version = "30.4.3" description = "license-expression is a comprehensive utility library to parse, compare, simplify and normalize license expressions (such as SPDX license expressions) using boolean logic." optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "license_expression-30.4.1-py3-none-any.whl", hash = "sha256:679646bc3261a17690494a3e1cada446e5ee342dbd87dcfa4a0c24cc5dce13ee"}, - {file = "license_expression-30.4.1.tar.gz", hash = "sha256:9f02105f9e0fcecba6a85dfbbed7d94ea1c3a70cf23ddbfb5adf3438a6f6fce0"}, + {file = "license_expression-30.4.3-py3-none-any.whl", hash = "sha256:fd3db53418133e0eef917606623bc125fbad3d1225ba8d23950999ee87c99280"}, + {file = "license_expression-30.4.3.tar.gz", hash = "sha256:49f439fea91c4d1a642f9f2902b58db1d42396c5e331045f41ce50df9b40b1f2"}, ] [package.dependencies] "boolean.py" = ">=4.0" [package.extras] -docs = ["Sphinx (>=5.0.2)", "doc8 (>=0.11.2)", "sphinx-autobuild", "sphinx-copybutton", "sphinx-reredirects (>=0.1.2)", "sphinx-rtd-dark-mode (>=1.3.0)", "sphinx-rtd-theme (>=1.0.0)", "sphinxcontrib-apidoc (>=0.4.0)"] -testing = ["black", "isort", "pytest (>=6,!=7.0.0)", "pytest-xdist (>=2)", "twine"] +dev = ["Sphinx (>=5.0.2)", "doc8 (>=0.11.2)", "pytest (>=7.0.1)", "pytest-xdist (>=2)", "ruff", "sphinx-autobuild", "sphinx-copybutton", "sphinx-reredirects (>=0.1.2)", "sphinx-rtd-dark-mode (>=1.3.0)", "sphinx-rtd-theme (>=1.0.0)", "sphinxcontrib-apidoc (>=0.4.0)", "twine"] [[package]] name = "markdown" -version = "3.8" +version = "3.8.2" description = "Python implementation of John Gruber's Markdown." optional = false python-versions = ">=3.9" groups = ["main", "docs"] files = [ - {file = "markdown-3.8-py3-none-any.whl", hash = "sha256:794a929b79c5af141ef5ab0f2f642d0f7b1872981250230e72682346f7cc90dc"}, - {file = "markdown-3.8.tar.gz", hash = "sha256:7df81e63f0df5c4b24b7d156eb81e4690595239b7d70937d0409f1b0de319c6f"}, + {file = "markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24"}, + {file = "markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45"}, ] [package.dependencies] @@ -3323,14 +3346,14 @@ tests = ["pytest (>=4.6)"] [[package]] name = "msal" -version = "1.32.0" +version = "1.32.3" 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.32.0-py3-none-any.whl", hash = "sha256:9dbac5384a10bbbf4dae5c7ea0d707d14e087b92c5aa4954b3feaa2d1aa0bcb7"}, - {file = "msal-1.32.0.tar.gz", hash = "sha256:5445fe3af1da6be484991a7ab32eaa82461dc2347de105b76af92c610c3335c2"}, + {file = "msal-1.32.3-py3-none-any.whl", hash = "sha256:b2798db57760b1961b142f027ffb7c8169536bf77316e99a0df5c4aaebb11569"}, + {file = "msal-1.32.3.tar.gz", hash = "sha256:5eea038689c78a5a70ca8ecbe1245458b55a857bd096efb6989c69ba15985d35"}, ] [package.dependencies] @@ -3361,14 +3384,14 @@ portalocker = ["portalocker (>=1.4,<4)"] [[package]] name = "msgraph-core" -version = "1.3.3" +version = "1.3.4" description = "Core component of the Microsoft Graph Python SDK" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "msgraph_core-1.3.3-py3-none-any.whl", hash = "sha256:9dbbc0c88e174c1d5da1c17d286965d6b26ebaf24996c7af64a39e2069006cf6"}, - {file = "msgraph_core-1.3.3.tar.gz", hash = "sha256:a3226b08b4cf9b6dbb16b868be21d5f82d8ee514ae8e46d9f0cad896159ef8d3"}, + {file = "msgraph_core-1.3.4-py3-none-any.whl", hash = "sha256:bce306ce193f8f96b3dd32af8b6ffd28eaaaf0db7560bfac8a334158e616c821"}, + {file = "msgraph_core-1.3.4.tar.gz", hash = "sha256:27e0d95344d29b6f05cf6ceb24a28bac363f19f18d6efaed7af6457e9fe4d6a9"}, ] [package.dependencies] @@ -3427,116 +3450,110 @@ async = ["aiodns ; python_version >= \"3.5\"", "aiohttp (>=3.0) ; python_version [[package]] name = "multidict" -version = "6.4.3" +version = "6.5.1" description = "multidict implementation" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "multidict-6.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32a998bd8a64ca48616eac5a8c1cc4fa38fb244a3facf2eeb14abe186e0f6cc5"}, - {file = "multidict-6.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a54ec568f1fc7f3c313c2f3b16e5db346bf3660e1309746e7fccbbfded856188"}, - {file = "multidict-6.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a7be07e5df178430621c716a63151165684d3e9958f2bbfcb644246162007ab7"}, - {file = "multidict-6.4.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b128dbf1c939674a50dd0b28f12c244d90e5015e751a4f339a96c54f7275e291"}, - {file = "multidict-6.4.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b9cb19dfd83d35b6ff24a4022376ea6e45a2beba8ef3f0836b8a4b288b6ad685"}, - {file = "multidict-6.4.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3cf62f8e447ea2c1395afa289b332e49e13d07435369b6f4e41f887db65b40bf"}, - {file = "multidict-6.4.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:909f7d43ff8f13d1adccb6a397094adc369d4da794407f8dd592c51cf0eae4b1"}, - {file = "multidict-6.4.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0bb8f8302fbc7122033df959e25777b0b7659b1fd6bcb9cb6bed76b5de67afef"}, - {file = "multidict-6.4.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:224b79471b4f21169ea25ebc37ed6f058040c578e50ade532e2066562597b8a9"}, - {file = "multidict-6.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a7bd27f7ab3204f16967a6f899b3e8e9eb3362c0ab91f2ee659e0345445e0078"}, - {file = "multidict-6.4.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:99592bd3162e9c664671fd14e578a33bfdba487ea64bcb41d281286d3c870ad7"}, - {file = "multidict-6.4.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a62d78a1c9072949018cdb05d3c533924ef8ac9bcb06cbf96f6d14772c5cd451"}, - {file = "multidict-6.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:3ccdde001578347e877ca4f629450973c510e88e8865d5aefbcb89b852ccc666"}, - {file = "multidict-6.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:eccb67b0e78aa2e38a04c5ecc13bab325a43e5159a181a9d1a6723db913cbb3c"}, - {file = "multidict-6.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8b6fcf6054fc4114a27aa865f8840ef3d675f9316e81868e0ad5866184a6cba5"}, - {file = "multidict-6.4.3-cp310-cp310-win32.whl", hash = "sha256:f92c7f62d59373cd93bc9969d2da9b4b21f78283b1379ba012f7ee8127b3152e"}, - {file = "multidict-6.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:b57e28dbc031d13916b946719f213c494a517b442d7b48b29443e79610acd887"}, - {file = "multidict-6.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f6f19170197cc29baccd33ccc5b5d6a331058796485857cf34f7635aa25fb0cd"}, - {file = "multidict-6.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f2882bf27037eb687e49591690e5d491e677272964f9ec7bc2abbe09108bdfb8"}, - {file = "multidict-6.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fbf226ac85f7d6b6b9ba77db4ec0704fde88463dc17717aec78ec3c8546c70ad"}, - {file = "multidict-6.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e329114f82ad4b9dd291bef614ea8971ec119ecd0f54795109976de75c9a852"}, - {file = "multidict-6.4.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1f4e0334d7a555c63f5c8952c57ab6f1c7b4f8c7f3442df689fc9f03df315c08"}, - {file = "multidict-6.4.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:740915eb776617b57142ce0bb13b7596933496e2f798d3d15a20614adf30d229"}, - {file = "multidict-6.4.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255dac25134d2b141c944b59a0d2f7211ca12a6d4779f7586a98b4b03ea80508"}, - {file = "multidict-6.4.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4e8535bd4d741039b5aad4285ecd9b902ef9e224711f0b6afda6e38d7ac02c7"}, - {file = "multidict-6.4.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c433a33be000dd968f5750722eaa0991037be0be4a9d453eba121774985bc8"}, - {file = "multidict-6.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4eb33b0bdc50acd538f45041f5f19945a1f32b909b76d7b117c0c25d8063df56"}, - {file = "multidict-6.4.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:75482f43465edefd8a5d72724887ccdcd0c83778ded8f0cb1e0594bf71736cc0"}, - {file = "multidict-6.4.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ce5b3082e86aee80b3925ab4928198450d8e5b6466e11501fe03ad2191c6d777"}, - {file = "multidict-6.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e413152e3212c4d39f82cf83c6f91be44bec9ddea950ce17af87fbf4e32ca6b2"}, - {file = "multidict-6.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:8aac2eeff69b71f229a405c0a4b61b54bade8e10163bc7b44fcd257949620618"}, - {file = "multidict-6.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ab583ac203af1d09034be41458feeab7863c0635c650a16f15771e1386abf2d7"}, - {file = "multidict-6.4.3-cp311-cp311-win32.whl", hash = "sha256:1b2019317726f41e81154df636a897de1bfe9228c3724a433894e44cd2512378"}, - {file = "multidict-6.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:43173924fa93c7486402217fab99b60baf78d33806af299c56133a3755f69589"}, - {file = "multidict-6.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1f1c2f58f08b36f8475f3ec6f5aeb95270921d418bf18f90dffd6be5c7b0e676"}, - {file = "multidict-6.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:26ae9ad364fc61b936fb7bf4c9d8bd53f3a5b4417142cd0be5c509d6f767e2f1"}, - {file = "multidict-6.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:659318c6c8a85f6ecfc06b4e57529e5a78dfdd697260cc81f683492ad7e9435a"}, - {file = "multidict-6.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1eb72c741fd24d5a28242ce72bb61bc91f8451877131fa3fe930edb195f7054"}, - {file = "multidict-6.4.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3cd06d88cb7398252284ee75c8db8e680aa0d321451132d0dba12bc995f0adcc"}, - {file = "multidict-6.4.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4543d8dc6470a82fde92b035a92529317191ce993533c3c0c68f56811164ed07"}, - {file = "multidict-6.4.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:30a3ebdc068c27e9d6081fca0e2c33fdf132ecea703a72ea216b81a66860adde"}, - {file = "multidict-6.4.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b038f10e23f277153f86f95c777ba1958bcd5993194fda26a1d06fae98b2f00c"}, - {file = "multidict-6.4.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c605a2b2dc14282b580454b9b5d14ebe0668381a3a26d0ac39daa0ca115eb2ae"}, - {file = "multidict-6.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8bd2b875f4ca2bb527fe23e318ddd509b7df163407b0fb717df229041c6df5d3"}, - {file = "multidict-6.4.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c2e98c840c9c8e65c0e04b40c6c5066c8632678cd50c8721fdbcd2e09f21a507"}, - {file = "multidict-6.4.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:66eb80dd0ab36dbd559635e62fba3083a48a252633164857a1d1684f14326427"}, - {file = "multidict-6.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c23831bdee0a2a3cf21be057b5e5326292f60472fb6c6f86392bbf0de70ba731"}, - {file = "multidict-6.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1535cec6443bfd80d028052e9d17ba6ff8a5a3534c51d285ba56c18af97e9713"}, - {file = "multidict-6.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3b73e7227681f85d19dec46e5b881827cd354aabe46049e1a61d2f9aaa4e285a"}, - {file = "multidict-6.4.3-cp312-cp312-win32.whl", hash = "sha256:8eac0c49df91b88bf91f818e0a24c1c46f3622978e2c27035bfdca98e0e18124"}, - {file = "multidict-6.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:11990b5c757d956cd1db7cb140be50a63216af32cd6506329c2c59d732d802db"}, - {file = "multidict-6.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a76534263d03ae0cfa721fea40fd2b5b9d17a6f85e98025931d41dc49504474"}, - {file = "multidict-6.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:805031c2f599eee62ac579843555ed1ce389ae00c7e9f74c2a1b45e0564a88dd"}, - {file = "multidict-6.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c56c179839d5dcf51d565132185409d1d5dd8e614ba501eb79023a6cab25576b"}, - {file = "multidict-6.4.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c64f4ddb3886dd8ab71b68a7431ad4aa01a8fa5be5b11543b29674f29ca0ba3"}, - {file = "multidict-6.4.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3002a856367c0b41cad6784f5b8d3ab008eda194ed7864aaa58f65312e2abcac"}, - {file = "multidict-6.4.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3d75e621e7d887d539d6e1d789f0c64271c250276c333480a9e1de089611f790"}, - {file = "multidict-6.4.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:995015cf4a3c0d72cbf453b10a999b92c5629eaf3a0c3e1efb4b5c1f602253bb"}, - {file = "multidict-6.4.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2b0fabae7939d09d7d16a711468c385272fa1b9b7fb0d37e51143585d8e72e0"}, - {file = "multidict-6.4.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:61ed4d82f8a1e67eb9eb04f8587970d78fe7cddb4e4d6230b77eda23d27938f9"}, - {file = "multidict-6.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:062428944a8dc69df9fdc5d5fc6279421e5f9c75a9ee3f586f274ba7b05ab3c8"}, - {file = "multidict-6.4.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b90e27b4674e6c405ad6c64e515a505c6d113b832df52fdacb6b1ffd1fa9a1d1"}, - {file = "multidict-6.4.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7d50d4abf6729921e9613d98344b74241572b751c6b37feed75fb0c37bd5a817"}, - {file = "multidict-6.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:43fe10524fb0a0514be3954be53258e61d87341008ce4914f8e8b92bee6f875d"}, - {file = "multidict-6.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:236966ca6c472ea4e2d3f02f6673ebfd36ba3f23159c323f5a496869bc8e47c9"}, - {file = "multidict-6.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:422a5ec315018e606473ba1f5431e064cf8b2a7468019233dcf8082fabad64c8"}, - {file = "multidict-6.4.3-cp313-cp313-win32.whl", hash = "sha256:f901a5aace8e8c25d78960dcc24c870c8d356660d3b49b93a78bf38eb682aac3"}, - {file = "multidict-6.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:1c152c49e42277bc9a2f7b78bd5fa10b13e88d1b0328221e7aef89d5c60a99a5"}, - {file = "multidict-6.4.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:be8751869e28b9c0d368d94f5afcb4234db66fe8496144547b4b6d6a0645cfc6"}, - {file = "multidict-6.4.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d4b31f8a68dccbcd2c0ea04f0e014f1defc6b78f0eb8b35f2265e8716a6df0c"}, - {file = "multidict-6.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:032efeab3049e37eef2ff91271884303becc9e54d740b492a93b7e7266e23756"}, - {file = "multidict-6.4.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e78006af1a7c8a8007e4f56629d7252668344442f66982368ac06522445e375"}, - {file = "multidict-6.4.3-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:daeac9dd30cda8703c417e4fddccd7c4dc0c73421a0b54a7da2713be125846be"}, - {file = "multidict-6.4.3-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f6f90700881438953eae443a9c6f8a509808bc3b185246992c4233ccee37fea"}, - {file = "multidict-6.4.3-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f84627997008390dd15762128dcf73c3365f4ec0106739cde6c20a07ed198ec8"}, - {file = "multidict-6.4.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3307b48cd156153b117c0ea54890a3bdbf858a5b296ddd40dc3852e5f16e9b02"}, - {file = "multidict-6.4.3-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ead46b0fa1dcf5af503a46e9f1c2e80b5d95c6011526352fa5f42ea201526124"}, - {file = "multidict-6.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1748cb2743bedc339d63eb1bca314061568793acd603a6e37b09a326334c9f44"}, - {file = "multidict-6.4.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:acc9fa606f76fc111b4569348cc23a771cb52c61516dcc6bcef46d612edb483b"}, - {file = "multidict-6.4.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:31469d5832b5885adeb70982e531ce86f8c992334edd2f2254a10fa3182ac504"}, - {file = "multidict-6.4.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ba46b51b6e51b4ef7bfb84b82f5db0dc5e300fb222a8a13b8cd4111898a869cf"}, - {file = "multidict-6.4.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:389cfefb599edf3fcfd5f64c0410da686f90f5f5e2c4d84e14f6797a5a337af4"}, - {file = "multidict-6.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:64bc2bbc5fba7b9db5c2c8d750824f41c6994e3882e6d73c903c2afa78d091e4"}, - {file = "multidict-6.4.3-cp313-cp313t-win32.whl", hash = "sha256:0ecdc12ea44bab2807d6b4a7e5eef25109ab1c82a8240d86d3c1fc9f3b72efd5"}, - {file = "multidict-6.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7146a8742ea71b5d7d955bffcef58a9e6e04efba704b52a460134fefd10a8208"}, - {file = "multidict-6.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5427a2679e95a642b7f8b0f761e660c845c8e6fe3141cddd6b62005bd133fc21"}, - {file = "multidict-6.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:24a8caa26521b9ad09732972927d7b45b66453e6ebd91a3c6a46d811eeb7349b"}, - {file = "multidict-6.4.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6b5a272bc7c36a2cd1b56ddc6bff02e9ce499f9f14ee4a45c45434ef083f2459"}, - {file = "multidict-6.4.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edf74dc5e212b8c75165b435c43eb0d5e81b6b300a938a4eb82827119115e840"}, - {file = "multidict-6.4.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9f35de41aec4b323c71f54b0ca461ebf694fb48bec62f65221f52e0017955b39"}, - {file = "multidict-6.4.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae93e0ff43b6f6892999af64097b18561691ffd835e21a8348a441e256592e1f"}, - {file = "multidict-6.4.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e3929269e9d7eff905d6971d8b8c85e7dbc72c18fb99c8eae6fe0a152f2e343"}, - {file = "multidict-6.4.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb6214fe1750adc2a1b801a199d64b5a67671bf76ebf24c730b157846d0e90d2"}, - {file = "multidict-6.4.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d79cf5c0c6284e90f72123f4a3e4add52d6c6ebb4a9054e88df15b8d08444c6"}, - {file = "multidict-6.4.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2427370f4a255262928cd14533a70d9738dfacadb7563bc3b7f704cc2360fc4e"}, - {file = "multidict-6.4.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:fbd8d737867912b6c5f99f56782b8cb81f978a97b4437a1c476de90a3e41c9a1"}, - {file = "multidict-6.4.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0ee1bf613c448997f73fc4efb4ecebebb1c02268028dd4f11f011f02300cf1e8"}, - {file = "multidict-6.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:578568c4ba5f2b8abd956baf8b23790dbfdc953e87d5b110bce343b4a54fc9e7"}, - {file = "multidict-6.4.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:a059ad6b80de5b84b9fa02a39400319e62edd39d210b4e4f8c4f1243bdac4752"}, - {file = "multidict-6.4.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:dd53893675b729a965088aaadd6a1f326a72b83742b056c1065bdd2e2a42b4df"}, - {file = "multidict-6.4.3-cp39-cp39-win32.whl", hash = "sha256:abcfed2c4c139f25c2355e180bcc077a7cae91eefbb8b3927bb3f836c9586f1f"}, - {file = "multidict-6.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:b1b389ae17296dd739015d5ddb222ee99fd66adeae910de21ac950e00979d897"}, - {file = "multidict-6.4.3-py3-none-any.whl", hash = "sha256:59fe01ee8e2a1e8ceb3f6dbb216b09c8d9f4ef1c22c4fc825d045a147fa2ebc9"}, - {file = "multidict-6.4.3.tar.gz", hash = "sha256:3ada0b058c9f213c5f95ba301f922d402ac234f1111a7d8fd70f1b99f3c281ec"}, + {file = "multidict-6.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7b7d75cb5b90fa55700edbbdca12cd31f6b19c919e98712933c7a1c3c6c71b73"}, + {file = "multidict-6.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ad32e43e028276612bf5bab762677e7d131d2df00106b53de2efb2b8a28d5bce"}, + {file = "multidict-6.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0499cbc67c1b02ba333781798560c5b1e7cd03e9273b678c92c6de1b1657fac9"}, + {file = "multidict-6.5.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c78fc6bc1dd7a139dab7ee9046f79a2082dce9360e3899b762615d564e2e857"}, + {file = "multidict-6.5.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f369d6619b24da4df4a02455fea8641fe8324fc0100a3e0dcebc5bf55fa903f3"}, + {file = "multidict-6.5.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:719af50a44ce9cf9ab15d829bf8cf146de486b4816284c17c3c9b9c9735abb8f"}, + {file = "multidict-6.5.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:199a0a9b3de8bbeb6881460d32b857dc7abec94448aeb6d607c336628c53580a"}, + {file = "multidict-6.5.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fe09318a28b00c6f43180d0d889df1535e98fb2d93d25955d46945f8d5410d87"}, + {file = "multidict-6.5.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ab94923ae54385ed480e4ab19f10269ee60f3eabd0b35e2a5d1ba6dbf3b0cc27"}, + {file = "multidict-6.5.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:de2b253a3a90e1fa55eef5f9b3146bb5c722bd3400747112c9963404a2f5b9cf"}, + {file = "multidict-6.5.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b3bd88c1bc1f749db6a1e1f01696c3498bc25596136eceebb45766d24a320b27"}, + {file = "multidict-6.5.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0ce8f0ea49e8f54203f7d80e083a7aa017dbcb6f2d76d674273e25144c8aa3d7"}, + {file = "multidict-6.5.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dc62c8ac1b73ec704ed1a05be0267358fd5c99d1952f30448db1637336635cf8"}, + {file = "multidict-6.5.1-cp310-cp310-win32.whl", hash = "sha256:7a365a579fb3e067943d0278474e14c2244c252f460b401ccbf49f962e7b70fa"}, + {file = "multidict-6.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:4b299a2ffed33ad0733a9d47805b538d59465f8439bfea44df542cfb285c4db2"}, + {file = "multidict-6.5.1-cp310-cp310-win_arm64.whl", hash = "sha256:ed98ac527278372251fbc8f5c6c41bdf64ded1db0e6e86f9b9622744306060f6"}, + {file = "multidict-6.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:153d7ff738d9b67b94418b112dc5a662d89d2fc26846a9e942f039089048c804"}, + {file = "multidict-6.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1d784c0a1974f00d87f632d0fb6b1078baf7e15d2d2d1408af92f54d120f136e"}, + {file = "multidict-6.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dedf667cded1cdac5bfd3f3c2ff30010f484faccae4e871cc8a9316d2dc27363"}, + {file = "multidict-6.5.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7cbf407313236a79ce9b8af11808c29756cfb9c9a49a7f24bb1324537eec174b"}, + {file = "multidict-6.5.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2bf0068fe9abb0ebed1436a4e415117386951cf598eb8146ded4baf8e1ff6d1e"}, + {file = "multidict-6.5.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:195882f2f6272dacc88194ecd4de3608ad0ee29b161e541403b781a5f5dd346f"}, + {file = "multidict-6.5.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5776f9d2c3a1053f022f744af5f467c2f65b40d4cc00082bcf70e8c462c7dbad"}, + {file = "multidict-6.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a266373c604e49552d295d9f8ec4fd59bd364f2dd73eb18e7d36d5533b88f45"}, + {file = "multidict-6.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:79101d58094419b6e8d07e24946eba440136b9095590271cd6ccc4a90674a57d"}, + {file = "multidict-6.5.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:62eb76be8c20d9017a82b74965db93ddcf472b929b6b2b78c56972c73bacf2e4"}, + {file = "multidict-6.5.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:70c742357dd6207be30922207f8d59c91e2776ddbefa23830c55c09020e59f8a"}, + {file = "multidict-6.5.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:29eff1c9a905e298e9cd29f856f77485e58e59355f0ee323ac748203e002bbd3"}, + {file = "multidict-6.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:090e0b37fde199b58ea050c472c21dc8a3fbf285f42b862fe1ff02aab8942239"}, + {file = "multidict-6.5.1-cp311-cp311-win32.whl", hash = "sha256:6037beca8cb481307fb586ee0b73fae976a3e00d8f6ad7eb8af94a878a4893f0"}, + {file = "multidict-6.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:b632c1e4a2ff0bb4c1367d6c23871aa95dbd616bf4a847034732a142bb6eea94"}, + {file = "multidict-6.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:2ec3aa63f0c668f591d43195f8e555f803826dee34208c29ade9d63355f9e095"}, + {file = "multidict-6.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:48f95fe064f63d9601ef7a3dce2fc2a437d5fcc11bca960bc8be720330b13b6a"}, + {file = "multidict-6.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b7b6e1ce9b61f721417c68eeeb37599b769f3b631e6b25c21f50f8f619420b9"}, + {file = "multidict-6.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8b83b055889bda09fc866c0a652cdb6c36eeeafc2858259c9a7171fe82df5773"}, + {file = "multidict-6.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7bd4d655dc460c7aebb73b58ed1c074e85f7286105b012556cf0f25c6d1dba3"}, + {file = "multidict-6.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aa6dcf25ced31cdce10f004506dbc26129f28a911b32ed10e54453a0842a6173"}, + {file = "multidict-6.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:059fb556c3e6ce1a168496f92ef139ad839a47f898eaa512b1d43e5e05d78c6b"}, + {file = "multidict-6.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f97680c839dd9fa208e9584b1c2a5f1224bd01d31961f7f7d94984408c4a6b9e"}, + {file = "multidict-6.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7710c716243525cc05cd038c6e09f1807ee0fef2510a6e484450712c389c8d7f"}, + {file = "multidict-6.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:83eb172b4856ffff2814bdcf9c7792c0439302faab1b31376817b067b26cd8f5"}, + {file = "multidict-6.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:562d4714fa43f6ebc043a657535e4575e7d6141a818c9b3055f0868d29a1a41b"}, + {file = "multidict-6.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2d7def2fc47695c46a427b8f298fb5ace03d635c1fb17f30d6192c9a8fb69e70"}, + {file = "multidict-6.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:77bc8ab5c6bfe696eff564824e73a451fdeca22f3b960261750836cee02bcbfa"}, + {file = "multidict-6.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9eec51891d3c210948ead894ec1483d48748abec08db5ce9af52cc13fef37aee"}, + {file = "multidict-6.5.1-cp312-cp312-win32.whl", hash = "sha256:189f0c2bd1c0ae5509e453707d0e187e030c9e873a0116d1f32d1c870d0fc347"}, + {file = "multidict-6.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:e81f23b4b6f2a588f15d5cb554b2d8b482bb6044223d64b86bc7079cae9ebaad"}, + {file = "multidict-6.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:79d13e06d5241f9c8479dfeaf0f7cce8f453a4a302c9a0b1fa9b1a6869ff7757"}, + {file = "multidict-6.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:98011312f36d1e496f15454a95578d1212bc2ffc25650a8484752b06d304fd9b"}, + {file = "multidict-6.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bae589fb902b47bd94e6f539b34eefe55a1736099f616f614ec1544a43f95b05"}, + {file = "multidict-6.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6eb3bf26cd94eb306e4bc776d0964cc67a7967e4ad9299309f0ff5beec3c62be"}, + {file = "multidict-6.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e5e1a5a99c72d1531501406fcc06b6bf699ebd079dacd6807bb43fc0ff260e5c"}, + {file = "multidict-6.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:38755bcba18720cb2338bea23a5afcff234445ee75fa11518f6130e22f2ab970"}, + {file = "multidict-6.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f42fef9bcba3c32fd4e4a23c5757fc807d218b249573aaffa8634879f95feb73"}, + {file = "multidict-6.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:071b962f4cc87469cda90c7cc1c077b76496878b39851d7417a3d994e27fe2c6"}, + {file = "multidict-6.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:627ba4b7ce7c0115981f0fd91921f5d101dfb9972622178aeef84ccce1c2bbf3"}, + {file = "multidict-6.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:05dcaed3e5e54f0d0f99a39762b0195274b75016cbf246f600900305581cf1a2"}, + {file = "multidict-6.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:11f5ecf3e741a18c578d118ad257c5588ca33cc7c46d51c0487d7ae76f072c32"}, + {file = "multidict-6.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b948eb625411c20b15088fca862c51a39140b9cf7875b5fb47a72bb249fa2f42"}, + {file = "multidict-6.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc993a96dfc8300befd03d03df46efdb1d8d5a46911b014e956a4443035f470d"}, + {file = "multidict-6.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2d333380f22d35a56c6461f4579cfe186e143cd0b010b9524ac027de2a34cd"}, + {file = "multidict-6.5.1-cp313-cp313-win32.whl", hash = "sha256:5891e3327e6a426ddd443c87339b967c84feb8c022dd425e0c025fa0fcd71e68"}, + {file = "multidict-6.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:fcdaa72261bff25fad93e7cb9bd7112bd4bac209148e698e380426489d8ed8a9"}, + {file = "multidict-6.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:84292145303f354a35558e601c665cdf87059d87b12777417e2e57ba3eb98903"}, + {file = "multidict-6.5.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f8316e58db799a1972afbc46770dfaaf20b0847003ab80de6fcb9861194faa3f"}, + {file = "multidict-6.5.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3468f0db187aca59eb56e0aa9f7c8c5427bcb844ad1c86557b4886aeb4484d8"}, + {file = "multidict-6.5.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:228533a5f99f1248cd79f6470779c424d63bc3e10d47c82511c65cc294458445"}, + {file = "multidict-6.5.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527076fdf5854901b1246c589af9a8a18b4a308375acb0020b585f696a10c794"}, + {file = "multidict-6.5.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9a17a17bad5c22f43e6a6b285dd9c16b1e8f8428202cd9bc22adaac68d0bbfed"}, + {file = "multidict-6.5.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:efd1951edab4a6cb65108d411867811f2b283f4b972337fb4269e40142f7f6a6"}, + {file = "multidict-6.5.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c07d5f38b39acb4f8f61a7aa4166d140ed628245ff0441630df15340532e3b3c"}, + {file = "multidict-6.5.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a6605dc74cd333be279e1fcb568ea24f7bdf1cf09f83a77360ce4dd32d67f14"}, + {file = "multidict-6.5.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d64e30ae9ba66ce303a567548a06d64455d97c5dff7052fe428d154274d7174"}, + {file = "multidict-6.5.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2fb5dde79a7f6d98ac5e26a4c9de77ccd2c5224a7ce89aeac6d99df7bbe06464"}, + {file = "multidict-6.5.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:8a0d22e8b07cf620e9aeb1582340d00f0031e6a1f3e39d9c2dcbefa8691443b4"}, + {file = "multidict-6.5.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0120ed5cff2082c7a0ed62a8f80f4f6ac266010c722381816462f279bfa19487"}, + {file = "multidict-6.5.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3dea06ba27401c4b54317aa04791182dc9295e7aa623732dd459071a0e0f65db"}, + {file = "multidict-6.5.1-cp313-cp313t-win32.whl", hash = "sha256:93b21be44f3cfee3be68ed5cd8848a3c0420d76dbd12d74f7776bde6b29e5f33"}, + {file = "multidict-6.5.1-cp313-cp313t-win_amd64.whl", hash = "sha256:c5c18f8646a520cc34d00f65f9f6f77782b8a8c59fd8de10713e0de7f470b5d0"}, + {file = "multidict-6.5.1-cp313-cp313t-win_arm64.whl", hash = "sha256:eb27128141474a1d545f0531b496c7c2f1c4beff50cb5a828f36eb62fef16c67"}, + {file = "multidict-6.5.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:279a37cb9d04097bf1c6308d7495cb4dfbd8fb538301bfe464266b045dfeb1cd"}, + {file = "multidict-6.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e63ac6adc668cfe52e29c00afe33c3b8dbec8e37b529aa83bf31ba4bad0c509"}, + {file = "multidict-6.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:36b138c6ec3aedaa975653ea90099efb22042bab31727dd4cd2921a64de46b25"}, + {file = "multidict-6.5.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:576a1887a5c5becbe4fb484d0bdf6ed8ec89e9c11770f8f3214fd127ba137b8b"}, + {file = "multidict-6.5.1-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a0f1890f9a05d038720a7c3b5d82467534495bcb6bbda929f6f0914977cc56d1"}, + {file = "multidict-6.5.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5125a9faed98738d7d6e23650bd8af70abb95628d011f57f70a4d8f349e6d073"}, + {file = "multidict-6.5.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e09d7852100bcc3e466e63478ee18c68cc4d2ca2a978f29b90d5e2ea814f7b3e"}, + {file = "multidict-6.5.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e615032b684a1d6faffe41d64cd896801bd3f2c1b642355e9b5d11fd8d40223e"}, + {file = "multidict-6.5.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:70c0e51d55f9bc5e97de950c3b3e88f501b3ca2b3894f231f3957dd3985b4d54"}, + {file = "multidict-6.5.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:a6523258f2eb24c91995ae64172c19cd73bacd5a7f2b0733676966c527ab08f8"}, + {file = "multidict-6.5.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:5e8fefd7c062b0657af2480d789dcb347450d17c7bd20b02303c25f1f59a33a7"}, + {file = "multidict-6.5.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:3d749b10cc6acb2c0814df881910ffd8d8ab1ec54493585579b4a75f89fe86d6"}, + {file = "multidict-6.5.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:22b47f7e76ebea0b802df9ed08165b1e6ab52b140c7180c3e740e6205b3781b3"}, + {file = "multidict-6.5.1-cp39-cp39-win32.whl", hash = "sha256:cc80c7e8f297484c4511e887c244adec9a7ed3a76826cb8dbc6183b717a37d1f"}, + {file = "multidict-6.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:253e5c41fcc02e2956ab276b5a702f3972db1e87c080be55e87ca31a2f4f8012"}, + {file = "multidict-6.5.1-cp39-cp39-win_arm64.whl", hash = "sha256:a9e15dfe441aec31e0fa78f497aca83f0ad992ca58782cbaba8220e5a87608fc"}, + {file = "multidict-6.5.1-py3-none-any.whl", hash = "sha256:895354f4a38f53a1df2cc3fa2223fa714cff2b079a9f018a76cad35e7f0f044c"}, + {file = "multidict-6.5.1.tar.gz", hash = "sha256:a835ea8103f4723915d7d621529c80ef48db48ae0c818afcabe0f95aa1febc3a"}, ] [package.dependencies] @@ -3572,14 +3589,14 @@ files = [ [[package]] name = "narwhals" -version = "1.35.0" +version = "1.44.0" description = "Extremely lightweight compatibility layer between dataframe libraries" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "narwhals-1.35.0-py3-none-any.whl", hash = "sha256:7562af132fa3f8aaaf34dc96d7ec95bdca29d1c795e8fcf14e01edf1d32122bc"}, - {file = "narwhals-1.35.0.tar.gz", hash = "sha256:07477d18487fbc940243b69818a177ed7119b737910a8a254fb67688b48a7c96"}, + {file = "narwhals-1.44.0-py3-none-any.whl", hash = "sha256:a170ea0bab4cf1f323d9f8bf17f2d7042c3d73802bea321996b39bf075d57de5"}, + {file = "narwhals-1.44.0.tar.gz", hash = "sha256:8cf0616d4f6f21225b3b56fcde96ccab6d05023561a0f162402aa9b8c33ad31d"}, ] [package.extras] @@ -3588,10 +3605,11 @@ dask = ["dask[dataframe] (>=2024.8)"] duckdb = ["duckdb (>=1.0)"] ibis = ["ibis-framework (>=6.0.0)", "packaging", "pyarrow-hotfix", "rich"] modin = ["modin"] -pandas = ["pandas (>=0.25.3)"] +pandas = ["pandas (>=1.1.3)"] polars = ["polars (>=0.20.3)"] pyarrow = ["pyarrow (>=11.0.0)"] pyspark = ["pyspark (>=3.5.0)"] +pyspark-connect = ["pyspark[connect] (>=3.5.0)"] sqlframe = ["sqlframe (>=3.22.0)"] [[package]] @@ -3694,14 +3712,14 @@ files = [ [[package]] name = "oauthlib" -version = "3.2.2" +version = "3.3.1" description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" groups = ["main"] files = [ - {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, - {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, + {file = "oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1"}, + {file = "oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9"}, ] [package.extras] @@ -3746,52 +3764,52 @@ openapi-schema-validator = ">=0.6.0,<0.7.0" [[package]] name = "opentelemetry-api" -version = "1.32.1" +version = "1.34.1" description = "OpenTelemetry Python API" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "opentelemetry_api-1.32.1-py3-none-any.whl", hash = "sha256:bbd19f14ab9f15f0e85e43e6a958aa4cb1f36870ee62b7fd205783a112012724"}, - {file = "opentelemetry_api-1.32.1.tar.gz", hash = "sha256:a5be71591694a4d9195caf6776b055aa702e964d961051a0715d05f8632c32fb"}, + {file = "opentelemetry_api-1.34.1-py3-none-any.whl", hash = "sha256:b7df4cb0830d5a6c29ad0c0691dbae874d8daefa934b8b1d642de48323d32a8c"}, + {file = "opentelemetry_api-1.34.1.tar.gz", hash = "sha256:64f0bd06d42824843731d05beea88d4d4b6ae59f9fe347ff7dfa2cc14233bbb3"}, ] [package.dependencies] -deprecated = ">=1.2.6" -importlib-metadata = ">=6.0,<8.7.0" +importlib-metadata = ">=6.0,<8.8.0" +typing-extensions = ">=4.5.0" [[package]] name = "opentelemetry-sdk" -version = "1.32.1" +version = "1.34.1" description = "OpenTelemetry Python SDK" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "opentelemetry_sdk-1.32.1-py3-none-any.whl", hash = "sha256:bba37b70a08038613247bc42beee5a81b0ddca422c7d7f1b097b32bf1c7e2f17"}, - {file = "opentelemetry_sdk-1.32.1.tar.gz", hash = "sha256:8ef373d490961848f525255a42b193430a0637e064dd132fd2a014d94792a092"}, + {file = "opentelemetry_sdk-1.34.1-py3-none-any.whl", hash = "sha256:308effad4059562f1d92163c61c8141df649da24ce361827812c40abb2a1e96e"}, + {file = "opentelemetry_sdk-1.34.1.tar.gz", hash = "sha256:8091db0d763fcd6098d4781bbc80ff0971f94e260739aa6afe6fd379cdf3aa4d"}, ] [package.dependencies] -opentelemetry-api = "1.32.1" -opentelemetry-semantic-conventions = "0.53b1" -typing-extensions = ">=3.7.4" +opentelemetry-api = "1.34.1" +opentelemetry-semantic-conventions = "0.55b1" +typing-extensions = ">=4.5.0" [[package]] name = "opentelemetry-semantic-conventions" -version = "0.53b1" +version = "0.55b1" description = "OpenTelemetry Semantic Conventions" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "opentelemetry_semantic_conventions-0.53b1-py3-none-any.whl", hash = "sha256:21df3ed13f035f8f3ea42d07cbebae37020367a53b47f1ebee3b10a381a00208"}, - {file = "opentelemetry_semantic_conventions-0.53b1.tar.gz", hash = "sha256:4c5a6fede9de61211b2e9fc1e02e8acacce882204cd770177342b6a3be682992"}, + {file = "opentelemetry_semantic_conventions-0.55b1-py3-none-any.whl", hash = "sha256:5da81dfdf7d52e3d37f8fe88d5e771e191de924cfff5f550ab0b8f7b2409baed"}, + {file = "opentelemetry_semantic_conventions-0.55b1.tar.gz", hash = "sha256:ef95b1f009159c28d7a7849f5cbc71c4c34c845bb514d66adfdf1b3fff3598b3"}, ] [package.dependencies] -deprecated = ">=1.2.6" -opentelemetry-api = "1.32.1" +opentelemetry-api = "1.34.1" +typing-extensions = ">=4.5.0" [[package]] name = "orjson" @@ -4049,14 +4067,14 @@ setuptools = "*" [[package]] name = "platformdirs" -version = "4.3.7" +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 = ["dev", "docs"] files = [ - {file = "platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94"}, - {file = "platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351"}, + {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, + {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, ] [package.extras] @@ -4066,14 +4084,14 @@ type = ["mypy (>=1.14.1)"] [[package]] name = "plotly" -version = "6.0.1" +version = "6.1.2" description = "An open-source interactive data visualization library for Python" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "plotly-6.0.1-py3-none-any.whl", hash = "sha256:4714db20fea57a435692c548a4eb4fae454f7daddf15f8d8ba7e1045681d7768"}, - {file = "plotly-6.0.1.tar.gz", hash = "sha256:dd8400229872b6e3c964b099be699f8d00c489a974f2cfccfad5e8240873366b"}, + {file = "plotly-6.1.2-py3-none-any.whl", hash = "sha256:f1548a8ed9158d59e03d7fed548c7db5549f3130d9ae19293c8638c202648f6d"}, + {file = "plotly-6.1.2.tar.gz", hash = "sha256:4fdaa228926ba3e3a213f4d1713287e69dcad1a7e66cf2025bd7d7026d5014b4"}, ] [package.dependencies] @@ -4081,23 +4099,25 @@ narwhals = ">=1.15.1" packaging = "*" [package.extras] +dev = ["black (==25.1.0)"] express = ["numpy"] +kaleido = ["kaleido (==1.0.0rc13)"] [[package]] name = "pluggy" -version = "1.5.0" +version = "1.6.0" description = "plugin and hook calling mechanisms for python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, - {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, + {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 = ["pytest", "pytest-benchmark"] +testing = ["coverage", "pytest", "pytest-benchmark"] [[package]] name = "ply" @@ -4170,110 +4190,110 @@ tests = ["pytest", "pytest-cov", "pytest-lazy-fixtures"] [[package]] name = "propcache" -version = "0.3.1" +version = "0.3.2" description = "Accelerated property cache" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "propcache-0.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f27785888d2fdd918bc36de8b8739f2d6c791399552333721b58193f68ea3e98"}, - {file = "propcache-0.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4e89cde74154c7b5957f87a355bb9c8ec929c167b59c83d90654ea36aeb6180"}, - {file = "propcache-0.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:730178f476ef03d3d4d255f0c9fa186cb1d13fd33ffe89d39f2cda4da90ceb71"}, - {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:967a8eec513dbe08330f10137eacb427b2ca52118769e82ebcfcab0fba92a649"}, - {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b9145c35cc87313b5fd480144f8078716007656093d23059e8993d3a8fa730f"}, - {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e64e948ab41411958670f1093c0a57acfdc3bee5cf5b935671bbd5313bcf229"}, - {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:319fa8765bfd6a265e5fa661547556da381e53274bc05094fc9ea50da51bfd46"}, - {file = "propcache-0.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66d8ccbc902ad548312b96ed8d5d266d0d2c6d006fd0f66323e9d8f2dd49be7"}, - {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2d219b0dbabe75e15e581fc1ae796109b07c8ba7d25b9ae8d650da582bed01b0"}, - {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cd6a55f65241c551eb53f8cf4d2f4af33512c39da5d9777694e9d9c60872f519"}, - {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9979643ffc69b799d50d3a7b72b5164a2e97e117009d7af6dfdd2ab906cb72cd"}, - {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4cf9e93a81979f1424f1a3d155213dc928f1069d697e4353edb8a5eba67c6259"}, - {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2fce1df66915909ff6c824bbb5eb403d2d15f98f1518e583074671a30fe0c21e"}, - {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4d0dfdd9a2ebc77b869a0b04423591ea8823f791293b527dc1bb896c1d6f1136"}, - {file = "propcache-0.3.1-cp310-cp310-win32.whl", hash = "sha256:1f6cc0ad7b4560e5637eb2c994e97b4fa41ba8226069c9277eb5ea7101845b42"}, - {file = "propcache-0.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:47ef24aa6511e388e9894ec16f0fbf3313a53ee68402bc428744a367ec55b833"}, - {file = "propcache-0.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7f30241577d2fef2602113b70ef7231bf4c69a97e04693bde08ddab913ba0ce5"}, - {file = "propcache-0.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:43593c6772aa12abc3af7784bff4a41ffa921608dd38b77cf1dfd7f5c4e71371"}, - {file = "propcache-0.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a75801768bbe65499495660b777e018cbe90c7980f07f8aa57d6be79ea6f71da"}, - {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6f1324db48f001c2ca26a25fa25af60711e09b9aaf4b28488602776f4f9a744"}, - {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cdb0f3e1eb6dfc9965d19734d8f9c481b294b5274337a8cb5cb01b462dcb7e0"}, - {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1eb34d90aac9bfbced9a58b266f8946cb5935869ff01b164573a7634d39fbcb5"}, - {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f35c7070eeec2cdaac6fd3fe245226ed2a6292d3ee8c938e5bb645b434c5f256"}, - {file = "propcache-0.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b23c11c2c9e6d4e7300c92e022046ad09b91fd00e36e83c44483df4afa990073"}, - {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3e19ea4ea0bf46179f8a3652ac1426e6dcbaf577ce4b4f65be581e237340420d"}, - {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:bd39c92e4c8f6cbf5f08257d6360123af72af9f4da75a690bef50da77362d25f"}, - {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b0313e8b923b3814d1c4a524c93dfecea5f39fa95601f6a9b1ac96cd66f89ea0"}, - {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e861ad82892408487be144906a368ddbe2dc6297074ade2d892341b35c59844a"}, - {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:61014615c1274df8da5991a1e5da85a3ccb00c2d4701ac6f3383afd3ca47ab0a"}, - {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:71ebe3fe42656a2328ab08933d420df5f3ab121772eef78f2dc63624157f0ed9"}, - {file = "propcache-0.3.1-cp311-cp311-win32.whl", hash = "sha256:58aa11f4ca8b60113d4b8e32d37e7e78bd8af4d1a5b5cb4979ed856a45e62005"}, - {file = "propcache-0.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:9532ea0b26a401264b1365146c440a6d78269ed41f83f23818d4b79497aeabe7"}, - {file = "propcache-0.3.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f78eb8422acc93d7b69964012ad7048764bb45a54ba7a39bb9e146c72ea29723"}, - {file = "propcache-0.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:89498dd49c2f9a026ee057965cdf8192e5ae070ce7d7a7bd4b66a8e257d0c976"}, - {file = "propcache-0.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:09400e98545c998d57d10035ff623266927cb784d13dd2b31fd33b8a5316b85b"}, - {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa8efd8c5adc5a2c9d3b952815ff8f7710cefdcaf5f2c36d26aff51aeca2f12f"}, - {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2fe5c910f6007e716a06d269608d307b4f36e7babee5f36533722660e8c4a70"}, - {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a0ab8cf8cdd2194f8ff979a43ab43049b1df0b37aa64ab7eca04ac14429baeb7"}, - {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:563f9d8c03ad645597b8d010ef4e9eab359faeb11a0a2ac9f7b4bc8c28ebef25"}, - {file = "propcache-0.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb6e0faf8cb6b4beea5d6ed7b5a578254c6d7df54c36ccd3d8b3eb00d6770277"}, - {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1c5c7ab7f2bb3f573d1cb921993006ba2d39e8621019dffb1c5bc94cdbae81e8"}, - {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:050b571b2e96ec942898f8eb46ea4bfbb19bd5502424747e83badc2d4a99a44e"}, - {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e1c4d24b804b3a87e9350f79e2371a705a188d292fd310e663483af6ee6718ee"}, - {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e4fe2a6d5ce975c117a6bb1e8ccda772d1e7029c1cca1acd209f91d30fa72815"}, - {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:feccd282de1f6322f56f6845bf1207a537227812f0a9bf5571df52bb418d79d5"}, - {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ec314cde7314d2dd0510c6787326bbffcbdc317ecee6b7401ce218b3099075a7"}, - {file = "propcache-0.3.1-cp312-cp312-win32.whl", hash = "sha256:7d2d5a0028d920738372630870e7d9644ce437142197f8c827194fca404bf03b"}, - {file = "propcache-0.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:88c423efef9d7a59dae0614eaed718449c09a5ac79a5f224a8b9664d603f04a3"}, - {file = "propcache-0.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f1528ec4374617a7a753f90f20e2f551121bb558fcb35926f99e3c42367164b8"}, - {file = "propcache-0.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc1915ec523b3b494933b5424980831b636fe483d7d543f7afb7b3bf00f0c10f"}, - {file = "propcache-0.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a110205022d077da24e60b3df8bcee73971be9575dec5573dd17ae5d81751111"}, - {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d249609e547c04d190e820d0d4c8ca03ed4582bcf8e4e160a6969ddfb57b62e5"}, - {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ced33d827625d0a589e831126ccb4f5c29dfdf6766cac441d23995a65825dcb"}, - {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4114c4ada8f3181af20808bedb250da6bae56660e4b8dfd9cd95d4549c0962f7"}, - {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:975af16f406ce48f1333ec5e912fe11064605d5c5b3f6746969077cc3adeb120"}, - {file = "propcache-0.3.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a34aa3a1abc50740be6ac0ab9d594e274f59960d3ad253cd318af76b996dd654"}, - {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9cec3239c85ed15bfaded997773fdad9fb5662b0a7cbc854a43f291eb183179e"}, - {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:05543250deac8e61084234d5fc54f8ebd254e8f2b39a16b1dce48904f45b744b"}, - {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5cb5918253912e088edbf023788de539219718d3b10aef334476b62d2b53de53"}, - {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f3bbecd2f34d0e6d3c543fdb3b15d6b60dd69970c2b4c822379e5ec8f6f621d5"}, - {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aca63103895c7d960a5b9b044a83f544b233c95e0dcff114389d64d762017af7"}, - {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a0a9898fdb99bf11786265468571e628ba60af80dc3f6eb89a3545540c6b0ef"}, - {file = "propcache-0.3.1-cp313-cp313-win32.whl", hash = "sha256:3a02a28095b5e63128bcae98eb59025924f121f048a62393db682f049bf4ac24"}, - {file = "propcache-0.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:813fbb8b6aea2fc9659815e585e548fe706d6f663fa73dff59a1677d4595a037"}, - {file = "propcache-0.3.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a444192f20f5ce8a5e52761a031b90f5ea6288b1eef42ad4c7e64fef33540b8f"}, - {file = "propcache-0.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fbe94666e62ebe36cd652f5fc012abfbc2342de99b523f8267a678e4dfdee3c"}, - {file = "propcache-0.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f011f104db880f4e2166bcdcf7f58250f7a465bc6b068dc84c824a3d4a5c94dc"}, - {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e584b6d388aeb0001d6d5c2bd86b26304adde6d9bb9bfa9c4889805021b96de"}, - {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a17583515a04358b034e241f952f1715243482fc2c2945fd99a1b03a0bd77d6"}, - {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5aed8d8308215089c0734a2af4f2e95eeb360660184ad3912686c181e500b2e7"}, - {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d8e309ff9a0503ef70dc9a0ebd3e69cf7b3894c9ae2ae81fc10943c37762458"}, - {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b655032b202028a582d27aeedc2e813299f82cb232f969f87a4fde491a233f11"}, - {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f64d91b751df77931336b5ff7bafbe8845c5770b06630e27acd5dbb71e1931c"}, - {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:19a06db789a4bd896ee91ebc50d059e23b3639c25d58eb35be3ca1cbe967c3bf"}, - {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:bef100c88d8692864651b5f98e871fb090bd65c8a41a1cb0ff2322db39c96c27"}, - {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:87380fb1f3089d2a0b8b00f006ed12bd41bd858fabfa7330c954c70f50ed8757"}, - {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e474fc718e73ba5ec5180358aa07f6aded0ff5f2abe700e3115c37d75c947e18"}, - {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:17d1c688a443355234f3c031349da69444be052613483f3e4158eef751abcd8a"}, - {file = "propcache-0.3.1-cp313-cp313t-win32.whl", hash = "sha256:359e81a949a7619802eb601d66d37072b79b79c2505e6d3fd8b945538411400d"}, - {file = "propcache-0.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e7fb9a84c9abbf2b2683fa3e7b0d7da4d8ecf139a1c635732a8bda29c5214b0e"}, - {file = "propcache-0.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ed5f6d2edbf349bd8d630e81f474d33d6ae5d07760c44d33cd808e2f5c8f4ae6"}, - {file = "propcache-0.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:668ddddc9f3075af019f784456267eb504cb77c2c4bd46cc8402d723b4d200bf"}, - {file = "propcache-0.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0c86e7ceea56376216eba345aa1fc6a8a6b27ac236181f840d1d7e6a1ea9ba5c"}, - {file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83be47aa4e35b87c106fc0c84c0fc069d3f9b9b06d3c494cd404ec6747544894"}, - {file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:27c6ac6aa9fc7bc662f594ef380707494cb42c22786a558d95fcdedb9aa5d035"}, - {file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64a956dff37080b352c1c40b2966b09defb014347043e740d420ca1eb7c9b908"}, - {file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82de5da8c8893056603ac2d6a89eb8b4df49abf1a7c19d536984c8dd63f481d5"}, - {file = "propcache-0.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c3c3a203c375b08fd06a20da3cf7aac293b834b6f4f4db71190e8422750cca5"}, - {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b303b194c2e6f171cfddf8b8ba30baefccf03d36a4d9cab7fd0bb68ba476a3d7"}, - {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:916cd229b0150129d645ec51614d38129ee74c03293a9f3f17537be0029a9641"}, - {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a461959ead5b38e2581998700b26346b78cd98540b5524796c175722f18b0294"}, - {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:069e7212890b0bcf9b2be0a03afb0c2d5161d91e1bf51569a64f629acc7defbf"}, - {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ef2e4e91fb3945769e14ce82ed53007195e616a63aa43b40fb7ebaaf907c8d4c"}, - {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8638f99dca15b9dff328fb6273e09f03d1c50d9b6512f3b65a4154588a7595fe"}, - {file = "propcache-0.3.1-cp39-cp39-win32.whl", hash = "sha256:6f173bbfe976105aaa890b712d1759de339d8a7cef2fc0a1714cc1a1e1c47f64"}, - {file = "propcache-0.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:603f1fe4144420374f1a69b907494c3acbc867a581c2d49d4175b0de7cc64566"}, - {file = "propcache-0.3.1-py3-none-any.whl", hash = "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40"}, - {file = "propcache-0.3.1.tar.gz", hash = "sha256:40d980c33765359098837527e18eddefc9a24cea5b45e078a7f3bb5b032c6ecf"}, + {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]] @@ -4734,14 +4754,14 @@ urllib3 = ">=1.26.0" [[package]] name = "pygments" -version = "2.19.1" +version = "2.19.2" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" groups = ["dev", "docs"] files = [ - {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, - {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, ] [package.extras] @@ -4801,14 +4821,14 @@ testutils = ["gitpython (>3)"] [[package]] name = "pymdown-extensions" -version = "10.14.3" +version = "10.16" description = "Extension pack for Python Markdown." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["docs"] files = [ - {file = "pymdown_extensions-10.14.3-py3-none-any.whl", hash = "sha256:05e0bee73d64b9c71a4ae17c72abc2f700e8bc8403755a00580b49a4e9f189e9"}, - {file = "pymdown_extensions-10.14.3.tar.gz", hash = "sha256:41e576ce3f5d650be59e900e4ceff231e0aed2a88cf30acaee41e02f063a061b"}, + {file = "pymdown_extensions-10.16-py3-none-any.whl", hash = "sha256:f5dd064a4db588cb2d95229fc4ee63a1b16cc8b4d0e6145c0899ed8723da1df2"}, + {file = "pymdown_extensions-10.16.tar.gz", hash = "sha256:71dac4fca63fabeffd3eb9038b756161a33ec6e8d230853d3cecf562155ab3de"}, ] [package.dependencies] @@ -5132,14 +5152,14 @@ files = [ [[package]] name = "pyyaml-env-tag" -version = "0.1" -description = "A custom YAML tag for referencing environment variables in YAML files. " +version = "1.1" +description = "A custom YAML tag for referencing environment variables in YAML files." optional = false -python-versions = ">=3.6" +python-versions = ">=3.9" groups = ["docs"] files = [ - {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, - {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, + {file = "pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04"}, + {file = "pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff"}, ] [package.dependencies] @@ -5367,19 +5387,16 @@ tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asy [[package]] name = "retrying" -version = "1.3.4" +version = "1.4.0" description = "Retrying" optional = false -python-versions = "*" +python-versions = ">=3.6" groups = ["main"] files = [ - {file = "retrying-1.3.4-py3-none-any.whl", hash = "sha256:8cc4d43cb8e1125e0ff3344e9de678fefd85db3b750b81b2240dc0183af37b35"}, - {file = "retrying-1.3.4.tar.gz", hash = "sha256:345da8c5765bd982b1d1915deb9102fd3d1f7ad16bd84a9700b85f64d24e8f3e"}, + {file = "retrying-1.4.0-py3-none-any.whl", hash = "sha256:6509d829c70271937605bce361c8f76e91f9123d355d14df7dc6972b1518064a"}, + {file = "retrying-1.4.0.tar.gz", hash = "sha256:efa99c78bf4fbdbe6f0cba4101470fbc684b93d30ca45ffa1288443a9805172f"}, ] -[package.dependencies] -six = ">=1.7.0" - [[package]] name = "rfc3339-validator" version = "0.1.4" @@ -5417,126 +5434,129 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "rpds-py" -version = "0.24.0" +version = "0.25.1" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "rpds_py-0.24.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:006f4342fe729a368c6df36578d7a348c7c716be1da0a1a0f86e3021f8e98724"}, - {file = "rpds_py-0.24.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2d53747da70a4e4b17f559569d5f9506420966083a31c5fbd84e764461c4444b"}, - {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8acd55bd5b071156bae57b555f5d33697998752673b9de554dd82f5b5352727"}, - {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7e80d375134ddb04231a53800503752093dbb65dad8dabacce2c84cccc78e964"}, - {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60748789e028d2a46fc1c70750454f83c6bdd0d05db50f5ae83e2db500b34da5"}, - {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6e1daf5bf6c2be39654beae83ee6b9a12347cb5aced9a29eecf12a2d25fff664"}, - {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b221c2457d92a1fb3c97bee9095c874144d196f47c038462ae6e4a14436f7bc"}, - {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:66420986c9afff67ef0c5d1e4cdc2d0e5262f53ad11e4f90e5e22448df485bf0"}, - {file = "rpds_py-0.24.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:43dba99f00f1d37b2a0265a259592d05fcc8e7c19d140fe51c6e6f16faabeb1f"}, - {file = "rpds_py-0.24.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a88c0d17d039333a41d9bf4616bd062f0bd7aa0edeb6cafe00a2fc2a804e944f"}, - {file = "rpds_py-0.24.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc31e13ce212e14a539d430428cd365e74f8b2d534f8bc22dd4c9c55b277b875"}, - {file = "rpds_py-0.24.0-cp310-cp310-win32.whl", hash = "sha256:fc2c1e1b00f88317d9de6b2c2b39b012ebbfe35fe5e7bef980fd2a91f6100a07"}, - {file = "rpds_py-0.24.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0145295ca415668420ad142ee42189f78d27af806fcf1f32a18e51d47dd2052"}, - {file = "rpds_py-0.24.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2d3ee4615df36ab8eb16c2507b11e764dcc11fd350bbf4da16d09cda11fcedef"}, - {file = "rpds_py-0.24.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e13ae74a8a3a0c2f22f450f773e35f893484fcfacb00bb4344a7e0f4f48e1f97"}, - {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf86f72d705fc2ef776bb7dd9e5fbba79d7e1f3e258bf9377f8204ad0fc1c51e"}, - {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c43583ea8517ed2e780a345dd9960896afc1327e8cf3ac8239c167530397440d"}, - {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4cd031e63bc5f05bdcda120646a0d32f6d729486d0067f09d79c8db5368f4586"}, - {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34d90ad8c045df9a4259c47d2e16a3f21fdb396665c94520dbfe8766e62187a4"}, - {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e838bf2bb0b91ee67bf2b889a1a841e5ecac06dd7a2b1ef4e6151e2ce155c7ae"}, - {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04ecf5c1ff4d589987b4d9882872f80ba13da7d42427234fce8f22efb43133bc"}, - {file = "rpds_py-0.24.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:630d3d8ea77eabd6cbcd2ea712e1c5cecb5b558d39547ac988351195db433f6c"}, - {file = "rpds_py-0.24.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ebcb786b9ff30b994d5969213a8430cbb984cdd7ea9fd6df06663194bd3c450c"}, - {file = "rpds_py-0.24.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:174e46569968ddbbeb8a806d9922f17cd2b524aa753b468f35b97ff9c19cb718"}, - {file = "rpds_py-0.24.0-cp311-cp311-win32.whl", hash = "sha256:5ef877fa3bbfb40b388a5ae1cb00636a624690dcb9a29a65267054c9ea86d88a"}, - {file = "rpds_py-0.24.0-cp311-cp311-win_amd64.whl", hash = "sha256:e274f62cbd274359eff63e5c7e7274c913e8e09620f6a57aae66744b3df046d6"}, - {file = "rpds_py-0.24.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:d8551e733626afec514b5d15befabea0dd70a343a9f23322860c4f16a9430205"}, - {file = "rpds_py-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e374c0ce0ca82e5b67cd61fb964077d40ec177dd2c4eda67dba130de09085c7"}, - {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d69d003296df4840bd445a5d15fa5b6ff6ac40496f956a221c4d1f6f7b4bc4d9"}, - {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8212ff58ac6dfde49946bea57474a386cca3f7706fc72c25b772b9ca4af6b79e"}, - {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:528927e63a70b4d5f3f5ccc1fa988a35456eb5d15f804d276709c33fc2f19bda"}, - {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a824d2c7a703ba6daaca848f9c3d5cb93af0505be505de70e7e66829affd676e"}, - {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44d51febb7a114293ffd56c6cf4736cb31cd68c0fddd6aa303ed09ea5a48e029"}, - {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3fab5f4a2c64a8fb64fc13b3d139848817a64d467dd6ed60dcdd6b479e7febc9"}, - {file = "rpds_py-0.24.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9be4f99bee42ac107870c61dfdb294d912bf81c3c6d45538aad7aecab468b6b7"}, - {file = "rpds_py-0.24.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:564c96b6076a98215af52f55efa90d8419cc2ef45d99e314fddefe816bc24f91"}, - {file = "rpds_py-0.24.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:75a810b7664c17f24bf2ffd7f92416c00ec84b49bb68e6a0d93e542406336b56"}, - {file = "rpds_py-0.24.0-cp312-cp312-win32.whl", hash = "sha256:f6016bd950be4dcd047b7475fdf55fb1e1f59fc7403f387be0e8123e4a576d30"}, - {file = "rpds_py-0.24.0-cp312-cp312-win_amd64.whl", hash = "sha256:998c01b8e71cf051c28f5d6f1187abbdf5cf45fc0efce5da6c06447cba997034"}, - {file = "rpds_py-0.24.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3d2d8e4508e15fc05b31285c4b00ddf2e0eb94259c2dc896771966a163122a0c"}, - {file = "rpds_py-0.24.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f00c16e089282ad68a3820fd0c831c35d3194b7cdc31d6e469511d9bffc535c"}, - {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951cc481c0c395c4a08639a469d53b7d4afa252529a085418b82a6b43c45c240"}, - {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9ca89938dff18828a328af41ffdf3902405a19f4131c88e22e776a8e228c5a8"}, - {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0ef550042a8dbcd657dfb284a8ee00f0ba269d3f2286b0493b15a5694f9fe8"}, - {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b2356688e5d958c4d5cb964af865bea84db29971d3e563fb78e46e20fe1848b"}, - {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78884d155fd15d9f64f5d6124b486f3d3f7fd7cd71a78e9670a0f6f6ca06fb2d"}, - {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6a4a535013aeeef13c5532f802708cecae8d66c282babb5cd916379b72110cf7"}, - {file = "rpds_py-0.24.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:84e0566f15cf4d769dade9b366b7b87c959be472c92dffb70462dd0844d7cbad"}, - {file = "rpds_py-0.24.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:823e74ab6fbaa028ec89615ff6acb409e90ff45580c45920d4dfdddb069f2120"}, - {file = "rpds_py-0.24.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c61a2cb0085c8783906b2f8b1f16a7e65777823c7f4d0a6aaffe26dc0d358dd9"}, - {file = "rpds_py-0.24.0-cp313-cp313-win32.whl", hash = "sha256:60d9b630c8025b9458a9d114e3af579a2c54bd32df601c4581bd054e85258143"}, - {file = "rpds_py-0.24.0-cp313-cp313-win_amd64.whl", hash = "sha256:6eea559077d29486c68218178ea946263b87f1c41ae7f996b1f30a983c476a5a"}, - {file = "rpds_py-0.24.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:d09dc82af2d3c17e7dd17120b202a79b578d79f2b5424bda209d9966efeed114"}, - {file = "rpds_py-0.24.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5fc13b44de6419d1e7a7e592a4885b323fbc2f46e1f22151e3a8ed3b8b920405"}, - {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c347a20d79cedc0a7bd51c4d4b7dbc613ca4e65a756b5c3e57ec84bd43505b47"}, - {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20f2712bd1cc26a3cc16c5a1bfee9ed1abc33d4cdf1aabd297fe0eb724df4272"}, - {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aad911555286884be1e427ef0dc0ba3929e6821cbeca2194b13dc415a462c7fd"}, - {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0aeb3329c1721c43c58cae274d7d2ca85c1690d89485d9c63a006cb79a85771a"}, - {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a0f156e9509cee987283abd2296ec816225145a13ed0391df8f71bf1d789e2d"}, - {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aa6800adc8204ce898c8a424303969b7aa6a5e4ad2789c13f8648739830323b7"}, - {file = "rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a18fc371e900a21d7392517c6f60fe859e802547309e94313cd8181ad9db004d"}, - {file = "rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9168764133fd919f8dcca2ead66de0105f4ef5659cbb4fa044f7014bed9a1797"}, - {file = "rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f6e3cec44ba05ee5cbdebe92d052f69b63ae792e7d05f1020ac5e964394080c"}, - {file = "rpds_py-0.24.0-cp313-cp313t-win32.whl", hash = "sha256:8ebc7e65ca4b111d928b669713865f021b7773350eeac4a31d3e70144297baba"}, - {file = "rpds_py-0.24.0-cp313-cp313t-win_amd64.whl", hash = "sha256:675269d407a257b8c00a6b58205b72eec8231656506c56fd429d924ca00bb350"}, - {file = "rpds_py-0.24.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a36b452abbf29f68527cf52e181fced56685731c86b52e852053e38d8b60bc8d"}, - {file = "rpds_py-0.24.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b3b397eefecec8e8e39fa65c630ef70a24b09141a6f9fc17b3c3a50bed6b50e"}, - {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdabcd3beb2a6dca7027007473d8ef1c3b053347c76f685f5f060a00327b8b65"}, - {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5db385bacd0c43f24be92b60c857cf760b7f10d8234f4bd4be67b5b20a7c0b6b"}, - {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8097b3422d020ff1c44effc40ae58e67d93e60d540a65649d2cdaf9466030791"}, - {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:493fe54318bed7d124ce272fc36adbf59d46729659b2c792e87c3b95649cdee9"}, - {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8aa362811ccdc1f8dadcc916c6d47e554169ab79559319ae9fae7d7752d0d60c"}, - {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d8f9a6e7fd5434817526815f09ea27f2746c4a51ee11bb3439065f5fc754db58"}, - {file = "rpds_py-0.24.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8205ee14463248d3349131bb8099efe15cd3ce83b8ef3ace63c7e976998e7124"}, - {file = "rpds_py-0.24.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:921ae54f9ecba3b6325df425cf72c074cd469dea843fb5743a26ca7fb2ccb149"}, - {file = "rpds_py-0.24.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:32bab0a56eac685828e00cc2f5d1200c548f8bc11f2e44abf311d6b548ce2e45"}, - {file = "rpds_py-0.24.0-cp39-cp39-win32.whl", hash = "sha256:f5c0ed12926dec1dfe7d645333ea59cf93f4d07750986a586f511c0bc61fe103"}, - {file = "rpds_py-0.24.0-cp39-cp39-win_amd64.whl", hash = "sha256:afc6e35f344490faa8276b5f2f7cbf71f88bc2cda4328e00553bd451728c571f"}, - {file = "rpds_py-0.24.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:619ca56a5468f933d940e1bf431c6f4e13bef8e688698b067ae68eb4f9b30e3a"}, - {file = "rpds_py-0.24.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:4b28e5122829181de1898c2c97f81c0b3246d49f585f22743a1246420bb8d399"}, - {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e5ab32cf9eb3647450bc74eb201b27c185d3857276162c101c0f8c6374e098"}, - {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:208b3a70a98cf3710e97cabdc308a51cd4f28aa6e7bb11de3d56cd8b74bab98d"}, - {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbc4362e06f950c62cad3d4abf1191021b2ffaf0b31ac230fbf0526453eee75e"}, - {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ebea2821cdb5f9fef44933617be76185b80150632736f3d76e54829ab4a3b4d1"}, - {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a4df06c35465ef4d81799999bba810c68d29972bf1c31db61bfdb81dd9d5bb"}, - {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d3aa13bdf38630da298f2e0d77aca967b200b8cc1473ea05248f6c5e9c9bdb44"}, - {file = "rpds_py-0.24.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:041f00419e1da7a03c46042453598479f45be3d787eb837af382bfc169c0db33"}, - {file = "rpds_py-0.24.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:d8754d872a5dfc3c5bf9c0e059e8107451364a30d9fd50f1f1a85c4fb9481164"}, - {file = "rpds_py-0.24.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:896c41007931217a343eff197c34513c154267636c8056fb409eafd494c3dcdc"}, - {file = "rpds_py-0.24.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:92558d37d872e808944c3c96d0423b8604879a3d1c86fdad508d7ed91ea547d5"}, - {file = "rpds_py-0.24.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f9e0057a509e096e47c87f753136c9b10d7a91842d8042c2ee6866899a717c0d"}, - {file = "rpds_py-0.24.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6e109a454412ab82979c5b1b3aee0604eca4bbf9a02693bb9df027af2bfa91a"}, - {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc1c892b1ec1f8cbd5da8de287577b455e388d9c328ad592eabbdcb6fc93bee5"}, - {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c39438c55983d48f4bb3487734d040e22dad200dab22c41e331cee145e7a50d"}, - {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d7e8ce990ae17dda686f7e82fd41a055c668e13ddcf058e7fb5e9da20b57793"}, - {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9ea7f4174d2e4194289cb0c4e172d83e79a6404297ff95f2875cf9ac9bced8ba"}, - {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb2954155bb8f63bb19d56d80e5e5320b61d71084617ed89efedb861a684baea"}, - {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04f2b712a2206e13800a8136b07aaedc23af3facab84918e7aa89e4be0260032"}, - {file = "rpds_py-0.24.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:eda5c1e2a715a4cbbca2d6d304988460942551e4e5e3b7457b50943cd741626d"}, - {file = "rpds_py-0.24.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:9abc80fe8c1f87218db116016de575a7998ab1629078c90840e8d11ab423ee25"}, - {file = "rpds_py-0.24.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6a727fd083009bc83eb83d6950f0c32b3c94c8b80a9b667c87f4bd1274ca30ba"}, - {file = "rpds_py-0.24.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e0f3ef95795efcd3b2ec3fe0a5bcfb5dadf5e3996ea2117427e524d4fbf309c6"}, - {file = "rpds_py-0.24.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:2c13777ecdbbba2077670285dd1fe50828c8742f6a4119dbef6f83ea13ad10fb"}, - {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79e8d804c2ccd618417e96720ad5cd076a86fa3f8cb310ea386a3e6229bae7d1"}, - {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd822f019ccccd75c832deb7aa040bb02d70a92eb15a2f16c7987b7ad4ee8d83"}, - {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0047638c3aa0dbcd0ab99ed1e549bbf0e142c9ecc173b6492868432d8989a046"}, - {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5b66d1b201cc71bc3081bc2f1fc36b0c1f268b773e03bbc39066651b9e18391"}, - {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbcbb6db5582ea33ce46a5d20a5793134b5365110d84df4e30b9d37c6fd40ad3"}, - {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:63981feca3f110ed132fd217bf7768ee8ed738a55549883628ee3da75bb9cb78"}, - {file = "rpds_py-0.24.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:3a55fc10fdcbf1a4bd3c018eea422c52cf08700cf99c28b5cb10fe97ab77a0d3"}, - {file = "rpds_py-0.24.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:c30ff468163a48535ee7e9bf21bd14c7a81147c0e58a36c1078289a8ca7af0bd"}, - {file = "rpds_py-0.24.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:369d9c6d4c714e36d4a03957b4783217a3ccd1e222cdd67d464a3a479fc17796"}, - {file = "rpds_py-0.24.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:24795c099453e3721fda5d8ddd45f5dfcc8e5a547ce7b8e9da06fecc3832e26f"}, - {file = "rpds_py-0.24.0.tar.gz", hash = "sha256:772cc1b2cd963e7e17e6cc55fe0371fb9c704d63e44cacec7b9b7f523b78919e"}, + {file = "rpds_py-0.25.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:f4ad628b5174d5315761b67f212774a32f5bad5e61396d38108bd801c0a8f5d9"}, + {file = "rpds_py-0.25.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8c742af695f7525e559c16f1562cf2323db0e3f0fbdcabdf6865b095256b2d40"}, + {file = "rpds_py-0.25.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:605ffe7769e24b1800b4d024d24034405d9404f0bc2f55b6db3362cd34145a6f"}, + {file = "rpds_py-0.25.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ccc6f3ddef93243538be76f8e47045b4aad7a66a212cd3a0f23e34469473d36b"}, + {file = "rpds_py-0.25.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f70316f760174ca04492b5ab01be631a8ae30cadab1d1081035136ba12738cfa"}, + {file = "rpds_py-0.25.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1dafef8df605fdb46edcc0bf1573dea0d6d7b01ba87f85cd04dc855b2b4479e"}, + {file = "rpds_py-0.25.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0701942049095741a8aeb298a31b203e735d1c61f4423511d2b1a41dcd8a16da"}, + {file = "rpds_py-0.25.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e87798852ae0b37c88babb7f7bbbb3e3fecc562a1c340195b44c7e24d403e380"}, + {file = "rpds_py-0.25.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3bcce0edc1488906c2d4c75c94c70a0417e83920dd4c88fec1078c94843a6ce9"}, + {file = "rpds_py-0.25.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e2f6a2347d3440ae789505693a02836383426249d5293541cd712e07e7aecf54"}, + {file = "rpds_py-0.25.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4fd52d3455a0aa997734f3835cbc4c9f32571345143960e7d7ebfe7b5fbfa3b2"}, + {file = "rpds_py-0.25.1-cp310-cp310-win32.whl", hash = "sha256:3f0b1798cae2bbbc9b9db44ee068c556d4737911ad53a4e5093d09d04b3bbc24"}, + {file = "rpds_py-0.25.1-cp310-cp310-win_amd64.whl", hash = "sha256:3ebd879ab996537fc510a2be58c59915b5dd63bccb06d1ef514fee787e05984a"}, + {file = "rpds_py-0.25.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5f048bbf18b1f9120685c6d6bb70cc1a52c8cc11bdd04e643d28d3be0baf666d"}, + {file = "rpds_py-0.25.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fbb0dbba559959fcb5d0735a0f87cdbca9e95dac87982e9b95c0f8f7ad10255"}, + {file = "rpds_py-0.25.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4ca54b9cf9d80b4016a67a0193ebe0bcf29f6b0a96f09db942087e294d3d4c2"}, + {file = "rpds_py-0.25.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ee3e26eb83d39b886d2cb6e06ea701bba82ef30a0de044d34626ede51ec98b0"}, + {file = "rpds_py-0.25.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89706d0683c73a26f76a5315d893c051324d771196ae8b13e6ffa1ffaf5e574f"}, + {file = "rpds_py-0.25.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2013ee878c76269c7b557a9a9c042335d732e89d482606990b70a839635feb7"}, + {file = "rpds_py-0.25.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45e484db65e5380804afbec784522de84fa95e6bb92ef1bd3325d33d13efaebd"}, + {file = "rpds_py-0.25.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:48d64155d02127c249695abb87d39f0faf410733428d499867606be138161d65"}, + {file = "rpds_py-0.25.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:048893e902132fd6548a2e661fb38bf4896a89eea95ac5816cf443524a85556f"}, + {file = "rpds_py-0.25.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0317177b1e8691ab5879f4f33f4b6dc55ad3b344399e23df2e499de7b10a548d"}, + {file = "rpds_py-0.25.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bffcf57826d77a4151962bf1701374e0fc87f536e56ec46f1abdd6a903354042"}, + {file = "rpds_py-0.25.1-cp311-cp311-win32.whl", hash = "sha256:cda776f1967cb304816173b30994faaf2fd5bcb37e73118a47964a02c348e1bc"}, + {file = "rpds_py-0.25.1-cp311-cp311-win_amd64.whl", hash = "sha256:dc3c1ff0abc91444cd20ec643d0f805df9a3661fcacf9c95000329f3ddf268a4"}, + {file = "rpds_py-0.25.1-cp311-cp311-win_arm64.whl", hash = "sha256:5a3ddb74b0985c4387719fc536faced33cadf2172769540c62e2a94b7b9be1c4"}, + {file = "rpds_py-0.25.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b5ffe453cde61f73fea9430223c81d29e2fbf412a6073951102146c84e19e34c"}, + {file = "rpds_py-0.25.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:115874ae5e2fdcfc16b2aedc95b5eef4aebe91b28e7e21951eda8a5dc0d3461b"}, + {file = "rpds_py-0.25.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a714bf6e5e81b0e570d01f56e0c89c6375101b8463999ead3a93a5d2a4af91fa"}, + {file = "rpds_py-0.25.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:35634369325906bcd01577da4c19e3b9541a15e99f31e91a02d010816b49bfda"}, + {file = "rpds_py-0.25.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4cb2b3ddc16710548801c6fcc0cfcdeeff9dafbc983f77265877793f2660309"}, + {file = "rpds_py-0.25.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9ceca1cf097ed77e1a51f1dbc8d174d10cb5931c188a4505ff9f3e119dfe519b"}, + {file = "rpds_py-0.25.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c2cd1a4b0c2b8c5e31ffff50d09f39906fe351389ba143c195566056c13a7ea"}, + {file = "rpds_py-0.25.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1de336a4b164c9188cb23f3703adb74a7623ab32d20090d0e9bf499a2203ad65"}, + {file = "rpds_py-0.25.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9fca84a15333e925dd59ce01da0ffe2ffe0d6e5d29a9eeba2148916d1824948c"}, + {file = "rpds_py-0.25.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:88ec04afe0c59fa64e2f6ea0dd9657e04fc83e38de90f6de201954b4d4eb59bd"}, + {file = "rpds_py-0.25.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a8bd2f19e312ce3e1d2c635618e8a8d8132892bb746a7cf74780a489f0f6cdcb"}, + {file = "rpds_py-0.25.1-cp312-cp312-win32.whl", hash = "sha256:e5e2f7280d8d0d3ef06f3ec1b4fd598d386cc6f0721e54f09109a8132182fbfe"}, + {file = "rpds_py-0.25.1-cp312-cp312-win_amd64.whl", hash = "sha256:db58483f71c5db67d643857404da360dce3573031586034b7d59f245144cc192"}, + {file = "rpds_py-0.25.1-cp312-cp312-win_arm64.whl", hash = "sha256:6d50841c425d16faf3206ddbba44c21aa3310a0cebc3c1cdfc3e3f4f9f6f5728"}, + {file = "rpds_py-0.25.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:659d87430a8c8c704d52d094f5ba6fa72ef13b4d385b7e542a08fc240cb4a559"}, + {file = "rpds_py-0.25.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68f6f060f0bbdfb0245267da014d3a6da9be127fe3e8cc4a68c6f833f8a23bb1"}, + {file = "rpds_py-0.25.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:083a9513a33e0b92cf6e7a6366036c6bb43ea595332c1ab5c8ae329e4bcc0a9c"}, + {file = "rpds_py-0.25.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:816568614ecb22b18a010c7a12559c19f6fe993526af88e95a76d5a60b8b75fb"}, + {file = "rpds_py-0.25.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c6564c0947a7f52e4792983f8e6cf9bac140438ebf81f527a21d944f2fd0a40"}, + {file = "rpds_py-0.25.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c4a128527fe415d73cf1f70a9a688d06130d5810be69f3b553bf7b45e8acf79"}, + {file = "rpds_py-0.25.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a49e1d7a4978ed554f095430b89ecc23f42014a50ac385eb0c4d163ce213c325"}, + {file = "rpds_py-0.25.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d74ec9bc0e2feb81d3f16946b005748119c0f52a153f6db6a29e8cd68636f295"}, + {file = "rpds_py-0.25.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3af5b4cc10fa41e5bc64e5c198a1b2d2864337f8fcbb9a67e747e34002ce812b"}, + {file = "rpds_py-0.25.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:79dc317a5f1c51fd9c6a0c4f48209c6b8526d0524a6904fc1076476e79b00f98"}, + {file = "rpds_py-0.25.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1521031351865e0181bc585147624d66b3b00a84109b57fcb7a779c3ec3772cd"}, + {file = "rpds_py-0.25.1-cp313-cp313-win32.whl", hash = "sha256:5d473be2b13600b93a5675d78f59e63b51b1ba2d0476893415dfbb5477e65b31"}, + {file = "rpds_py-0.25.1-cp313-cp313-win_amd64.whl", hash = "sha256:a7b74e92a3b212390bdce1d93da9f6488c3878c1d434c5e751cbc202c5e09500"}, + {file = "rpds_py-0.25.1-cp313-cp313-win_arm64.whl", hash = "sha256:dd326a81afe332ede08eb39ab75b301d5676802cdffd3a8f287a5f0b694dc3f5"}, + {file = "rpds_py-0.25.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:a58d1ed49a94d4183483a3ce0af22f20318d4a1434acee255d683ad90bf78129"}, + {file = "rpds_py-0.25.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f251bf23deb8332823aef1da169d5d89fa84c89f67bdfb566c49dea1fccfd50d"}, + {file = "rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dbd586bfa270c1103ece2109314dd423df1fa3d9719928b5d09e4840cec0d72"}, + {file = "rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6d273f136e912aa101a9274c3145dcbddbe4bac560e77e6d5b3c9f6e0ed06d34"}, + {file = "rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:666fa7b1bd0a3810a7f18f6d3a25ccd8866291fbbc3c9b912b917a6715874bb9"}, + {file = "rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:921954d7fbf3fccc7de8f717799304b14b6d9a45bbeec5a8d7408ccbf531faf5"}, + {file = "rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3d86373ff19ca0441ebeb696ef64cb58b8b5cbacffcda5a0ec2f3911732a194"}, + {file = "rpds_py-0.25.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c8980cde3bb8575e7c956a530f2c217c1d6aac453474bf3ea0f9c89868b531b6"}, + {file = "rpds_py-0.25.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8eb8c84ecea987a2523e057c0d950bcb3f789696c0499290b8d7b3107a719d78"}, + {file = "rpds_py-0.25.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:e43a005671a9ed5a650f3bc39e4dbccd6d4326b24fb5ea8be5f3a43a6f576c72"}, + {file = "rpds_py-0.25.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:58f77c60956501a4a627749a6dcb78dac522f249dd96b5c9f1c6af29bfacfb66"}, + {file = "rpds_py-0.25.1-cp313-cp313t-win32.whl", hash = "sha256:2cb9e5b5e26fc02c8a4345048cd9998c2aca7c2712bd1b36da0c72ee969a3523"}, + {file = "rpds_py-0.25.1-cp313-cp313t-win_amd64.whl", hash = "sha256:401ca1c4a20cc0510d3435d89c069fe0a9ae2ee6495135ac46bdd49ec0495763"}, + {file = "rpds_py-0.25.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:ce4c8e485a3c59593f1a6f683cf0ea5ab1c1dc94d11eea5619e4fb5228b40fbd"}, + {file = "rpds_py-0.25.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d8222acdb51a22929c3b2ddb236b69c59c72af4019d2cba961e2f9add9b6e634"}, + {file = "rpds_py-0.25.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4593c4eae9b27d22df41cde518b4b9e4464d139e4322e2127daa9b5b981b76be"}, + {file = "rpds_py-0.25.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd035756830c712b64725a76327ce80e82ed12ebab361d3a1cdc0f51ea21acb0"}, + {file = "rpds_py-0.25.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:114a07e85f32b125404f28f2ed0ba431685151c037a26032b213c882f26eb908"}, + {file = "rpds_py-0.25.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dec21e02e6cc932538b5203d3a8bd6aa1480c98c4914cb88eea064ecdbc6396a"}, + {file = "rpds_py-0.25.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09eab132f41bf792c7a0ea1578e55df3f3e7f61888e340779b06050a9a3f16e9"}, + {file = "rpds_py-0.25.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c98f126c4fc697b84c423e387337d5b07e4a61e9feac494362a59fd7a2d9ed80"}, + {file = "rpds_py-0.25.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0e6a327af8ebf6baba1c10fadd04964c1965d375d318f4435d5f3f9651550f4a"}, + {file = "rpds_py-0.25.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:bc120d1132cff853ff617754196d0ac0ae63befe7c8498bd67731ba368abe451"}, + {file = "rpds_py-0.25.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:140f61d9bed7839446bdd44852e30195c8e520f81329b4201ceead4d64eb3a9f"}, + {file = "rpds_py-0.25.1-cp39-cp39-win32.whl", hash = "sha256:9c006f3aadeda131b438c3092124bd196b66312f0caa5823ef09585a669cf449"}, + {file = "rpds_py-0.25.1-cp39-cp39-win_amd64.whl", hash = "sha256:a61d0b2c7c9a0ae45732a77844917b427ff16ad5464b4d4f5e4adb955f582890"}, + {file = "rpds_py-0.25.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b24bf3cd93d5b6ecfbedec73b15f143596c88ee249fa98cefa9a9dc9d92c6f28"}, + {file = "rpds_py-0.25.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:0eb90e94f43e5085623932b68840b6f379f26db7b5c2e6bcef3179bd83c9330f"}, + {file = "rpds_py-0.25.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d50e4864498a9ab639d6d8854b25e80642bd362ff104312d9770b05d66e5fb13"}, + {file = "rpds_py-0.25.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c9409b47ba0650544b0bb3c188243b83654dfe55dcc173a86832314e1a6a35d"}, + {file = "rpds_py-0.25.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:796ad874c89127c91970652a4ee8b00d56368b7e00d3477f4415fe78164c8000"}, + {file = "rpds_py-0.25.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:85608eb70a659bf4c1142b2781083d4b7c0c4e2c90eff11856a9754e965b2540"}, + {file = "rpds_py-0.25.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4feb9211d15d9160bc85fa72fed46432cdc143eb9cf6d5ca377335a921ac37b"}, + {file = "rpds_py-0.25.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ccfa689b9246c48947d31dd9d8b16d89a0ecc8e0e26ea5253068efb6c542b76e"}, + {file = "rpds_py-0.25.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:3c5b317ecbd8226887994852e85de562f7177add602514d4ac40f87de3ae45a8"}, + {file = "rpds_py-0.25.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:454601988aab2c6e8fd49e7634c65476b2b919647626208e376afcd22019eeb8"}, + {file = "rpds_py-0.25.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:1c0c434a53714358532d13539272db75a5ed9df75a4a090a753ac7173ec14e11"}, + {file = "rpds_py-0.25.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f73ce1512e04fbe2bc97836e89830d6b4314c171587a99688082d090f934d20a"}, + {file = "rpds_py-0.25.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ee86d81551ec68a5c25373c5643d343150cc54672b5e9a0cafc93c1870a53954"}, + {file = "rpds_py-0.25.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89c24300cd4a8e4a51e55c31a8ff3918e6651b241ee8876a42cc2b2a078533ba"}, + {file = "rpds_py-0.25.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:771c16060ff4e79584dc48902a91ba79fd93eade3aa3a12d6d2a4aadaf7d542b"}, + {file = "rpds_py-0.25.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:785ffacd0ee61c3e60bdfde93baa6d7c10d86f15655bd706c89da08068dc5038"}, + {file = "rpds_py-0.25.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a40046a529cc15cef88ac5ab589f83f739e2d332cb4d7399072242400ed68c9"}, + {file = "rpds_py-0.25.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:85fc223d9c76cabe5d0bff82214459189720dc135db45f9f66aa7cffbf9ff6c1"}, + {file = "rpds_py-0.25.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0be9965f93c222fb9b4cc254235b3b2b215796c03ef5ee64f995b1b69af0762"}, + {file = "rpds_py-0.25.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8378fa4a940f3fb509c081e06cb7f7f2adae8cf46ef258b0e0ed7519facd573e"}, + {file = "rpds_py-0.25.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:33358883a4490287e67a2c391dfaea4d9359860281db3292b6886bf0be3d8692"}, + {file = "rpds_py-0.25.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1d1fadd539298e70cac2f2cb36f5b8a65f742b9b9f1014dd4ea1f7785e2470bf"}, + {file = "rpds_py-0.25.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:9a46c2fb2545e21181445515960006e85d22025bd2fe6db23e76daec6eb689fe"}, + {file = "rpds_py-0.25.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:50f2c501a89c9a5f4e454b126193c5495b9fb441a75b298c60591d8a2eb92e1b"}, + {file = "rpds_py-0.25.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7d779b325cc8238227c47fbc53964c8cc9a941d5dbae87aa007a1f08f2f77b23"}, + {file = "rpds_py-0.25.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:036ded36bedb727beeabc16dc1dad7cb154b3fa444e936a03b67a86dc6a5066e"}, + {file = "rpds_py-0.25.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:245550f5a1ac98504147cba96ffec8fabc22b610742e9150138e5d60774686d7"}, + {file = "rpds_py-0.25.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff7c23ba0a88cb7b104281a99476cccadf29de2a0ef5ce864959a52675b1ca83"}, + {file = "rpds_py-0.25.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e37caa8cdb3b7cf24786451a0bdb853f6347b8b92005eeb64225ae1db54d1c2b"}, + {file = "rpds_py-0.25.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f2f48ab00181600ee266a095fe815134eb456163f7d6699f525dee471f312cf"}, + {file = "rpds_py-0.25.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9e5fc7484fa7dce57e25063b0ec9638ff02a908304f861d81ea49273e43838c1"}, + {file = "rpds_py-0.25.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:d3c10228d6cf6fe2b63d2e7985e94f6916fa46940df46b70449e9ff9297bd3d1"}, + {file = "rpds_py-0.25.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:5d9e40f32745db28c1ef7aad23f6fc458dc1e29945bd6781060f0d15628b8ddf"}, + {file = "rpds_py-0.25.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:35a8d1a24b5936b35c5003313bc177403d8bdef0f8b24f28b1c4a255f94ea992"}, + {file = "rpds_py-0.25.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:6099263f526efff9cf3883dfef505518730f7a7a93049b1d90d42e50a22b4793"}, + {file = "rpds_py-0.25.1.tar.gz", hash = "sha256:8960b6dac09b62dac26e75d7e2c4a22efb835d827a7278c34f72b2b84fa160e3"}, ] [[package]] @@ -5556,18 +5576,18 @@ pyasn1 = ">=0.1.3" [[package]] name = "ruamel-yaml" -version = "0.18.10" +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.7" +python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "ruamel.yaml-0.18.10-py3-none-any.whl", hash = "sha256:30f22513ab2301b3d2b577adc121c6471f28734d3d9728581245f1e76468b4f1"}, - {file = "ruamel.yaml-0.18.10.tar.gz", hash = "sha256:20c86ab29ac2153f80a428e1254a8adf686d3383df04490514ca3b79a362db58"}, + {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.13\""} +"ruamel.yaml.clib" = {version = ">=0.2.7", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.14\""} [package.extras] docs = ["mercurial (>5.7)", "ryd"] @@ -5765,14 +5785,14 @@ doc = ["Sphinx", "sphinx-rtd-theme"] [[package]] name = "setuptools" -version = "79.0.0" +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-79.0.0-py3-none-any.whl", hash = "sha256:b9ab3a104bedb292323f53797b00864e10e434a3ab3906813a7169e4745b912a"}, - {file = "setuptools-79.0.0.tar.gz", hash = "sha256:9828422e7541213b0aacb6e10bbf9dd8febeaa45a48570e09b6d100e063fc9f9"}, + {file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}, + {file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"}, ] [package.extras] @@ -5921,14 +5941,14 @@ test = ["pyshacl", "pytest", "tzdata"] [[package]] name = "std-uritemplate" -version = "2.0.3" +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.3-py3-none-any.whl", hash = "sha256:434df26453bf68c6077879fed6609b2c39e2fc73080e74cd157269d5f8abdb3e"}, - {file = "std_uritemplate-2.0.3.tar.gz", hash = "sha256:ad4cb1d671bcf4a3608b3598c687be4b0929867c53a2d69c105989da6a5a2d4c"}, + {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]] @@ -5948,14 +5968,14 @@ pbr = ">=2.0.0" [[package]] name = "sympy" -version = "1.13.3" +version = "1.14.0" description = "Computer algebra system (CAS) in Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "sympy-1.13.3-py3-none-any.whl", hash = "sha256:54612cf55a62755ee71824ce692986f23c88ffa77207b30c1368eda4a7060f73"}, - {file = "sympy-1.13.3.tar.gz", hash = "sha256:b27fd2c6530e0ab39e275fc9b683895367e51d5da91baa8d3d64db2565fec4d9"}, + {file = "sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5"}, + {file = "sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517"}, ] [package.dependencies] @@ -6061,14 +6081,14 @@ files = [ [[package]] name = "tomlkit" -version = "0.13.2" +version = "0.13.3" description = "Style preserving TOML library" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, - {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, + {file = "tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0"}, + {file = "tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1"}, ] [[package]] @@ -6095,14 +6115,14 @@ telegram = ["requests"] [[package]] name = "typer" -version = "0.15.2" +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.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc"}, - {file = "typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5"}, + {file = "typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855"}, + {file = "typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b"}, ] [package.dependencies] @@ -6113,14 +6133,14 @@ typing-extensions = ">=3.7.4.3" [[package]] name = "typing-extensions" -version = "4.13.2" -description = "Backported and Experimental Type Hints for Python 3.8+" +version = "4.14.0" +description = "Backported and Experimental Type Hints for Python 3.9+" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, - {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, + {file = "typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af"}, + {file = "typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4"}, ] [[package]] @@ -6182,14 +6202,14 @@ files = [ [[package]] name = "uritemplate" -version = "4.1.1" +version = "4.2.0" description = "Implementation of RFC 6570 URI Templates" optional = false -python-versions = ">=3.6" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e"}, - {file = "uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0"}, + {file = "uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686"}, + {file = "uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e"}, ] [[package]] @@ -6331,14 +6351,14 @@ test = ["websockets"] [[package]] name = "werkzeug" -version = "3.0.6" +version = "3.1.3" description = "The comprehensive WSGI web application library." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "werkzeug-3.0.6-py3-none-any.whl", hash = "sha256:1bc0c2310d2fbb07b1dd1105eba2f7af72f322e1e455f2f93c993bee8c8a5f17"}, - {file = "werkzeug-3.0.6.tar.gz", hash = "sha256:a8dd59d4de28ca70471a34cba79bed5f7ef2e036a76b3ab0835474246eb41f8d"}, + {file = "werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e"}, + {file = "werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"}, ] [package.dependencies] @@ -6438,14 +6458,14 @@ files = [ [[package]] name = "xlsxwriter" -version = "3.2.3" +version = "3.2.5" description = "A Python module for creating Excel XLSX files." optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" groups = ["main"] files = [ - {file = "XlsxWriter-3.2.3-py3-none-any.whl", hash = "sha256:593f8296e8a91790c6d0378ab08b064f34a642b3feb787cf6738236bd0a4860d"}, - {file = "xlsxwriter-3.2.3.tar.gz", hash = "sha256:ad6fd41bdcf1b885876b1f6b7087560aecc9ae5a9cc2ba97dcac7ab2e210d3d5"}, + {file = "xlsxwriter-3.2.5-py3-none-any.whl", hash = "sha256:4f4824234e1eaf9d95df9a8fe974585ff91d0f5e3d3f12ace5b71e443c1c6abd"}, + {file = "xlsxwriter-3.2.5.tar.gz", hash = "sha256:7e88469d607cdc920151c0ab3ce9cf1a83992d4b7bc730c5ffdd1a12115a7dbe"}, ] [[package]] @@ -6462,116 +6482,116 @@ files = [ [[package]] name = "yarl" -version = "1.20.0" +version = "1.20.1" description = "Yet another URL library" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "yarl-1.20.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f1f6670b9ae3daedb325fa55fbe31c22c8228f6e0b513772c2e1c623caa6ab22"}, - {file = "yarl-1.20.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85a231fa250dfa3308f3c7896cc007a47bc76e9e8e8595c20b7426cac4884c62"}, - {file = "yarl-1.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a06701b647c9939d7019acdfa7ebbfbb78ba6aa05985bb195ad716ea759a569"}, - {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7595498d085becc8fb9203aa314b136ab0516c7abd97e7d74f7bb4eb95042abe"}, - {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af5607159085dcdb055d5678fc2d34949bd75ae6ea6b4381e784bbab1c3aa195"}, - {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:95b50910e496567434cb77a577493c26bce0f31c8a305135f3bda6a2483b8e10"}, - {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b594113a301ad537766b4e16a5a6750fcbb1497dcc1bc8a4daae889e6402a634"}, - {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:083ce0393ea173cd37834eb84df15b6853b555d20c52703e21fbababa8c129d2"}, - {file = "yarl-1.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f1a350a652bbbe12f666109fbddfdf049b3ff43696d18c9ab1531fbba1c977a"}, - {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fb0caeac4a164aadce342f1597297ec0ce261ec4532bbc5a9ca8da5622f53867"}, - {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:d88cc43e923f324203f6ec14434fa33b85c06d18d59c167a0637164863b8e995"}, - {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e52d6ed9ea8fd3abf4031325dc714aed5afcbfa19ee4a89898d663c9976eb487"}, - {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ce360ae48a5e9961d0c730cf891d40698a82804e85f6e74658fb175207a77cb2"}, - {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:06d06c9d5b5bc3eb56542ceeba6658d31f54cf401e8468512447834856fb0e61"}, - {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c27d98f4e5c4060582f44e58309c1e55134880558f1add7a87c1bc36ecfade19"}, - {file = "yarl-1.20.0-cp310-cp310-win32.whl", hash = "sha256:f4d3fa9b9f013f7050326e165c3279e22850d02ae544ace285674cb6174b5d6d"}, - {file = "yarl-1.20.0-cp310-cp310-win_amd64.whl", hash = "sha256:bc906b636239631d42eb8a07df8359905da02704a868983265603887ed68c076"}, - {file = "yarl-1.20.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fdb5204d17cb32b2de2d1e21c7461cabfacf17f3645e4b9039f210c5d3378bf3"}, - {file = "yarl-1.20.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eaddd7804d8e77d67c28d154ae5fab203163bd0998769569861258e525039d2a"}, - {file = "yarl-1.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:634b7ba6b4a85cf67e9df7c13a7fb2e44fa37b5d34501038d174a63eaac25ee2"}, - {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d409e321e4addf7d97ee84162538c7258e53792eb7c6defd0c33647d754172e"}, - {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ea52f7328a36960ba3231c6677380fa67811b414798a6e071c7085c57b6d20a9"}, - {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c8703517b924463994c344dcdf99a2d5ce9eca2b6882bb640aa555fb5efc706a"}, - {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:077989b09ffd2f48fb2d8f6a86c5fef02f63ffe6b1dd4824c76de7bb01e4f2e2"}, - {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0acfaf1da020253f3533526e8b7dd212838fdc4109959a2c53cafc6db611bff2"}, - {file = "yarl-1.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4230ac0b97ec5eeb91d96b324d66060a43fd0d2a9b603e3327ed65f084e41f8"}, - {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a6a1e6ae21cdd84011c24c78d7a126425148b24d437b5702328e4ba640a8902"}, - {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:86de313371ec04dd2531f30bc41a5a1a96f25a02823558ee0f2af0beaa7ca791"}, - {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dd59c9dd58ae16eaa0f48c3d0cbe6be8ab4dc7247c3ff7db678edecbaf59327f"}, - {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a0bc5e05f457b7c1994cc29e83b58f540b76234ba6b9648a4971ddc7f6aa52da"}, - {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c9471ca18e6aeb0e03276b5e9b27b14a54c052d370a9c0c04a68cefbd1455eb4"}, - {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:40ed574b4df723583a26c04b298b283ff171bcc387bc34c2683235e2487a65a5"}, - {file = "yarl-1.20.0-cp311-cp311-win32.whl", hash = "sha256:db243357c6c2bf3cd7e17080034ade668d54ce304d820c2a58514a4e51d0cfd6"}, - {file = "yarl-1.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:8c12cd754d9dbd14204c328915e23b0c361b88f3cffd124129955e60a4fbfcfb"}, - {file = "yarl-1.20.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e06b9f6cdd772f9b665e5ba8161968e11e403774114420737f7884b5bd7bdf6f"}, - {file = "yarl-1.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b9ae2fbe54d859b3ade40290f60fe40e7f969d83d482e84d2c31b9bff03e359e"}, - {file = "yarl-1.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d12b8945250d80c67688602c891237994d203d42427cb14e36d1a732eda480e"}, - {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:087e9731884621b162a3e06dc0d2d626e1542a617f65ba7cc7aeab279d55ad33"}, - {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69df35468b66c1a6e6556248e6443ef0ec5f11a7a4428cf1f6281f1879220f58"}, - {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b2992fe29002fd0d4cbaea9428b09af9b8686a9024c840b8a2b8f4ea4abc16f"}, - {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c903e0b42aab48abfbac668b5a9d7b6938e721a6341751331bcd7553de2dcae"}, - {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf099e2432131093cc611623e0b0bcc399b8cddd9a91eded8bfb50402ec35018"}, - {file = "yarl-1.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a7f62f5dc70a6c763bec9ebf922be52aa22863d9496a9a30124d65b489ea672"}, - {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:54ac15a8b60382b2bcefd9a289ee26dc0920cf59b05368c9b2b72450751c6eb8"}, - {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:25b3bc0763a7aca16a0f1b5e8ef0f23829df11fb539a1b70476dcab28bd83da7"}, - {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b2586e36dc070fc8fad6270f93242124df68b379c3a251af534030a4a33ef594"}, - {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:866349da9d8c5290cfefb7fcc47721e94de3f315433613e01b435473be63daa6"}, - {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:33bb660b390a0554d41f8ebec5cd4475502d84104b27e9b42f5321c5192bfcd1"}, - {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737e9f171e5a07031cbee5e9180f6ce21a6c599b9d4b2c24d35df20a52fabf4b"}, - {file = "yarl-1.20.0-cp312-cp312-win32.whl", hash = "sha256:839de4c574169b6598d47ad61534e6981979ca2c820ccb77bf70f4311dd2cc64"}, - {file = "yarl-1.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:3d7dbbe44b443b0c4aa0971cb07dcb2c2060e4a9bf8d1301140a33a93c98e18c"}, - {file = "yarl-1.20.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2137810a20b933b1b1b7e5cf06a64c3ed3b4747b0e5d79c9447c00db0e2f752f"}, - {file = "yarl-1.20.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:447c5eadd750db8389804030d15f43d30435ed47af1313303ed82a62388176d3"}, - {file = "yarl-1.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42fbe577272c203528d402eec8bf4b2d14fd49ecfec92272334270b850e9cd7d"}, - {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18e321617de4ab170226cd15006a565d0fa0d908f11f724a2c9142d6b2812ab0"}, - {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4345f58719825bba29895011e8e3b545e6e00257abb984f9f27fe923afca2501"}, - {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d9b980d7234614bc4674468ab173ed77d678349c860c3af83b1fffb6a837ddc"}, - {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af4baa8a445977831cbaa91a9a84cc09debb10bc8391f128da2f7bd070fc351d"}, - {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:123393db7420e71d6ce40d24885a9e65eb1edefc7a5228db2d62bcab3386a5c0"}, - {file = "yarl-1.20.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab47acc9332f3de1b39e9b702d9c916af7f02656b2a86a474d9db4e53ef8fd7a"}, - {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4a34c52ed158f89876cba9c600b2c964dfc1ca52ba7b3ab6deb722d1d8be6df2"}, - {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:04d8cfb12714158abf2618f792c77bc5c3d8c5f37353e79509608be4f18705c9"}, - {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7dc63ad0d541c38b6ae2255aaa794434293964677d5c1ec5d0116b0e308031f5"}, - {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d02b591a64e4e6ca18c5e3d925f11b559c763b950184a64cf47d74d7e41877"}, - {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:95fc9876f917cac7f757df80a5dda9de59d423568460fe75d128c813b9af558e"}, - {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bb769ae5760cd1c6a712135ee7915f9d43f11d9ef769cb3f75a23e398a92d384"}, - {file = "yarl-1.20.0-cp313-cp313-win32.whl", hash = "sha256:70e0c580a0292c7414a1cead1e076c9786f685c1fc4757573d2967689b370e62"}, - {file = "yarl-1.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:4c43030e4b0af775a85be1fa0433119b1565673266a70bf87ef68a9d5ba3174c"}, - {file = "yarl-1.20.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b6c4c3d0d6a0ae9b281e492b1465c72de433b782e6b5001c8e7249e085b69051"}, - {file = "yarl-1.20.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8681700f4e4df891eafa4f69a439a6e7d480d64e52bf460918f58e443bd3da7d"}, - {file = "yarl-1.20.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:84aeb556cb06c00652dbf87c17838eb6d92cfd317799a8092cee0e570ee11229"}, - {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f166eafa78810ddb383e930d62e623d288fb04ec566d1b4790099ae0f31485f1"}, - {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5d3d6d14754aefc7a458261027a562f024d4f6b8a798adb472277f675857b1eb"}, - {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a8f64df8ed5d04c51260dbae3cc82e5649834eebea9eadfd829837b8093eb00"}, - {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d9949eaf05b4d30e93e4034a7790634bbb41b8be2d07edd26754f2e38e491de"}, - {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c366b254082d21cc4f08f522ac201d0d83a8b8447ab562732931d31d80eb2a5"}, - {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91bc450c80a2e9685b10e34e41aef3d44ddf99b3a498717938926d05ca493f6a"}, - {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9c2aa4387de4bc3a5fe158080757748d16567119bef215bec643716b4fbf53f9"}, - {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:d2cbca6760a541189cf87ee54ff891e1d9ea6406079c66341008f7ef6ab61145"}, - {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:798a5074e656f06b9fad1a162be5a32da45237ce19d07884d0b67a0aa9d5fdda"}, - {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f106e75c454288472dbe615accef8248c686958c2e7dd3b8d8ee2669770d020f"}, - {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3b60a86551669c23dc5445010534d2c5d8a4e012163218fc9114e857c0586fdd"}, - {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e429857e341d5e8e15806118e0294f8073ba9c4580637e59ab7b238afca836f"}, - {file = "yarl-1.20.0-cp313-cp313t-win32.whl", hash = "sha256:65a4053580fe88a63e8e4056b427224cd01edfb5f951498bfefca4052f0ce0ac"}, - {file = "yarl-1.20.0-cp313-cp313t-win_amd64.whl", hash = "sha256:53b2da3a6ca0a541c1ae799c349788d480e5144cac47dba0266c7cb6c76151fe"}, - {file = "yarl-1.20.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:119bca25e63a7725b0c9d20ac67ca6d98fa40e5a894bd5d4686010ff73397914"}, - {file = "yarl-1.20.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:35d20fb919546995f1d8c9e41f485febd266f60e55383090010f272aca93edcc"}, - {file = "yarl-1.20.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:484e7a08f72683c0f160270566b4395ea5412b4359772b98659921411d32ad26"}, - {file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d8a3d54a090e0fff5837cd3cc305dd8a07d3435a088ddb1f65e33b322f66a94"}, - {file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f0cf05ae2d3d87a8c9022f3885ac6dea2b751aefd66a4f200e408a61ae9b7f0d"}, - {file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a884b8974729e3899d9287df46f015ce53f7282d8d3340fa0ed57536b440621c"}, - {file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f8d8aa8dd89ffb9a831fedbcb27d00ffd9f4842107d52dc9d57e64cb34073d5c"}, - {file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b4e88d6c3c8672f45a30867817e4537df1bbc6f882a91581faf1f6d9f0f1b5a"}, - {file = "yarl-1.20.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdb77efde644d6f1ad27be8a5d67c10b7f769804fff7a966ccb1da5a4de4b656"}, - {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4ba5e59f14bfe8d261a654278a0f6364feef64a794bd456a8c9e823071e5061c"}, - {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:d0bf955b96ea44ad914bc792c26a0edcd71b4668b93cbcd60f5b0aeaaed06c64"}, - {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:27359776bc359ee6eaefe40cb19060238f31228799e43ebd3884e9c589e63b20"}, - {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:04d9c7a1dc0a26efb33e1acb56c8849bd57a693b85f44774356c92d610369efa"}, - {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:faa709b66ae0e24c8e5134033187a972d849d87ed0a12a0366bedcc6b5dc14a5"}, - {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:44869ee8538208fe5d9342ed62c11cc6a7a1af1b3d0bb79bb795101b6e77f6e0"}, - {file = "yarl-1.20.0-cp39-cp39-win32.whl", hash = "sha256:b7fa0cb9fd27ffb1211cde944b41f5c67ab1c13a13ebafe470b1e206b8459da8"}, - {file = "yarl-1.20.0-cp39-cp39-win_amd64.whl", hash = "sha256:d4fad6e5189c847820288286732075f213eabf81be4d08d6cc309912e62be5b7"}, - {file = "yarl-1.20.0-py3-none-any.whl", hash = "sha256:5d0fe6af927a47a230f31e6004621fd0959eaa915fc62acfafa67ff7229a3124"}, - {file = "yarl-1.20.0.tar.gz", hash = "sha256:686d51e51ee5dfe62dec86e4866ee0e9ed66df700d55c828a615640adc885307"}, + {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] @@ -6581,14 +6601,14 @@ propcache = ">=0.2.1" [[package]] name = "zipp" -version = "3.21.0" +version = "3.23.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.9" groups = ["main", "dev", "docs"] files = [ - {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, - {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, + {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, + {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, ] markers = {dev = "python_version < \"3.10\"", docs = "python_version < \"3.10\""} @@ -6597,10 +6617,10 @@ check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \" 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", "importlib-resources ; python_version < \"3.9\"", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">3.9.1,<3.13" -content-hash = "d72c55b52949ba94f0c68004d5b778edb69514a05bbb7aba8d641b5058a99fd5" +content-hash = "4b0eee5566caf8e9d1e2e6fe8ac37733b29dd4275c2d65ac5291fa3acd514d9e" diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 7d72221ce2..a031d6ff5f 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -2,18 +2,46 @@ All notable changes to the **Prowler SDK** are documented in this file. -## [v5.8.0] (Prowler UNRELEASED) +## [v5.9.0] (Prowler UNRELEASED) ### Added -- Add `storage_geo_redundant_enabled` check for Azure provider. [(#7980)](https://github.com/prowler-cloud/prowler/pull/7980) -- Add `storage_cross_tenant_replication_disabled` check for Azure provider. [(#7977)](https://github.com/prowler-cloud/prowler/pull/7977) +- `storage_smb_channel_encryption_with_secure_algorithm` check for Azure provider [(#8123)](https://github.com/prowler-cloud/prowler/pull/8123) +- `storage_smb_protocol_version_is_latest` check for Azure provider [(#8128)](https://github.com/prowler-cloud/prowler/pull/8128) +- `vm_backup_enabled` check for Azure provider [(#8182)](https://github.com/prowler-cloud/prowler/pull/8182) +- `vm_linux_enforce_ssh_authentication` check for Azure provider [(#8149)](https://github.com/prowler-cloud/prowler/pull/8149) +- `vm_ensure_using_approved_images` check for Azure provider [(#8168)](https://github.com/prowler-cloud/prowler/pull/8168) +- `vm_scaleset_associated_load_balancer` check for Azure provider [(#8181)](https://github.com/prowler-cloud/prowler/pull/8181) + +### Changed + +### Fixed +- Add GitHub provider to lateral panel in documentation and change -h environment variable output [(#8246)](https://github.com/prowler-cloud/prowler/pull/8246) + +--- + +## [v5.8.1] (Prowler 5.8.1) + +### Fixed +- Detect wildcarded ARNs in sts:AssumeRole policy resources [(#8164)](https://github.com/prowler-cloud/prowler/pull/8164) +- List all streams and `firehose_stream_encrypted_at_rest` logic [(#8213)](https://github.com/prowler-cloud/prowler/pull/8213) +- Allow empty values for http_endpoint in templates [(#8184)](https://github.com/prowler-cloud/prowler/pull/8184) +- Convert all Azure Storage models to Pydantic models to avoid serialization issues [(#8222)](https://github.com/prowler-cloud/prowler/pull/8222) + +--- + +## [v5.8.0] (Prowler v5.8.0) + +### Added + +- `storage_geo_redundant_enabled` check for Azure provider [(#7980)](https://github.com/prowler-cloud/prowler/pull/7980) +- `storage_cross_tenant_replication_disabled` check for Azure provider [(#7977)](https://github.com/prowler-cloud/prowler/pull/7977) - CIS 1.11 compliance framework for Kubernetes [(#7790)](https://github.com/prowler-cloud/prowler/pull/7790) - Support `HTTPS_PROXY` and `K8S_SKIP_TLS_VERIFY` in Kubernetes [(#7720)](https://github.com/prowler-cloud/prowler/pull/7720) - Weight for Prowler ThreatScore scoring [(#7795)](https://github.com/prowler-cloud/prowler/pull/7795) -- New check `entra_users_mfa_capable` for M365 provider [(#7734)](https://github.com/prowler-cloud/prowler/pull/7734) -- New check `admincenter_organization_customer_lockbox_enabled` for M365 provider [(#7732)](https://github.com/prowler-cloud/prowler/pull/7732) -- New check `admincenter_external_calendar_sharing_disabled` for M365 provider [(#7733)](https://github.com/prowler-cloud/prowler/pull/7733) -- a level for Prowler ThreatScore in the accordion in Dashboard [(#7739)](https://github.com/prowler-cloud/prowler/pull/7739) +- `entra_users_mfa_capable` check for M365 provider [(#7734)](https://github.com/prowler-cloud/prowler/pull/7734) +- `admincenter_organization_customer_lockbox_enabled` check for M365 provider [(#7732)](https://github.com/prowler-cloud/prowler/pull/7732) +- `admincenter_external_calendar_sharing_disabled` check for M365 provider [(#7733)](https://github.com/prowler-cloud/prowler/pull/7733) +- Level for Prowler ThreatScore in the accordion in Dashboard [(#7739)](https://github.com/prowler-cloud/prowler/pull/7739) - CIS 4.0 compliance framework for GCP [(7785)](https://github.com/prowler-cloud/prowler/pull/7785) - `repository_has_codeowners_file` check for GitHub provider [(#7752)](https://github.com/prowler-cloud/prowler/pull/7752) - `repository_default_branch_requires_signed_commits` check for GitHub provider [(#7777)](https://github.com/prowler-cloud/prowler/pull/7777) @@ -31,13 +59,32 @@ All notable changes to the **Prowler SDK** are documented in this file. - Avoid bypassing IAM check using wildcards [(#7708)](https://github.com/prowler-cloud/prowler/pull/7708) - `storage_blob_versioning_is_enabled` new check for Azure provider [(#7927)](https://github.com/prowler-cloud/prowler/pull/7927) - New method to authenticate in AppInsights in check `app_function_application_insights_enabled` [(#7763)](https://github.com/prowler-cloud/prowler/pull/7763) -- ISO 27001 2022 for M365 provider. [(#7985)](https://github.com/prowler-cloud/prowler/pull/7985) +- ISO 27001 2022 for M365 provider [(#7985)](https://github.com/prowler-cloud/prowler/pull/7985) - `codebuild_project_uses_allowed_github_organizations` check for AWS provider [(#7595)](https://github.com/prowler-cloud/prowler/pull/7595) - IaC provider [(#7852)](https://github.com/prowler-cloud/prowler/pull/7852) - Azure Databricks service integration for Azure provider, including the `databricks_workspace_vnet_injection_enabled` check [(#8008)](https://github.com/prowler-cloud/prowler/pull/8008) -- Azure Databricks check `databricks_workspace_cmk_encryption_enabled` to ensure workspaces use customer-managed keys (CMK) for encryption at rest [(#8017)](https://github.com/prowler-cloud/prowler/pull/8017) -- Add `storage_account_default_to_entra_authorization_enabled` check for Azure provider. [(#7981)](https://github.com/prowler-cloud/prowler/pull/7981) +- `databricks_workspace_cmk_encryption_enabled` check for Azure provider [(#8017)](https://github.com/prowler-cloud/prowler/pull/8017) +- Appication auth for PowerShell in M365 provider [(#7992)](https://github.com/prowler-cloud/prowler/pull/7992) +- `storage_account_default_to_entra_authorization_enabled` check for Azure provider [(#7981)](https://github.com/prowler-cloud/prowler/pull/7981) +- Improve overview page from Prowler Dashboard [(#8118)](https://github.com/prowler-cloud/prowler/pull/8118) +- `keyvault_ensure_public_network_access_disabled` check for Azure provider [(#8072)](https://github.com/prowler-cloud/prowler/pull/8072) +- `monitor_alert_service_health_exists` check for Azure provider [(#8067)](https://github.com/prowler-cloud/prowler/pull/8067) - Replace `Domain.Read.All` with `Directory.Read.All` in Azure and M365 docs [(#8075)](https://github.com/prowler-cloud/prowler/pull/8075) +- Refactor IaC provider to use Checkov as Python library [(#8093)](https://github.com/prowler-cloud/prowler/pull/8093) +- New check `codebuild_project_not_publicly_accessible` for AWS provider [(#8127)](https://github.com/prowler-cloud/prowler/pull/8127) + +### Fixed +- Consolidate Azure Storage file service properties to the account level, improving the accuracy of the `storage_ensure_file_shares_soft_delete_is_enabled` check [(#8087)](https://github.com/prowler-cloud/prowler/pull/8087) +- Migrate Azure VM service and managed disk logic to Pydantic models for better serialization and type safety, and update all related tests to use the new models and fix UUID handling [(#https://github.com/prowler-cloud/prowler/pull/8151)](https://github.com/prowler-cloud/prowler/pull/https://github.com/prowler-cloud/prowler/pull/8151) +- `organizations_scp_check_deny_regions` check to pass when SCP policies have no statements [(#8091)](https://github.com/prowler-cloud/prowler/pull/8091) +- Fix logic in VPC and ELBv2 checks [(#8077)](https://github.com/prowler-cloud/prowler/pull/8077) +- Retrieve correctly ECS Container insights settings [(#8097)](https://github.com/prowler-cloud/prowler/pull/8097) +- Fix correct handling for different accounts-dates in prowler dashboard compliance page [(#8108)](https://github.com/prowler-cloud/prowler/pull/8108) +- Handling of `block-project-ssh-keys` in GCP check `compute_instance_block_project_wide_ssh_keys_disabled` [(#8115)](https://github.com/prowler-cloud/prowler/pull/8115) +- Handle empty name in Azure Defender and GCP checks [(#8120)](https://github.com/prowler-cloud/prowler/pull/8120) + +### Changed +- Reworked `S3.test_connection` to match the AwsProvider logic [(#8088)](https://github.com/prowler-cloud/prowler/pull/8088) ### Removed - OCSF version number references to point always to the latest [(#8064)](https://github.com/prowler-cloud/prowler/pull/8064) @@ -47,11 +94,11 @@ All notable changes to the **Prowler SDK** are documented in this file. --- -## [v5.7.5] (Prowler UNRELEASED) +## [v5.7.5] (Prowler 5.7.5) ### Fixed - Use unified timestamp for all requirements [(#8059)](https://github.com/prowler-cloud/prowler/pull/8059) -- Add EKS to service without subservices. [(#7959)](https://github.com/prowler-cloud/prowler/pull/7959) +- Add EKS to service without subservices [(#7959)](https://github.com/prowler-cloud/prowler/pull/7959) - `apiserver_strong_ciphers_only` check for K8S provider [(#7952)](https://github.com/prowler-cloud/prowler/pull/7952) - Handle `0` at the start and end of account uids in Prowler Dashboard [(#7955)](https://github.com/prowler-cloud/prowler/pull/7955) - Typo in PCI 4.0 for K8S provider [(#7971)](https://github.com/prowler-cloud/prowler/pull/7971) diff --git a/prowler/config/aws_mutelist_example.yaml b/prowler/config/aws_mutelist_example.yaml index ad2e5d36d4..9c95ee704d 100644 --- a/prowler/config/aws_mutelist_example.yaml +++ b/prowler/config/aws_mutelist_example.yaml @@ -28,6 +28,7 @@ Mutelist: Tags: - "test=test" # Will ignore every resource containing the string "test" and the tags 'test=test' and - "project=test|project=stage" # either of ('project=test' OR project=stage) in account 123456789012 and every region + - "environment=prod" # Will ignore every resource except in account 123456789012 except the ones containing the string "test" and tag environment=prod "*": Checks: @@ -46,9 +47,6 @@ Mutelist: - "*" Tags: - "environment=dev" # Will ignore every resource containing the tag 'environment=dev' in every account and region - - "*": - Checks: "ecs_task_definitions_no_environment_secrets": Regions: - "*" @@ -60,16 +58,3 @@ Mutelist: Regions: - "eu-west-1" - "eu-south-2" # Will ignore every resource in check ecs_task_definitions_no_environment_secrets except the ones in account 0123456789012 located in eu-south-2 or eu-west-1 - - "123456789012": - Checks: - "*": - Regions: - - "*" - Resources: - - "*" - Exceptions: - Resources: - - "test" - Tags: - - "environment=prod" # Will ignore every resource except in account 123456789012 except the ones containing the string "test" and tag environment=prod diff --git a/prowler/config/config.py b/prowler/config/config.py index dbbe71db9e..3311f552a9 100644 --- a/prowler/config/config.py +++ b/prowler/config/config.py @@ -12,7 +12,7 @@ from prowler.lib.logger import logger timestamp = datetime.today() timestamp_utc = datetime.now(timezone.utc).replace(tzinfo=timezone.utc) -prowler_version = "5.8.0" +prowler_version = "5.9.0" html_logo_url = "https://github.com/prowler-cloud/prowler/" square_logo_img = "https://prowler.com/wp-content/uploads/logo-html.png" aws_logo = "https://user-images.githubusercontent.com/38561120/235953920-3e3fba08-0795-41dc-b480-9bea57db9f2e.png" diff --git a/prowler/config/m365_mutelist_example.yaml b/prowler/config/m365_mutelist_example.yaml index 34fda956e0..fea88df6e2 100644 --- a/prowler/config/m365_mutelist_example.yaml +++ b/prowler/config/m365_mutelist_example.yaml @@ -28,9 +28,6 @@ Mutelist: Tags: - "test=test" # Will ignore every resource containing the string "test" and the tags 'test=test' and - "project=test|project=stage" # either of ('project=test' OR project=stage) in Azure subscription 1 and every location - - "*": - Checks: "admincenter_*": Regions: - "*" diff --git a/prowler/lib/check/models.py b/prowler/lib/check/models.py index 59c0d31b4d..bbd5b8ccc1 100644 --- a/prowler/lib/check/models.py +++ b/prowler/lib/check/models.py @@ -7,6 +7,7 @@ from dataclasses import asdict, dataclass, is_dataclass from enum import Enum from typing import Any, Dict, Optional, Set +from checkov.common.output.record import Record from pydantic.v1 import BaseModel, ValidationError, validator from prowler.config.config import Provider @@ -441,6 +442,8 @@ class Check_Report: self.resource = resource.to_dict() elif is_dataclass(resource): self.resource = asdict(resource) + elif hasattr(resource, "__dict__"): + self.resource = resource.__dict__ else: logger.error( f"Resource metadata {type(resource)} in {self.check_metadata.CheckID} could not be converted to dict" @@ -519,7 +522,11 @@ class Check_Report_GCP(Check_Report): or getattr(resource, "name", None) or "" ) - self.resource_name = resource_name or getattr(resource, "name", "") + self.resource_name = ( + resource_name + or getattr(resource, "name", "") + or getattr(resource, "id", "") + ) self.project_id = project_id or getattr(resource, "project_id", "") self.location = ( location @@ -622,7 +629,7 @@ class CheckReportIAC(Check_Report): resource_path: str resource_line_range: str - def __init__(self, metadata: dict = {}, finding: dict = {}) -> None: + def __init__(self, metadata: dict = {}, resource: Record = None) -> None: """ Initialize the IAC Check's finding information from a Checkov failed_check dict. @@ -630,11 +637,10 @@ class CheckReportIAC(Check_Report): metadata (Dict): Optional check metadata (can be None). failed_check (dict): A single failed_check result from Checkov's JSON output. """ - super().__init__(metadata, finding) - - self.resource_name = getattr(finding, "resource", "") - self.resource_path = getattr(finding, "file_path", "") - self.resource_line_range = getattr(finding, "file_line_range", "") + super().__init__(metadata, resource) + self.resource_name = resource.resource + self.resource_path = resource.file_path + self.resource_line_range = resource.file_line_range @dataclass diff --git a/prowler/lib/mutelist/mutelist.py b/prowler/lib/mutelist/mutelist.py index 4940d0202f..f76d51a39f 100644 --- a/prowler/lib/mutelist/mutelist.py +++ b/prowler/lib/mutelist/mutelist.py @@ -439,12 +439,13 @@ class Mutelist(ABC): return False @staticmethod - def validate_mutelist(mutelist: dict) -> dict: + def validate_mutelist(mutelist: dict, raise_on_exception: bool = False) -> dict: """ Validate the mutelist against the schema. Args: mutelist (dict): The mutelist to be validated. + raise_on_exception (bool): Whether to raise an exception if the mutelist is invalid. Returns: dict: The mutelist itself. @@ -453,7 +454,10 @@ class Mutelist(ABC): validate(mutelist, schema=mutelist_schema) return mutelist except Exception as error: - logger.error( - f"{error.__class__.__name__} -- Mutelist YAML is malformed - {error}[{error.__traceback__.tb_lineno}]" - ) + if raise_on_exception: + raise error + else: + logger.error( + f"{error.__class__.__name__} -- Mutelist YAML is malformed - {error}[{error.__traceback__.tb_lineno}]" + ) return {} diff --git a/prowler/lib/outputs/finding.py b/prowler/lib/outputs/finding.py index 21235d7d6f..433f04b041 100644 --- a/prowler/lib/outputs/finding.py +++ b/prowler/lib/outputs/finding.py @@ -286,8 +286,8 @@ class Finding(BaseModel): output_data["auth_method"] = "local" # Until we support remote repos output_data["account_uid"] = "iac" output_data["account_name"] = "iac" - output_data["resource_name"] = check_output.resource["resource"] - output_data["resource_uid"] = check_output.resource["resource"] + output_data["resource_name"] = check_output.resource_name + output_data["resource_uid"] = check_output.resource_name output_data["region"] = check_output.resource_path output_data["resource_line_range"] = check_output.resource_line_range output_data["framework"] = ( diff --git a/prowler/providers/aws/aws_regions_by_service.json b/prowler/providers/aws/aws_regions_by_service.json index ae992196e3..a56854b214 100644 --- a/prowler/providers/aws/aws_regions_by_service.json +++ b/prowler/providers/aws/aws_regions_by_service.json @@ -785,7 +785,10 @@ "ap-southeast-2", "ap-southeast-3", "ap-southeast-4", + "ap-southeast-5", + "ap-southeast-7", "ca-central-1", + "ca-west-1", "eu-central-1", "eu-central-2", "eu-north-1", @@ -948,6 +951,7 @@ "aws": [ "af-south-1", "ap-east-1", + "ap-east-2", "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", @@ -1333,7 +1337,9 @@ "aws": [ "us-east-1" ], - "aws-cn": [], + "aws-cn": [ + "cn-northwest-1" + ], "aws-us-gov": [] } }, @@ -1979,6 +1985,7 @@ "eu-central-2", "eu-north-1", "eu-south-1", + "eu-south-2", "eu-west-1", "eu-west-2", "eu-west-3", @@ -2782,6 +2789,9 @@ "regions": { "aws": [ "af-south-1", + "ap-northeast-1", + "ap-northeast-2", + "ap-southeast-1", "ap-southeast-2", "ca-central-1", "eu-central-1", @@ -3397,6 +3407,7 @@ "eu-west-1", "eu-west-2", "eu-west-3", + "il-central-1", "me-central-1", "sa-east-1", "us-east-1", @@ -3457,6 +3468,7 @@ "aws": [ "af-south-1", "ap-east-1", + "ap-east-2", "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", @@ -3502,6 +3514,7 @@ "regions": { "aws": [ "ap-northeast-1", + "ap-northeast-2", "ap-northeast-3", "eu-west-1", "eu-west-2", @@ -4983,6 +4996,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", @@ -5465,25 +5480,37 @@ "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-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", + "me-central-1", "me-south-1", + "mx-central-1", "sa-east-1", "us-east-1", "us-east-2", "us-west-1", "us-west-2" ], - "aws-cn": [], + "aws-cn": [ + "cn-north-1", + "cn-northwest-1" + ], "aws-us-gov": [ "us-gov-east-1", "us-gov-west-1" @@ -5499,18 +5526,27 @@ "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-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", + "me-central-1", "me-south-1", + "mx-central-1", "sa-east-1", "us-east-1", "us-east-2", @@ -6231,6 +6267,7 @@ "aws": [ "af-south-1", "ap-east-1", + "ap-east-2", "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", @@ -7825,13 +7862,18 @@ "regions": { "aws": [ "ap-northeast-1", + "ap-northeast-2", + "ap-northeast-3", + "ap-south-1", "ap-southeast-1", "ap-southeast-2", + "ca-central-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-1", @@ -7926,6 +7968,7 @@ "sa-east-1", "us-east-1", "us-east-2", + "us-west-1", "us-west-2" ], "aws-cn": [], @@ -8103,6 +8146,8 @@ "regions": { "aws": [ "ap-northeast-1", + "ap-northeast-3", + "ap-south-1", "ap-southeast-1", "eu-central-1", "eu-west-1", @@ -8205,7 +8250,10 @@ "us-west-2" ], "aws-cn": [], - "aws-us-gov": [] + "aws-us-gov": [ + "us-gov-east-1", + "us-gov-west-1" + ] } }, "personalize": { @@ -8843,6 +8891,7 @@ "aws": [ "af-south-1", "ap-east-1", + "ap-east-2", "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", @@ -9803,6 +9852,13 @@ ] } }, + "sagemakerautopilot": { + "regions": { + "aws": [], + "aws-cn": [], + "aws-us-gov": [] + } + }, "savingsplans": { "regions": { "aws": [ @@ -9885,7 +9941,10 @@ "cn-north-1", "cn-northwest-1" ], - "aws-us-gov": [] + "aws-us-gov": [ + "us-gov-east-1", + "us-gov-west-1" + ] } }, "schemas": { @@ -10303,17 +10362,20 @@ "ap-northeast-2", "ap-northeast-3", "ap-south-1", + "ap-south-2", "ap-southeast-1", "ap-southeast-2", "ap-southeast-3", "ca-central-1", "eu-central-1", + "eu-central-2", "eu-north-1", "eu-south-1", "eu-west-1", "eu-west-2", "eu-west-3", "il-central-1", + "me-central-1", "me-south-1", "sa-east-1", "us-east-1", @@ -10336,17 +10398,20 @@ "ap-northeast-2", "ap-northeast-3", "ap-south-1", + "ap-south-2", "ap-southeast-1", "ap-southeast-2", "ap-southeast-3", "ca-central-1", "eu-central-1", + "eu-central-2", "eu-north-1", "eu-south-1", "eu-west-1", "eu-west-2", "eu-west-3", "il-central-1", + "me-central-1", "me-south-1", "sa-east-1", "us-east-1", diff --git a/prowler/providers/aws/lib/s3/s3.py b/prowler/providers/aws/lib/s3/s3.py index 83347566bb..95c800cc5c 100644 --- a/prowler/providers/aws/lib/s3/s3.py +++ b/prowler/providers/aws/lib/s3/s3.py @@ -1,12 +1,39 @@ +import os import tempfile from os import path from tempfile import NamedTemporaryFile from typing import Optional -from botocore import exceptions +from boto3.session import Session +from botocore.exceptions import ClientError, NoCredentialsError, ProfileNotFound from prowler.lib.logger import logger from prowler.lib.outputs.output import Output +from prowler.providers.aws.aws_provider import AwsProvider +from prowler.providers.aws.config import ( + AWS_STS_GLOBAL_ENDPOINT_REGION, + ROLE_SESSION_NAME, +) +from prowler.providers.aws.exceptions.exceptions import ( + AWSAccessKeyIDInvalidError, + AWSArgumentTypeValidationError, + AWSAssumeRoleError, + AWSIAMRoleARNEmptyResourceError, + AWSIAMRoleARNInvalidAccountIDError, + AWSIAMRoleARNInvalidResourceTypeError, + AWSIAMRoleARNPartitionEmptyError, + AWSIAMRoleARNRegionNotEmtpyError, + AWSIAMRoleARNServiceNotIAMnorSTSError, + AWSNoCredentialsError, + AWSProfileNotFoundError, + AWSSecretAccessKeyInvalidError, + AWSSessionTokenExpiredError, + AWSSetUpSessionError, +) +from prowler.providers.aws.lib.arguments.arguments import ( + validate_role_session_name, + validate_session_duration, +) from prowler.providers.aws.lib.s3.exceptions.exceptions import ( S3BucketAccessDeniedError, S3ClientError, @@ -14,8 +41,11 @@ from prowler.providers.aws.lib.s3.exceptions.exceptions import ( S3InvalidBucketNameError, S3TestConnectionError, ) -from prowler.providers.aws.lib.session.aws_set_up_session import AwsSetUpSession -from prowler.providers.aws.models import AWSIdentityInfo, AWSSession +from prowler.providers.aws.lib.session.aws_set_up_session import ( + AwsSetUpSession, + parse_iam_credentials_arn, +) +from prowler.providers.aws.models import AWSAssumeRoleInfo, AWSIdentityInfo, AWSSession from prowler.providers.common.models import Connection @@ -220,7 +250,18 @@ class S3: @staticmethod def test_connection( - session, bucket_name: str, raise_on_exception: bool = True + bucket_name: str, + profile: str = None, + aws_region: str = AWS_STS_GLOBAL_ENDPOINT_REGION, + role_arn: str = None, + role_session_name: str = ROLE_SESSION_NAME, + session_duration: int = 3600, + external_id: str = None, + mfa_enabled: bool = False, + raise_on_exception: bool = True, + aws_access_key_id: str = None, + aws_secret_access_key: str = None, + aws_session_token: Optional[str] = None, ) -> Connection: """ Test the connection to the S3 bucket. @@ -236,7 +277,39 @@ class S3: Raises: - Exception: An exception indicating that the connection test failed. """ + # TODO: Refactor this method, the AWSProvider.test_connection() and the SecurityHubProvider.test_connection() are similar. try: + session = AwsProvider.setup_session( + mfa=mfa_enabled, + profile=profile, + aws_access_key_id=aws_access_key_id, + aws_secret_access_key=aws_secret_access_key, + aws_session_token=aws_session_token, + ) + + if role_arn: + session_duration = validate_session_duration(session_duration) + role_session_name = validate_role_session_name(role_session_name) + role_arn = parse_iam_credentials_arn(role_arn) + assumed_role_information = AWSAssumeRoleInfo( + role_arn=role_arn, + session_duration=session_duration, + external_id=external_id, + mfa_enabled=mfa_enabled, + role_session_name=role_session_name, + ) + assumed_role_credentials = AwsProvider.assume_role( + session, + assumed_role_information, + ) + session = Session( + aws_access_key_id=assumed_role_credentials.aws_access_key_id, + aws_secret_access_key=assumed_role_credentials.aws_secret_access_key, + aws_session_token=assumed_role_credentials.aws_session_token, + region_name=aws_region, + profile_name=profile, + ) + s3_client = session.client(__class__.__name__.lower()) if "s3://" in bucket_name: bucket_name = bucket_name.removeprefix("s3://") @@ -273,7 +346,125 @@ class S3: ) return Connection(is_connected=True) - except exceptions.ClientError as client_error: + except AWSSetUpSessionError as setup_session_error: + logger.error( + f"{setup_session_error.__class__.__name__}[{setup_session_error.__traceback__.tb_lineno}]: {setup_session_error}" + ) + if raise_on_exception: + raise setup_session_error + return Connection(error=setup_session_error) + + except AWSArgumentTypeValidationError as validation_error: + logger.error( + f"{validation_error.__class__.__name__}[{validation_error.__traceback__.tb_lineno}]: {validation_error}" + ) + if raise_on_exception: + raise validation_error + return Connection(error=validation_error) + + except AWSIAMRoleARNRegionNotEmtpyError as arn_region_not_empty_error: + logger.error( + f"{arn_region_not_empty_error.__class__.__name__}[{arn_region_not_empty_error.__traceback__.tb_lineno}]: {arn_region_not_empty_error}" + ) + if raise_on_exception: + raise arn_region_not_empty_error + return Connection(error=arn_region_not_empty_error) + + except AWSIAMRoleARNPartitionEmptyError as arn_partition_empty_error: + logger.error( + f"{arn_partition_empty_error.__class__.__name__}[{arn_partition_empty_error.__traceback__.tb_lineno}]: {arn_partition_empty_error}" + ) + if raise_on_exception: + raise arn_partition_empty_error + return Connection(error=arn_partition_empty_error) + + except AWSIAMRoleARNServiceNotIAMnorSTSError as arn_service_not_iam_sts_error: + logger.error( + f"{arn_service_not_iam_sts_error.__class__.__name__}[{arn_service_not_iam_sts_error.__traceback__.tb_lineno}]: {arn_service_not_iam_sts_error}" + ) + if raise_on_exception: + raise arn_service_not_iam_sts_error + return Connection(error=arn_service_not_iam_sts_error) + + except AWSIAMRoleARNInvalidAccountIDError as arn_invalid_account_id_error: + logger.error( + f"{arn_invalid_account_id_error.__class__.__name__}[{arn_invalid_account_id_error.__traceback__.tb_lineno}]: {arn_invalid_account_id_error}" + ) + if raise_on_exception: + raise arn_invalid_account_id_error + return Connection(error=arn_invalid_account_id_error) + + except AWSIAMRoleARNInvalidResourceTypeError as arn_invalid_resource_type_error: + logger.error( + f"{arn_invalid_resource_type_error.__class__.__name__}[{arn_invalid_resource_type_error.__traceback__.tb_lineno}]: {arn_invalid_resource_type_error}" + ) + if raise_on_exception: + raise arn_invalid_resource_type_error + return Connection(error=arn_invalid_resource_type_error) + + except AWSIAMRoleARNEmptyResourceError as arn_empty_resource_error: + logger.error( + f"{arn_empty_resource_error.__class__.__name__}[{arn_empty_resource_error.__traceback__.tb_lineno}]: {arn_empty_resource_error}" + ) + if raise_on_exception: + raise arn_empty_resource_error + return Connection(error=arn_empty_resource_error) + + except AWSAssumeRoleError as assume_role_error: + logger.error( + f"{assume_role_error.__class__.__name__}[{assume_role_error.__traceback__.tb_lineno}]: {assume_role_error}" + ) + if raise_on_exception: + raise assume_role_error + return Connection(error=assume_role_error) + + except ProfileNotFound as profile_not_found_error: + logger.error( + f"AWSProfileNotFoundError[{profile_not_found_error.__traceback__.tb_lineno}]: {profile_not_found_error}" + ) + if raise_on_exception: + raise AWSProfileNotFoundError( + file=os.path.basename(__file__), + original_exception=profile_not_found_error, + ) from profile_not_found_error + return Connection(error=profile_not_found_error) + + except NoCredentialsError as no_credentials_error: + logger.error( + f"AWSNoCredentialsError[{no_credentials_error.__traceback__.tb_lineno}]: {no_credentials_error}" + ) + if raise_on_exception: + raise AWSNoCredentialsError( + file=os.path.basename(__file__), + original_exception=no_credentials_error, + ) from no_credentials_error + return Connection(error=no_credentials_error) + + except AWSAccessKeyIDInvalidError as access_key_id_invalid_error: + logger.error( + f"{access_key_id_invalid_error.__class__.__name__}[{access_key_id_invalid_error.__traceback__.tb_lineno}]: {access_key_id_invalid_error}" + ) + if raise_on_exception: + raise access_key_id_invalid_error + return Connection(error=access_key_id_invalid_error) + + except AWSSecretAccessKeyInvalidError as secret_access_key_invalid_error: + logger.error( + f"{secret_access_key_invalid_error.__class__.__name__}[{secret_access_key_invalid_error.__traceback__.tb_lineno}]: {secret_access_key_invalid_error}" + ) + if raise_on_exception: + raise secret_access_key_invalid_error + return Connection(error=secret_access_key_invalid_error) + + except AWSSessionTokenExpiredError as session_token_expired: + logger.error( + f"{session_token_expired.__class__.__name__}[{session_token_expired.__traceback__.tb_lineno}]: {session_token_expired}" + ) + if raise_on_exception: + raise session_token_expired + return Connection(error=session_token_expired) + + except ClientError as client_error: if raise_on_exception: if ( "specified bucket does not exist" @@ -291,9 +482,9 @@ class S3: raise S3BucketAccessDeniedError(original_exception=client_error) else: raise S3ClientError(original_exception=client_error) - return Connection(is_connected=False, error=client_error) + return Connection(error=client_error) except Exception as error: if raise_on_exception: raise S3TestConnectionError(original_exception=error) - return False + return Connection(error=error) diff --git a/prowler/providers/aws/services/codebuild/codebuild_project_not_publicly_accessible/__init__.py b/prowler/providers/aws/services/codebuild/codebuild_project_not_publicly_accessible/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/codebuild/codebuild_project_not_publicly_accessible/codebuild_project_not_publicly_accessible.metadata.json b/prowler/providers/aws/services/codebuild/codebuild_project_not_publicly_accessible/codebuild_project_not_publicly_accessible.metadata.json new file mode 100644 index 0000000000..5d7427df8e --- /dev/null +++ b/prowler/providers/aws/services/codebuild/codebuild_project_not_publicly_accessible/codebuild_project_not_publicly_accessible.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "aws", + "CheckID": "codebuild_project_not_publicly_accessible", + "CheckTitle": "Ensure AWS CodeBuild projects are not public", + "CheckType": [], + "ServiceName": "codebuild", + "SubServiceName": "", + "ResourceIdTemplate": "arn:aws:codebuild:region:account-id:project:project-name", + "Severity": "high", + "ResourceType": "AwsCodeBuildProject", + "Description": "Check for CodeBuild projects ensuring that the project visibility is appropriate", + "Risk": "Public CodeBuild Project ensures all build logs and artifacts are available to the public. Environment variables, source code, and other sensitive information may have been output to the build logs and artifacts. You must be careful about what information is output to the build logs.", + "RelatedUrl": "", + "Remediation": { + "Code": { + "NativeIaC": "", + "Terraform": "", + "CLI": "aws codebuild update-project --name --project-visibility PRIVATE", + "Other": "" + }, + "Recommendation": { + "Text": "Ensure that all CodeBuild projects are private to avoid fact gathering about builds from an Attacker.", + "Url": "https://docs.aws.amazon.com/codebuild/latest/userguide/public-builds.html" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/codebuild/codebuild_project_not_publicly_accessible/codebuild_project_not_publicly_accessible.py b/prowler/providers/aws/services/codebuild/codebuild_project_not_publicly_accessible/codebuild_project_not_publicly_accessible.py new file mode 100644 index 0000000000..6fd42e971c --- /dev/null +++ b/prowler/providers/aws/services/codebuild/codebuild_project_not_publicly_accessible/codebuild_project_not_publicly_accessible.py @@ -0,0 +1,26 @@ +from typing import List + +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.codebuild.codebuild_client import codebuild_client + + +class codebuild_project_not_publicly_accessible(Check): + def execute(self) -> List[Check_Report_AWS]: + findings = [] + + projects = codebuild_client.projects + for arn, project in projects.items(): + report = Check_Report_AWS(self.metadata(), resource=project) + report.resource_id = project.name + report.resource_arn = arn + report.region = project.region + report.status = "FAIL" + report.status_extended = f"CodeBuild project {project.name} is public." + + if project.project_visibility == "PRIVATE": + report.status = "PASS" + report.status_extended = f"CodeBuild project {project.name} is private." + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/codebuild/codebuild_service.py b/prowler/providers/aws/services/codebuild/codebuild_service.py index c7210a67dd..475f578e94 100644 --- a/prowler/providers/aws/services/codebuild/codebuild_service.py +++ b/prowler/providers/aws/services/codebuild/codebuild_service.py @@ -121,6 +121,7 @@ class Codebuild(AWSService): ) project.tags = project_info.get("tags", []) project.service_role_arn = project_info.get("serviceRole", "") + project.project_visibility = project_info.get("projectVisibility", "") except Exception as error: logger.error( f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" @@ -222,6 +223,7 @@ class Project(BaseModel): s3_logs: Optional[s3Logs] cloudwatch_logs: Optional[CloudWatchLogs] tags: Optional[list] + project_visibility: Optional[str] = None class ExportConfig(BaseModel): diff --git a/prowler/providers/aws/services/ec2/ec2_launch_template_imdsv2_required/ec2_launch_template_imdsv2_required.py b/prowler/providers/aws/services/ec2/ec2_launch_template_imdsv2_required/ec2_launch_template_imdsv2_required.py index a974128061..b89c7a821a 100644 --- a/prowler/providers/aws/services/ec2/ec2_launch_template_imdsv2_required/ec2_launch_template_imdsv2_required.py +++ b/prowler/providers/aws/services/ec2/ec2_launch_template_imdsv2_required/ec2_launch_template_imdsv2_required.py @@ -18,7 +18,10 @@ class ec2_launch_template_imdsv2_required(Check): and version.template_data.http_tokens == "required" ): versions_with_imdsv2_required.append(str(version.version_number)) - elif version.template_data.http_endpoint == "disabled": + elif ( + version.template_data.http_endpoint == "disabled" + or not version.template_data.http_endpoint + ): versions_with_metadata_disabled.append(str(version.version_number)) else: versions_with_no_imdsv2.append(str(version.version_number)) diff --git a/prowler/providers/aws/services/ecs/ecs_cluster_container_insights_enabled/ecs_cluster_container_insights_enabled.py b/prowler/providers/aws/services/ecs/ecs_cluster_container_insights_enabled/ecs_cluster_container_insights_enabled.py index 59a18ff779..c113e72a60 100644 --- a/prowler/providers/aws/services/ecs/ecs_cluster_container_insights_enabled/ecs_cluster_container_insights_enabled.py +++ b/prowler/providers/aws/services/ecs/ecs_cluster_container_insights_enabled/ecs_cluster_container_insights_enabled.py @@ -13,11 +13,10 @@ class ecs_cluster_container_insights_enabled(Check): ) if cluster.settings: for setting in cluster.settings: - if ( - setting["name"] == "containerInsights" - and setting["value"] == "enabled" + if setting["name"] == "containerInsights" and ( + setting["value"] == "enabled" or setting["value"] == "enhanced" ): report.status = "PASS" - report.status_extended = f"ECS cluster {cluster.name} has container insights enabled." + report.status_extended = f"ECS cluster {cluster.name} has container insights {setting['value']}." findings.append(report) return findings diff --git a/prowler/providers/aws/services/ecs/ecs_service.py b/prowler/providers/aws/services/ecs/ecs_service.py index 811f2a26b1..560125bf58 100644 --- a/prowler/providers/aws/services/ecs/ecs_service.py +++ b/prowler/providers/aws/services/ecs/ecs_service.py @@ -175,6 +175,7 @@ class ECS(AWSService): clusters=[cluster.arn], include=[ "TAGS", + "SETTINGS", ], ) cluster.settings = response["clusters"][0].get("settings", []) diff --git a/prowler/providers/aws/services/elbv2/elbv2_desync_mitigation_mode/elbv2_desync_mitigation_mode.py b/prowler/providers/aws/services/elbv2/elbv2_desync_mitigation_mode/elbv2_desync_mitigation_mode.py index e0212c133e..a80b20d524 100644 --- a/prowler/providers/aws/services/elbv2/elbv2_desync_mitigation_mode/elbv2_desync_mitigation_mode.py +++ b/prowler/providers/aws/services/elbv2/elbv2_desync_mitigation_mode/elbv2_desync_mitigation_mode.py @@ -12,7 +12,7 @@ class elbv2_desync_mitigation_mode(Check): report.status_extended = f"ELBv2 ALB {lb.name} is configured with correct desync mitigation mode." if ( lb.desync_mitigation_mode != "strictest" - or lb.desync_mitigation_mode != "defensive" + and lb.desync_mitigation_mode != "defensive" ): if lb.drop_invalid_header_fields == "false": report.status = "FAIL" diff --git a/prowler/providers/aws/services/firehose/firehose_service.py b/prowler/providers/aws/services/firehose/firehose_service.py index d496da9c4e..0ec1c95242 100644 --- a/prowler/providers/aws/services/firehose/firehose_service.py +++ b/prowler/providers/aws/services/firehose/firehose_service.py @@ -25,18 +25,47 @@ class Firehose(AWSService): def _list_delivery_streams(self, regional_client): logger.info("Firehose - Listing delivery streams...") try: - for stream_name in regional_client.list_delivery_streams()[ - "DeliveryStreamNames" - ]: - stream_arn = f"arn:{self.audited_partition}:firehose:{regional_client.region}:{self.audited_account}:deliverystream/{stream_name}" - if not self.audit_resources or ( - is_resource_filtered(stream_arn, self.audit_resources) - ): - self.delivery_streams[stream_arn] = DeliveryStream( - arn=stream_arn, - name=stream_name, - region=regional_client.region, + # Manual pagination using ExclusiveStartDeliveryStreamName + # This ensures we get all streams alphabetically without duplicates + exclusive_start_delivery_stream_name = None + processed_streams = set() + + while True: + kwargs = {} + if exclusive_start_delivery_stream_name: + kwargs["ExclusiveStartDeliveryStreamName"] = ( + exclusive_start_delivery_stream_name ) + + response = regional_client.list_delivery_streams(**kwargs) + stream_names = response.get("DeliveryStreamNames", []) + + for stream_name in stream_names: + if stream_name in processed_streams: + continue + + processed_streams.add(stream_name) + stream_arn = f"arn:{self.audited_partition}:firehose:{regional_client.region}:{self.audited_account}:deliverystream/{stream_name}" + + if not self.audit_resources or ( + is_resource_filtered(stream_arn, self.audit_resources) + ): + self.delivery_streams[stream_arn] = DeliveryStream( + arn=stream_arn, + name=stream_name, + region=regional_client.region, + ) + + if not response.get("HasMoreDeliveryStreams", False): + break + + # Set the starting point for the next page (last stream name from current batch) + # ExclusiveStartDeliveryStreamName will start after this stream alphabetically + if stream_names: + exclusive_start_delivery_stream_name = stream_names[-1] + else: + break + except ClientError as error: logger.error( f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" @@ -61,13 +90,45 @@ class Firehose(AWSService): describe_stream = self.regional_clients[ stream.region ].describe_delivery_stream(DeliveryStreamName=stream.name) + encryption_config = describe_stream.get( "DeliveryStreamDescription", {} ).get("DeliveryStreamEncryptionConfiguration", {}) + stream.kms_encryption = EncryptionStatus( encryption_config.get("Status", "DISABLED") ) stream.kms_key_arn = encryption_config.get("KeyARN", "") + + stream.delivery_stream_type = describe_stream.get( + "DeliveryStreamDescription", {} + ).get("DeliveryStreamType", "") + + source_config = describe_stream.get("DeliveryStreamDescription", {}).get( + "Source", {} + ) + stream.source = Source( + direct_put=DirectPutSourceDescription( + troughput_hint_in_mb_per_sec=source_config.get( + "DirectPutSourceDescription", {} + ).get("TroughputHintInMBPerSec", 0) + ), + kinesis_stream=KinesisStreamSourceDescription( + kinesis_stream_arn=source_config.get( + "KinesisStreamSourceDescription", {} + ).get("KinesisStreamARN", "") + ), + msk=MSKSourceDescription( + msk_cluster_arn=source_config.get("MSKSourceDescription", {}).get( + "MSKClusterARN", "" + ) + ), + database=DatabaseSourceDescription( + endpoint=source_config.get("DatabaseSourceDescription", {}).get( + "Endpoint", "" + ) + ), + ) except ClientError as error: logger.error( f"{stream.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" @@ -85,6 +146,39 @@ class EncryptionStatus(Enum): DISABLING_FAILED = "DISABLING_FAILED" +class DirectPutSourceDescription(BaseModel): + """Model for the DirectPut source of a Firehose stream""" + + troughput_hint_in_mb_per_sec: int = Field(default_factory=int) + + +class KinesisStreamSourceDescription(BaseModel): + """Model for the KinesisStream source of a Firehose stream""" + + kinesis_stream_arn: str = Field(default_factory=str) + + +class MSKSourceDescription(BaseModel): + """Model for the MSK source of a Firehose stream""" + + msk_cluster_arn: str = Field(default_factory=str) + + +class DatabaseSourceDescription(BaseModel): + """Model for the Database source of a Firehose stream""" + + endpoint: str = Field(default_factory=str) + + +class Source(BaseModel): + """Model for the source of a Firehose stream""" + + direct_put: Optional[DirectPutSourceDescription] + kinesis_stream: Optional[KinesisStreamSourceDescription] + msk: Optional[MSKSourceDescription] + database: Optional[DatabaseSourceDescription] + + class DeliveryStream(BaseModel): """Model for a Firehose Delivery Stream""" @@ -94,3 +188,5 @@ class DeliveryStream(BaseModel): kms_key_arn: Optional[str] = Field(default_factory=str) kms_encryption: Optional[str] = Field(default_factory=str) tags: Optional[List[Dict[str, str]]] = Field(default_factory=list) + delivery_stream_type: Optional[str] = Field(default_factory=str) + source: Source = Field(default_factory=Source) diff --git a/prowler/providers/aws/services/firehose/firehose_stream_encrypted_at_rest/firehose_stream_encrypted_at_rest.py b/prowler/providers/aws/services/firehose/firehose_stream_encrypted_at_rest/firehose_stream_encrypted_at_rest.py index 8154a84b66..417d893260 100644 --- a/prowler/providers/aws/services/firehose/firehose_stream_encrypted_at_rest/firehose_stream_encrypted_at_rest.py +++ b/prowler/providers/aws/services/firehose/firehose_stream_encrypted_at_rest/firehose_stream_encrypted_at_rest.py @@ -3,6 +3,8 @@ from typing import List from prowler.lib.check.models import Check, Check_Report_AWS from prowler.providers.aws.services.firehose.firehose_client import firehose_client from prowler.providers.aws.services.firehose.firehose_service import EncryptionStatus +from prowler.providers.aws.services.kinesis.kinesis_client import kinesis_client +from prowler.providers.aws.services.kinesis.kinesis_service import EncryptionType class firehose_stream_encrypted_at_rest(Check): @@ -22,14 +24,22 @@ class firehose_stream_encrypted_at_rest(Check): findings = [] for stream in firehose_client.delivery_streams.values(): report = Check_Report_AWS(metadata=self.metadata(), resource=stream) - report.status = "PASS" - report.status_extended = ( - f"Firehose Stream {stream.name} does have at rest encryption enabled." - ) + report.status = "FAIL" + report.status_extended = f"Firehose Stream {stream.name} does not have at rest encryption enabled or the source stream is not encrypted." - if stream.kms_encryption != EncryptionStatus.ENABLED: - report.status = "FAIL" - report.status_extended = f"Firehose Stream {stream.name} does not have at rest encryption enabled." + # Encrypted Kinesis Stream source + if stream.delivery_stream_type == "KinesisStreamAsSource": + source_stream = kinesis_client.streams.get( + stream.source.kinesis_stream.kinesis_stream_arn + ) + if source_stream.encrypted_at_rest != EncryptionType.NONE: + report.status = "PASS" + report.status_extended = f"Firehose Stream {stream.name} does not have at rest encryption enabled but the source stream {source_stream.name} has at rest encryption enabled." + + # Check if the stream has encryption enabled directly + elif stream.kms_encryption == EncryptionStatus.ENABLED: + report.status = "PASS" + report.status_extended = f"Firehose Stream {stream.name} does have at rest encryption enabled." findings.append(report) 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 881e362d22..48142c7068 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 @@ -5,6 +5,14 @@ from prowler.providers.aws.services.iam.iam_client import iam_client class iam_no_custom_policy_permissive_role_assumption(Check): def execute(self) -> Check_Report_AWS: findings = [] + + def resource_has_wildcard(resource): + if isinstance(resource, str): + return "*" in resource + if isinstance(resource, list): + return any("*" in r for r in resource) + return False + for policy in iam_client.policies: # Check only custom policies if policy.type == "Custom": @@ -12,6 +20,7 @@ class iam_no_custom_policy_permissive_role_assumption(Check): report.region = iam_client.region report.status = "PASS" report.status_extended = f"Custom Policy {policy.name} does not allow permissive STS Role assumption." + if policy.document: if not isinstance(policy.document["Statement"], list): policy_statements = [policy.document["Statement"]] @@ -19,30 +28,23 @@ class iam_no_custom_policy_permissive_role_assumption(Check): policy_statements = policy.document["Statement"] for statement in policy_statements: if ( - statement["Effect"] == "Allow" + statement.get("Effect") == "Allow" and "Action" in statement and "Resource" in statement - and "*" in statement["Resource"] + and resource_has_wildcard(statement["Resource"]) ): - if isinstance(statement["Action"], list): - for action in statement["Action"]: - if ( - action == "sts:AssumeRole" - or action == "sts:*" - or action == "*" - ): - report.status = "FAIL" - report.status_extended = f"Custom Policy {policy.name} allows permissive STS Role assumption." - break - else: - if ( - statement["Action"] == "sts:AssumeRole" - or statement["Action"] == "sts:*" - or statement["Action"] == "*" - ): + actions = ( + statement["Action"] + if isinstance(statement["Action"], list) + else [statement["Action"]] + ) + for action in actions: + if action in ["sts:AssumeRole", "sts:*", "*"]: report.status = "FAIL" report.status_extended = f"Custom Policy {policy.name} allows permissive STS Role assumption." - break + break + if report.status == "FAIL": + break findings.append(report) diff --git a/prowler/providers/aws/services/organizations/organizations_scp_check_deny_regions/organizations_scp_check_deny_regions.py b/prowler/providers/aws/services/organizations/organizations_scp_check_deny_regions/organizations_scp_check_deny_regions.py index 8de3f30cdd..cced82a762 100644 --- a/prowler/providers/aws/services/organizations/organizations_scp_check_deny_regions/organizations_scp_check_deny_regions.py +++ b/prowler/providers/aws/services/organizations/organizations_scp_check_deny_regions/organizations_scp_check_deny_regions.py @@ -34,9 +34,9 @@ class organizations_scp_check_deny_regions(Check): "SERVICE_CONTROL_POLICY", [] ): # Statements are not always list - statements = policy.content.get("Statement") - if type(policy.content["Statement"]) is not list: - statements = [policy.content.get("Statement")] + statements = policy.content.get("Statement", []) + if type(statements) is not list: + statements = [statements] for statement in statements: # Deny if Condition = {"StringNotEquals": {"aws:RequestedRegion": [region1, region2]}} diff --git a/prowler/providers/aws/services/vpc/vpc_endpoint_multi_az_enabled/vpc_endpoint_multi_az_enabled.py b/prowler/providers/aws/services/vpc/vpc_endpoint_multi_az_enabled/vpc_endpoint_multi_az_enabled.py index 2a79594172..203522c274 100644 --- a/prowler/providers/aws/services/vpc/vpc_endpoint_multi_az_enabled/vpc_endpoint_multi_az_enabled.py +++ b/prowler/providers/aws/services/vpc/vpc_endpoint_multi_az_enabled/vpc_endpoint_multi_az_enabled.py @@ -9,10 +9,10 @@ class vpc_endpoint_multi_az_enabled(Check): if endpoint.vpc_id in vpc_client.vpcs and endpoint.type == "Interface": report = Check_Report_AWS(metadata=self.metadata(), resource=endpoint) report.status = "FAIL" - report.status_extended = f"VPC Endpoint {endpoint.id} in VPC {endpoint.vpc_id} has subnets in different AZs." + report.status_extended = f"VPC Endpoint {endpoint.id} in VPC {endpoint.vpc_id} does not have subnets in different AZs." if len(endpoint.subnet_ids) > 1: report.status = "PASS" - report.status_extended = f"VPC Endpoint {endpoint.id} in VPC {endpoint.vpc_id} does not have subnets in different AZs." + report.status_extended = f"VPC Endpoint {endpoint.id} in VPC {endpoint.vpc_id} has subnets in different AZs." findings.append(report) diff --git a/prowler/providers/azure/services/defender/defender_service.py b/prowler/providers/azure/services/defender/defender_service.py index 697df3f692..2f6eaeb05d 100644 --- a/prowler/providers/azure/services/defender/defender_service.py +++ b/prowler/providers/azure/services/defender/defender_service.py @@ -161,7 +161,8 @@ class Defender(AzureService): { security_contact_default.name: SecurityContacts( resource_id=security_contact_default.id, - name=getattr(security_contact_default, "name", "default"), + name=getattr(security_contact_default, "name", "default") + or "default", emails=security_contact_default.emails, phone=security_contact_default.phone, alert_notifications_minimal_severity=security_contact_default.alert_notifications.minimal_severity, diff --git a/prowler/providers/azure/services/keyvault/keyvault_access_only_through_private_endpoints/__init__.py b/prowler/providers/azure/services/keyvault/keyvault_access_only_through_private_endpoints/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/azure/services/keyvault/keyvault_access_only_through_private_endpoints/keyvault_access_only_through_private_endpoints.metadata.json b/prowler/providers/azure/services/keyvault/keyvault_access_only_through_private_endpoints/keyvault_access_only_through_private_endpoints.metadata.json new file mode 100644 index 0000000000..2e1fe0e59c --- /dev/null +++ b/prowler/providers/azure/services/keyvault/keyvault_access_only_through_private_endpoints/keyvault_access_only_through_private_endpoints.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "azure", + "CheckID": "keyvault_access_only_through_private_endpoints", + "CheckTitle": "Ensure that public network access when using private endpoint is disabled.", + "CheckType": [], + "ServiceName": "keyvault", + "SubServiceName": "", + "ResourceIdTemplate": "/subscriptions/{subscription_id}/resourceGroups/{resource_group}/providers/Microsoft.KeyVault/vaults/{vault_name}", + "Severity": "high", + "ResourceType": "KeyVault", + "Description": "Checks if Key Vaults with private endpoints have public network access disabled.", + "Risk": "Allowing public network access to Key Vault when using private endpoint can expose sensitive data to unauthorized access.", + "RelatedUrl": "https://learn.microsoft.com/en-us/azure/key-vault/general/network-security", + "Remediation": { + "Code": { + "CLI": "az keyvault update --resource-group --name --public-network-access disabled", + "NativeIaC": "{\n \"type\": \"Microsoft.KeyVault/vaults\",\n \"apiVersion\": \"2022-07-01\",\n \"properties\": {\n \"publicNetworkAccess\": \"disabled\"\n }\n}", + "Terraform": "resource \"azurerm_key_vault\" \"example\" {\n # ... other configuration ...\n\n public_network_access_enabled = false\n}", + "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/KeyVault/use-private-endpoints.html" + }, + "Recommendation": { + "Text": "Disable public network access for Key Vaults that use private endpoint to ensure network traffic only flows through the private endpoint.", + "Url": "https://learn.microsoft.com/en-us/azure/private-link/private-endpoint-overview" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} 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 new file mode 100644 index 0000000000..1a363f2d61 --- /dev/null +++ b/prowler/providers/azure/services/keyvault/keyvault_access_only_through_private_endpoints/keyvault_access_only_through_private_endpoints.py @@ -0,0 +1,37 @@ +from prowler.lib.check.models import Check, Check_Report_Azure +from prowler.providers.azure.services.keyvault.keyvault_client import keyvault_client + + +class keyvault_access_only_through_private_endpoints(Check): + """ + Ensure that Public Network Access when using Private Endpoint is disabled. + + This check evaluates whether Azure Key Vaults with private endpoints configured have + public network access disabled. Disabling public network access enhances security by + isolating the Key Vault from the public internet, thereby reducing its exposure. + + - PASS: The Key Vault has private endpoints and public network access is disabled. + - FAIL: The Key Vault has private endpoints and public network access is enabled. + """ + + def execute(self) -> Check_Report_Azure: + findings = [] + for subscription, key_vaults in keyvault_client.key_vaults.items(): + for keyvault in key_vaults: + if ( + keyvault.properties + and keyvault.properties.private_endpoint_connections + ): + report = Check_Report_Azure( + metadata=self.metadata(), resource=keyvault + ) + report.subscription = subscription + + 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." + else: + report.status = "FAIL" + report.status_extended = f"Keyvault {keyvault.name} from subscription {subscription} has public network access enabled while using private endpoints." + 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 ccee275282..e63e799017 100644 --- a/prowler/providers/azure/services/keyvault/keyvault_service.py +++ b/prowler/providers/azure/services/keyvault/keyvault_service.py @@ -70,6 +70,14 @@ class KeyVault(AzureService): "enable_purge_protection", False, ), + public_network_access_disabled=( + getattr( + keyvault_properties, + "public_network_access", + "Enabled", + ) + == "Disabled" + ), ), keys=keys, secrets=secrets, @@ -247,6 +255,7 @@ class VaultProperties: private_endpoint_connections: List[PrivateEndpointConnection] enable_soft_delete: bool enable_purge_protection: bool + public_network_access_disabled: bool = False @dataclass diff --git a/prowler/providers/azure/services/monitor/monitor_alert_service_health_exists/__init__.py b/prowler/providers/azure/services/monitor/monitor_alert_service_health_exists/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/azure/services/monitor/monitor_alert_service_health_exists/monitor_alert_service_health_exists.metadata.json b/prowler/providers/azure/services/monitor/monitor_alert_service_health_exists/monitor_alert_service_health_exists.metadata.json new file mode 100644 index 0000000000..4354005d98 --- /dev/null +++ b/prowler/providers/azure/services/monitor/monitor_alert_service_health_exists/monitor_alert_service_health_exists.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "azure", + "CheckID": "monitor_alert_service_health_exists", + "CheckTitle": "Ensure that an Activity Log Alert exists for Service Health", + "CheckType": [], + "ServiceName": "monitor", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "Monitor", + "Description": "Ensure that an Azure activity log alert is configured to trigger when Service Health events occur within your Microsoft Azure cloud account. The alert should activate when new events match the specified conditions in the alert rule configuration.", + "Risk": "Lack of monitoring for Service Health events may result in missing critical service issues, planned maintenance, security advisories, or other changes that could impact Azure services and regions in use.", + "RelatedUrl": "https://learn.microsoft.com/en-us/azure/service-health/overview", + "Remediation": { + "Code": { + "CLI": "az monitor activity-log alert create --subscription --resource-group --name --condition category=ServiceHealth and properties.incidentType=Incident --scope /subscriptions/ --action-group ", + "NativeIaC": "", + "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/ActivityLog/service-health-alert.html", + "Terraform": "" + }, + "Recommendation": { + "Text": "Create an activity log alert for Service Health events and configure an action group to notify appropriate personnel.", + "Url": "https://learn.microsoft.com/en-us/azure/service-health/alerts-activity-log-service-notifications-portal" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "By default, in your Azure subscription there will not be any activity log alerts configured for Service Health events." +} 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 new file mode 100644 index 0000000000..94dc9747c3 --- /dev/null +++ b/prowler/providers/azure/services/monitor/monitor_alert_service_health_exists/monitor_alert_service_health_exists.py @@ -0,0 +1,48 @@ +from prowler.lib.check.models import Check, Check_Report_Azure +from prowler.providers.azure.services.monitor.monitor_client import monitor_client + + +class monitor_alert_service_health_exists(Check): + def execute(self) -> list[Check_Report_Azure]: + findings = [] + + for ( + subscription_name, + activity_log_alerts, + ) in monitor_client.alert_rules.items(): + for alert_rule in activity_log_alerts: + # Check if alert rule is enabled and has required Service Health conditions + if alert_rule.enabled: + has_service_health_category = False + has_incident_type_incident = False + for element in alert_rule.condition.all_of: + if ( + element.field == "category" + and element.equals == "ServiceHealth" + ): + has_service_health_category = True + if ( + element.field == "properties.incidentType" + and element.equals == "Incident" + ): + has_incident_type_incident = True + + if has_service_health_category and has_incident_type_incident: + report = Check_Report_Azure( + metadata=self.metadata(), resource=alert_rule + ) + report.subscription = subscription_name + report.status = "PASS" + report.status_extended = f"There is an activity log alert for Service Health in subscription {subscription_name}." + break + else: + report = Check_Report_Azure(metadata=self.metadata(), resource={}) + report.subscription = subscription_name + report.resource_name = "Monitor" + report.resource_id = "Monitor" + report.status = "FAIL" + report.status_extended = f"There is no activity log alert for Service Health in subscription {subscription_name}." + + findings.append(report) + + return findings diff --git a/prowler/providers/azure/services/monitor/monitor_service.py b/prowler/providers/azure/services/monitor/monitor_service.py index 7f5b9c8232..b4542a7b14 100644 --- a/prowler/providers/azure/services/monitor/monitor_service.py +++ b/prowler/providers/azure/services/monitor/monitor_service.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import List +from typing import List, Optional from azure.mgmt.monitor import MonitorManagementClient @@ -131,4 +131,4 @@ class AlertRule: name: str condition: AlertRuleAllOfCondition enabled: bool - description: str + description: Optional[str] diff --git a/prowler/providers/azure/services/recovery/recovery_client.py b/prowler/providers/azure/services/recovery/recovery_client.py new file mode 100644 index 0000000000..5c17199237 --- /dev/null +++ b/prowler/providers/azure/services/recovery/recovery_client.py @@ -0,0 +1,4 @@ +from prowler.providers.azure.services.recovery.recovery_service import Recovery +from prowler.providers.common.provider import Provider + +recovery_client = Recovery(Provider.get_global_provider()) diff --git a/prowler/providers/azure/services/recovery/recovery_service.py b/prowler/providers/azure/services/recovery/recovery_service.py new file mode 100644 index 0000000000..e39a6fc1f7 --- /dev/null +++ b/prowler/providers/azure/services/recovery/recovery_service.py @@ -0,0 +1,101 @@ +from typing import Optional + +from azure.mgmt.recoveryservices import RecoveryServicesClient +from azure.mgmt.recoveryservicesbackup import RecoveryServicesBackupClient +from azure.mgmt.recoveryservicesbackup.activestamp.models import DataSourceType +from pydantic import BaseModel, Field + +from prowler.lib.logger import logger +from prowler.providers.azure.azure_provider import AzureProvider +from prowler.providers.azure.lib.service.service import AzureService + + +class BackupItem(BaseModel): + """Minimal BackupItem: only essential identifying and descriptive fields.""" + + id: str + name: str + workload_type: Optional[DataSourceType] + + +class BackupVault(BaseModel): + """Minimal BackupVault: only essential identifying fields and its backup items.""" + + id: str + name: str + location: str + backup_protected_items: dict[str, BackupItem] = Field(default_factory=dict) + + +class Recovery(AzureService): + def __init__(self, provider: AzureProvider): + super().__init__(RecoveryServicesClient, provider) + self.vaults: dict[str, dict[str, BackupVault]] = self._get_vaults() + RecoveryBackup(provider, self.vaults) + + def _get_vaults(self) -> dict[str, dict[str, BackupVault]]: + """ + Retrieve all Recovery Services vaults for each subscription. + + Returns: + Nested dictionary of vaults by subscription. + """ + logger.info("Recovery - Getting Recovery Services vaults...") + vaults_dict: dict[str, dict[str, BackupVault]] = {} + try: + vaults_dict: dict[str, dict[str, BackupVault]] = {} + for subscription_name, client in self.clients.items(): + vaults = client.vaults.list_by_subscription_id() + vaults_dict[subscription_name] = {} + 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 + except Exception as error: + logger.error( + f"Subscription name: {subscription_name} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return vaults_dict + + +class RecoveryBackup(AzureService): + def __init__( + self, provider: AzureProvider, vaults: dict[str, dict[str, BackupVault]] + ): + super().__init__(RecoveryServicesBackupClient, provider) + for subscription_name, vaults in vaults.items(): + for vault in vaults.values(): + vault.backup_protected_items = self._get_backup_protected_items( + subscription_name=subscription_name, vault=vault + ) + + def _get_backup_protected_items( + self, subscription_name: str, vault: BackupVault + ) -> dict[str, BackupItem]: + """ + Retrieve all backup protected items for a given vault. + """ + logger.info("Recovery - Getting backup protected items...") + backup_protected_items_dict: dict[str, BackupItem] = {} + try: + backup_protected_items = self.clients[ + subscription_name + ].backup_protected_items.list( + vault_name=vault.name, + resource_group_name=vault.id.split("/")[4], + ) + for item in backup_protected_items: + item_properties = getattr(item, "properties", None) + backup_protected_items_dict[item.id] = BackupItem( + id=item.id, + name=item.name, + workload_type=( + item_properties.workload_type if item_properties else None + ), + ) + except Exception as e: + logger.error(f"Recovery - Error getting backup protected items: {e}") + return backup_protected_items_dict diff --git a/prowler/providers/azure/services/storage/lib/__init__.py b/prowler/providers/azure/services/storage/lib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/azure/services/storage/lib/constants.py b/prowler/providers/azure/services/storage/lib/constants.py new file mode 100644 index 0000000000..225d7bc5e2 --- /dev/null +++ b/prowler/providers/azure/services/storage/lib/constants.py @@ -0,0 +1,3 @@ +"""Constants for the storage service.""" + +LATEST_SMB_VERSION = "SMB3.1.1" 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 9a08809c8e..cf92ee25f3 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 @@ -7,29 +7,24 @@ class storage_ensure_file_shares_soft_delete_is_enabled(Check): findings = [] for subscription, storage_accounts in storage_client.storage_accounts.items(): for storage_account in storage_accounts: - if ( - hasattr(storage_account, "file_shares") - and storage_account.file_shares - ): - for file_share in storage_account.file_shares: - report = Check_Report_Azure( - metadata=self.metadata(), resource=storage_account - ) - report.subscription = subscription - report.resource_id = file_share.name - if file_share.soft_delete_enabled: - report.status = "PASS" - report.status_extended = ( - f"File share {file_share.name} in storage account {storage_account.name} " - f"from subscription {subscription} has soft delete enabled with a retention period of " - f"{file_share.retention_days} days." - ) - else: - report.status = "FAIL" - report.status_extended = ( - f"File share {file_share.name} in storage account {storage_account.name} " - f"from subscription {subscription} does not have soft delete enabled or has an invalid " - f"retention period." - ) - findings.append(report) + if getattr(storage_account, "file_service_properties", None): + report = Check_Report_Azure( + metadata=self.metadata(), + resource=storage_account.file_service_properties, + ) + report.subscription = subscription + report.resource_name = storage_account.name + report.location = storage_account.location + + if ( + 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." + else: + report.status = "FAIL" + report.status_extended = f"File share soft delete is not enabled for storage account {storage_account.name}." + + findings.append(report) + return findings diff --git a/prowler/providers/azure/services/storage/storage_service.py b/prowler/providers/azure/services/storage/storage_service.py index 374409b470..887fb80b7b 100644 --- a/prowler/providers/azure/services/storage/storage_service.py +++ b/prowler/providers/azure/services/storage/storage_service.py @@ -1,8 +1,8 @@ -from dataclasses import dataclass from enum import Enum -from typing import List, Optional +from typing import Optional from azure.mgmt.storage import StorageManagementClient +from pydantic import BaseModel from prowler.lib.logger import logger from prowler.providers.azure.azure_provider import AzureProvider @@ -32,7 +32,7 @@ class Storage(AzureService): resouce_group_name = None key_expiration_period_in_days = None if storage_account.key_policy: - key_expiration_period_in_days = ( + key_expiration_period_in_days = int( storage_account.key_policy.key_expiration_period_in_days ) replication_settings = ReplicationSettings(storage_account.sku.name) @@ -147,73 +147,100 @@ class Storage(AzureService): client = self.clients[subscription] for account in accounts: try: - service_properties = client.file_services.get_service_properties( - account.resouce_group_name, account.name - ) - soft_delete_enabled = False - retention_days = 0 - if ( - hasattr(service_properties, "share_delete_retention_policy") - and service_properties.share_delete_retention_policy - ): - soft_delete_enabled = getattr( - service_properties.share_delete_retention_policy, - "enabled", - False, + file_service_properties = ( + client.file_services.get_service_properties( + account.resouce_group_name, account.name ) - retention_days = ( + ) + share_delete_retention_policy = getattr( + file_service_properties, + "share_delete_retention_policy", + None, + ) + + smb_channel_encryption_raw = getattr( + getattr( getattr( - service_properties.share_delete_retention_policy, + file_service_properties, + "protocol_settings", + None, + ), + "smb", + None, + ), + "channel_encryption", + None, + ) + + smb_supported_versions_raw = getattr( + getattr( + getattr( + file_service_properties, + "protocol_settings", + None, + ), + "smb", + None, + ), + "versions", + None, + ) + + account.file_service_properties = FileServiceProperties( + id=file_service_properties.id, + name=file_service_properties.name, + type=file_service_properties.type, + share_delete_retention_policy=DeleteRetentionPolicy( + enabled=getattr( + share_delete_retention_policy, + "enabled", + False, + ), + days=getattr( + share_delete_retention_policy, "days", 0, - ) - if soft_delete_enabled - else 0 - ) - - file_shares = client.file_shares.list( - account.resouce_group_name, account.name + ), + ), + smb_protocol_settings=SMBProtocolSettings( + channel_encryption=( + smb_channel_encryption_raw.rstrip(";").split(";") + if smb_channel_encryption_raw + else [] + ), + supported_versions=( + smb_supported_versions_raw.rstrip(";").split(";") + if smb_supported_versions_raw + else [] + ), + ), ) - account.file_shares = [] - for file_share in file_shares: - account.file_shares.append( - FileShare( - id=file_share.id, - name=file_share.name, - soft_delete_enabled=soft_delete_enabled, - retention_days=retention_days, - ) - ) except Exception as error: logger.error( f"Subscription name: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) -@dataclass -class DeleteRetentionPolicy: +class DeleteRetentionPolicy(BaseModel): enabled: bool days: int -@dataclass -class BlobProperties: +class BlobProperties(BaseModel): id: str name: str type: str - default_service_version: str container_delete_retention_policy: DeleteRetentionPolicy - versioning_enabled: bool = False + default_service_version: Optional[str] = None + versioning_enabled: Optional[bool] = None -@dataclass -class NetworkRuleSet: +class NetworkRuleSet(BaseModel): bypass: str default_action: str -@dataclass -class PrivateEndpointConnection: +class PrivateEndpointConnection(BaseModel): id: str name: str type: str @@ -230,31 +257,35 @@ class ReplicationSettings(Enum): STANDARD_RAGZRS = "Standard_RAGZRS" -@dataclass -class Account: +class SMBProtocolSettings(BaseModel): + channel_encryption: list[str] + supported_versions: list[str] + + +class FileServiceProperties(BaseModel): id: str name: str + type: str + share_delete_retention_policy: DeleteRetentionPolicy + smb_protocol_settings: SMBProtocolSettings + + +class Account(BaseModel): + id: str + name: str + location: str resouce_group_name: str enable_https_traffic_only: bool - infrastructure_encryption: bool + infrastructure_encryption: Optional[bool] = None allow_blob_public_access: bool network_rule_set: NetworkRuleSet encryption_type: str minimum_tls_version: str - private_endpoint_connections: List[PrivateEndpointConnection] - key_expiration_period_in_days: str - location: str + private_endpoint_connections: list[PrivateEndpointConnection] + key_expiration_period_in_days: Optional[int] = None replication_settings: ReplicationSettings = ReplicationSettings.STANDARD_LRS allow_cross_tenant_replication: bool = True allow_shared_key_access: bool = True blob_properties: Optional[BlobProperties] = None default_to_entra_authorization: bool = False - file_shares: list = None - - -@dataclass -class FileShare: - id: str - name: str - soft_delete_enabled: bool - retention_days: int + file_service_properties: Optional[FileServiceProperties] = None diff --git a/prowler/providers/azure/services/storage/storage_smb_channel_encryption_with_secure_algorithm/__init__.py b/prowler/providers/azure/services/storage/storage_smb_channel_encryption_with_secure_algorithm/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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 new file mode 100644 index 0000000000..02ea0ab141 --- /dev/null +++ b/prowler/providers/azure/services/storage/storage_smb_channel_encryption_with_secure_algorithm/storage_smb_channel_encryption_with_secure_algorithm.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "azure", + "CheckID": "storage_smb_channel_encryption_with_secure_algorithm", + "CheckTitle": "Ensure SMB channel encryption uses a secure algorithm for SMB file shares", + "CheckType": [], + "ServiceName": "storage", + "SubServiceName": "", + "ResourceIdTemplate": "/subscriptions/{subscription_id}/resourceGroups/{resource_group}/providers/Microsoft.Storage/storageAccounts/{storageAccountName}/fileServices/default", + "Severity": "medium", + "ResourceType": "AzureStorageAccount", + "Description": "Implement SMB channel encryption with a secure algorithm for SMB file shares to ensure data confidentiality and integrity in transit.", + "Risk": "Not using the recommended SMB channel encryption may expose data transmitted over SMB channels to unauthorized interception and tampering.", + "RelatedUrl": "https://learn.microsoft.com/en-us/azure/well-architected/service-guides/azure-files#recommendations-for-smb-file-shares", + "Remediation": { + "Code": { + "CLI": "az storage account file-service-properties update --resource-group --account-name --channel-encryption AES-256-GCM", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Use the portal, CLI or PowerShell to set the SMB channel encryption to a secure algorithm.", + "Url": "https://learn.microsoft.com/en-us/azure/storage/files/files-smb-protocol?tabs=azure-portal#smb-security-settings" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "This check passes if SMB channel encryption is set to a secure algorithm." +} 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 new file mode 100644 index 0000000000..c8e9f1da3c --- /dev/null +++ b/prowler/providers/azure/services/storage/storage_smb_channel_encryption_with_secure_algorithm/storage_smb_channel_encryption_with_secure_algorithm.py @@ -0,0 +1,51 @@ +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"] + + +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). + + 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. + """ + + def execute(self) -> list[Check_Report_Azure]: + findings = [] + for subscription, storage_accounts in storage_client.storage_accounts.items(): + for account in storage_accounts: + if account.file_service_properties: + 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" + ) + report = Check_Report_Azure( + metadata=self.metadata(), + resource=account.file_service_properties, + ) + report.subscription = subscription + report.resource_name = account.name + + if ( + not account.file_service_properties.smb_protocol_settings.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 = "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}." + 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}." + + findings.append(report) + return findings diff --git a/prowler/providers/azure/services/storage/storage_smb_protocol_version_is_latest/__init__.py b/prowler/providers/azure/services/storage/storage_smb_protocol_version_is_latest/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/azure/services/storage/storage_smb_protocol_version_is_latest/storage_smb_protocol_version_is_latest.metadata.json b/prowler/providers/azure/services/storage/storage_smb_protocol_version_is_latest/storage_smb_protocol_version_is_latest.metadata.json new file mode 100644 index 0000000000..0dea47ba46 --- /dev/null +++ b/prowler/providers/azure/services/storage/storage_smb_protocol_version_is_latest/storage_smb_protocol_version_is_latest.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "azure", + "CheckID": "storage_smb_protocol_version_is_latest", + "CheckTitle": "Ensure SMB protocol version for file shares is set to the latest version.", + "CheckType": [], + "ServiceName": "storage", + "SubServiceName": "", + "ResourceIdTemplate": "/subscriptions/{subscription_id}/resourceGroups/{resource_group}/providers/Microsoft.Storage/storageAccounts/{storageAccountName}/fileServices/default", + "Severity": "medium", + "ResourceType": "AzureStorageAccount", + "Description": "Ensure that SMB file shares are configured to use only the latest SMB protocol version.", + "Risk": "Allowing older SMB protocol versions may expose file shares to known vulnerabilities and security risks.", + "RelatedUrl": "https://learn.microsoft.com/en-us/azure/storage/files/files-smb-protocol#smb-security-settings", + "Remediation": { + "Code": { + "CLI": "az storage account file-service-properties update --resource-group --account-name --versions ", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Configure your Azure Storage Account file shares to allow only the latest SMB protocol version.", + "Url": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/StorageAccounts/latest-smb-protocol-version.html" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} 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 new file mode 100644 index 0000000000..19f2d37765 --- /dev/null +++ b/prowler/providers/azure/services/storage/storage_smb_protocol_version_is_latest/storage_smb_protocol_version_is_latest.py @@ -0,0 +1,48 @@ +from prowler.lib.check.models import Check, Check_Report_Azure +from prowler.providers.azure.services.storage.lib.constants import LATEST_SMB_VERSION +from prowler.providers.azure.services.storage.storage_client import storage_client + + +class storage_smb_protocol_version_is_latest(Check): + """ + Ensure SMB protocol version for file shares is set to the latest version. + + This check evaluates whether SMB file shares are configured to use only the latest SMB protocol version. + - PASS: Storage account allows only the latest SMB protocol version for file shares. + - FAIL: Storage account allows other SMB protocol versions for file shares. + """ + + def execute(self) -> list[Check_Report_Azure]: + findings = [] + + for subscription, storage_accounts in storage_client.storage_accounts.items(): + for account in storage_accounts: + if getattr(account, "file_service_properties", None) and getattr( + account.file_service_properties.smb_protocol_settings, + "supported_versions", + None, + ): + report = Check_Report_Azure( + metadata=self.metadata(), + resource=account.file_service_properties, + ) + report.subscription = subscription + report.resource_name = account.name + report.location = account.location + if ( + len( + account.file_service_properties.smb_protocol_settings.supported_versions + ) + == 1 + and account.file_service_properties.smb_protocol_settings.supported_versions[ + 0 + ] + == 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." + 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." + findings.append(report) + return findings diff --git a/prowler/providers/azure/services/vm/vm_backup_enabled/__init__.py b/prowler/providers/azure/services/vm/vm_backup_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/azure/services/vm/vm_backup_enabled/vm_backup_enabled.metadata.json b/prowler/providers/azure/services/vm/vm_backup_enabled/vm_backup_enabled.metadata.json new file mode 100644 index 0000000000..331609596f --- /dev/null +++ b/prowler/providers/azure/services/vm/vm_backup_enabled/vm_backup_enabled.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "azure", + "CheckID": "vm_backup_enabled", + "CheckTitle": "Ensure Backups are enabled for Azure Virtual Machines", + "CheckType": [], + "ServiceName": "vm", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "Microsoft.Compute/virtualMachines", + "Description": "Ensure that Microsoft Azure Backup service is in use for your Azure virtual machines (VMs) to protect against accidental deletion or corruption.", + "Risk": "Without Azure Backup enabled, VMs are at risk of data loss due to accidental deletion, corruption, or other failures, and recovery options are limited.", + "RelatedUrl": "https://docs.microsoft.com/en-us/azure/backup/backup-overview", + "Remediation": { + "Code": { + "CLI": "az backup protection enable-for-vm --resource-group --vm --vault-name --policy-name DefaultPolicy", + "NativeIaC": "", + "Other": "https://learn.microsoft.com/en-us/azure/backup/quick-backup-vm-portal", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable Azure Backup for each VM by associating it with a Recovery Services vault and a backup policy.", + "Url": "https://docs.microsoft.com/en-us/azure/backup/quick-backup-vm-portal" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} 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 new file mode 100644 index 0000000000..e9da4662ed --- /dev/null +++ b/prowler/providers/azure/services/vm/vm_backup_enabled/vm_backup_enabled.py @@ -0,0 +1,50 @@ +from azure.mgmt.recoveryservicesbackup.activestamp.models import DataSourceType + +from prowler.lib.check.models import Check, Check_Report_Azure +from prowler.providers.azure.services.recovery.recovery_client import recovery_client +from prowler.providers.azure.services.vm.vm_client import vm_client + + +class vm_backup_enabled(Check): + """ + Ensure that Microsoft Azure Backup service is in use for your Azure virtual machines (VMs). + + This check evaluates whether each Azure VM in the subscription is protected by Azure Backup. + + - PASS: The VM is protected by Azure Backup (present in a Recovery Services vault). + - FAIL: The VM is not protected by Azure Backup (not present in any Recovery Services vault). + """ + + def execute(self) -> list[Check_Report_Azure]: + """Execute Azure VM backup enabled check. + + Returns: + 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 vm in vms.values(): + found = False + found_vault_name = None + for vault in vaults.values(): + for backup_item in vault.backup_protected_items.values(): + if ( + backup_item.workload_type == DataSourceType.VM + and backup_item.name.split(";")[-1] == vm.resource_name + ): + found = True + found_vault_name = vault.name + break + if found: + break + report = Check_Report_Azure(metadata=self.metadata(), resource=vm) + report.subscription = subscription_name + 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})." + else: + report.status = "FAIL" + report.status_extended = f"VM {vm.resource_name} in subscription {subscription_name} is not protected by Azure Backup." + findings.append(report) + return findings diff --git a/prowler/providers/azure/services/vm/vm_ensure_using_approved_images/__init__.py b/prowler/providers/azure/services/vm/vm_ensure_using_approved_images/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/azure/services/vm/vm_ensure_using_approved_images/vm_ensure_using_approved_images.metadata.json b/prowler/providers/azure/services/vm/vm_ensure_using_approved_images/vm_ensure_using_approved_images.metadata.json new file mode 100644 index 0000000000..c1b8605612 --- /dev/null +++ b/prowler/providers/azure/services/vm/vm_ensure_using_approved_images/vm_ensure_using_approved_images.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "azure", + "CheckID": "vm_ensure_using_approved_images", + "CheckTitle": "Ensure that Azure VMs are using an approved machine image.", + "CheckType": [], + "ServiceName": "vm", + "SubServiceName": "image", + "ResourceIdTemplate": "/subscriptions//resourceGroups//providers/Microsoft.Compute/images/", + "Severity": "medium", + "ResourceType": "Microsoft.Compute/images", + "Description": "Ensure that all your Azure virtual machine instances are launched from approved machine images only.", + "Risk": "An approved machine image is a custom virtual machine (VM) image that contains a pre-configured OS and a well-defined stack of server software approved by Azure, fully configured to run your application. Using approved (golden) machine images to launch new VM instances within your Azure cloud environment brings major benefits such as fast and stable application deployment and scaling, secure application stack upgrades, and versioning.", + "RelatedUrl": "https://learn.microsoft.com/en-us/azure/virtual-machines/windows/create-vm-generalized-managed", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/VirtualMachines/approved-machine-image.html", + "Terraform": "" + }, + "Recommendation": { + "Text": "Re-create the required VM instances using the approved (golden) machine image.", + "Url": "https://docs.microsoft.com/en-us/azure/virtual-machines/windows/create-vm-generalized-managed" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "This check only validates if the VM was launched from a custom image. It does not validate the image content or security baseline." +} 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 new file mode 100644 index 0000000000..4f6c378777 --- /dev/null +++ b/prowler/providers/azure/services/vm/vm_ensure_using_approved_images/vm_ensure_using_approved_images.py @@ -0,0 +1,33 @@ +from prowler.lib.check.models import Check, Check_Report_Azure +from prowler.providers.azure.services.vm.vm_client import vm_client + + +class vm_ensure_using_approved_images(Check): + """ + Ensure that Azure VMs are using an approved (custom) machine image. + + This check evaluates whether Azure Virtual Machines are launched from an approved (custom) machine image by checking the image reference ID format. + + - PASS: The Azure VM is using an approved custom machine image. + - FAIL: The Azure VM is not using an approved custom machine image. + """ + + def execute(self): + findings = [] + for subscription_name, vms in vm_client.virtual_machines.items(): + for vm in vms.values(): + report = Check_Report_Azure(metadata=self.metadata(), resource=vm) + report.subscription = subscription_name + image_id = getattr(vm, "image_reference", None) + if ( + image_id + and image_id.startswith("/subscriptions/") + 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]}." + else: + report.status = "FAIL" + report.status_extended = f"VM {vm.resource_name} in subscription {subscription_name} 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 22735fa9b7..e771fc80e5 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 @@ -15,17 +15,17 @@ class vm_ensure_using_managed_disks(Check): using_managed_disks = ( True - if vm.storage_profile - and getattr(vm.storage_profile, "os_disk", False) - and getattr(vm.storage_profile.os_disk, "managed_disk", False) + if getattr( + getattr(getattr(vm, "storage_profile", None), "os_disk", None), + "managed_disk", + None, + ) else False ) - if using_managed_disks and getattr( - vm.storage_profile, "data_disks", False - ): + if using_managed_disks and getattr(vm, "storage_profile", None): for data_disk in vm.storage_profile.data_disks: - if not getattr(data_disk, "managed_disk", False): + if not getattr(data_disk, "managed_disk", None): using_managed_disks = False break diff --git a/prowler/providers/azure/services/vm/vm_linux_enforce_ssh_authentication/__init__.py b/prowler/providers/azure/services/vm/vm_linux_enforce_ssh_authentication/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/azure/services/vm/vm_linux_enforce_ssh_authentication/vm_linux_enforce_ssh_authentication.metadata.json b/prowler/providers/azure/services/vm/vm_linux_enforce_ssh_authentication/vm_linux_enforce_ssh_authentication.metadata.json new file mode 100644 index 0000000000..3067ca5736 --- /dev/null +++ b/prowler/providers/azure/services/vm/vm_linux_enforce_ssh_authentication/vm_linux_enforce_ssh_authentication.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "azure", + "CheckID": "vm_linux_enforce_ssh_authentication", + "CheckTitle": "Ensure SSH key authentication is enforced on Linux-based Virtual Machines", + "CheckType": [], + "ServiceName": "vm", + "SubServiceName": "", + "ResourceIdTemplate": "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/virtualMachines/{vmName}", + "Severity": "high", + "ResourceType": "Microsoft.Compute/virtualMachines", + "Description": "Ensure that Azure Linux-based virtual machines are configured to use SSH keys by disabling password authentication.", + "Risk": "Allowing password-based SSH authentication increases the risk of brute-force attacks and unauthorized access. Enforcing SSH key authentication ensures only users with the private key can access the VM.", + "RelatedUrl": "https://docs.microsoft.com/en-us/azure/virtual-machines/linux/create-ssh-keys-detailed", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/VirtualMachines/ssh-authentication-type.html", + "Terraform": "" + }, + "Recommendation": { + "Text": "Recreate Linux VMs with SSH key authentication enabled and password authentication disabled.", + "Url": "https://docs.microsoft.com/en-us/azure/virtual-machines/linux/create-ssh-keys-detailed" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} 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 new file mode 100644 index 0000000000..c31b2c9a18 --- /dev/null +++ b/prowler/providers/azure/services/vm/vm_linux_enforce_ssh_authentication/vm_linux_enforce_ssh_authentication.py @@ -0,0 +1,29 @@ +from prowler.lib.check.models import Check, Check_Report_Azure +from prowler.providers.azure.services.vm.vm_client import vm_client + + +class vm_linux_enforce_ssh_authentication(Check): + """ + Ensure that Azure Linux-based virtual machines are configured to use SSH keys (password authentication is disabled). + + This check evaluates whether disablePasswordAuthentication is set to True for Linux VMs to ensure only SSH key authentication is allowed. + - PASS: VM has password authentication disabled (SSH key authentication enforced). + - FAIL: VM has password authentication enabled (password-based SSH allowed). + """ + + def execute(self) -> list[Check_Report_Azure]: + findings = [] + for subscription_name, vms in vm_client.virtual_machines.items(): + for vm in vms.values(): + if vm.linux_configuration: + report = Check_Report_Azure(metadata=self.metadata(), resource=vm) + report.subscription = subscription_name + + 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)." + else: + report.status = "FAIL" + report.status_extended = f"VM {vm.resource_name} in subscription {subscription_name} 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/__init__.py b/prowler/providers/azure/services/vm/vm_scaleset_associated_with_load_balancer/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/azure/services/vm/vm_scaleset_associated_with_load_balancer/vm_scaleset_associated_with_load_balancer.metadata.json b/prowler/providers/azure/services/vm/vm_scaleset_associated_with_load_balancer/vm_scaleset_associated_with_load_balancer.metadata.json new file mode 100644 index 0000000000..d73e44fd10 --- /dev/null +++ b/prowler/providers/azure/services/vm/vm_scaleset_associated_with_load_balancer/vm_scaleset_associated_with_load_balancer.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "azure", + "CheckID": "vm_scaleset_associated_with_load_balancer", + "CheckTitle": "VM Scale Set Is Associated With Load Balancer", + "CheckType": [], + "ServiceName": "vm", + "SubServiceName": "scaleset", + "ResourceIdTemplate": "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/virtualMachineScaleSets/{vmScaleSetName}", + "Severity": "medium", + "ResourceType": "Microsoft.Compute/virtualMachineScaleSets", + "Description": "Ensure that your Azure virtual machine scale sets are using load balancers for traffic distribution.", + "Risk": "Without load balancer integration, Azure virtual machine scale sets may experience reduced availability and potential service disruptions during traffic spikes or instance failures, leading to degraded user experience and potential business impact.", + "RelatedUrl": "https://learn.microsoft.com/en-us/azure/virtual-network/network-overview", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/VirtualMachines/associated-load-balancers.html", + "Terraform": "" + }, + "Recommendation": { + "Text": "Attach a load balancer to your Azure virtual machine scale set to ensure high availability and optimal traffic distribution.", + "Url": "https://docs.microsoft.com/en-us/azure/load-balancer/load-balancer-overview" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} 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 new file mode 100644 index 0000000000..c6150f1d7f --- /dev/null +++ b/prowler/providers/azure/services/vm/vm_scaleset_associated_with_load_balancer/vm_scaleset_associated_with_load_balancer.py @@ -0,0 +1,36 @@ +from prowler.lib.check.models import Check, Check_Report_Azure +from prowler.providers.azure.services.vm.vm_client import vm_client + + +class vm_scaleset_associated_with_load_balancer(Check): + """ + Ensure that Azure virtual machine scale sets are associated with a load balancer backend pool. + + This check evaluates whether each VM scale set is associated with at least one load balancer backend pool. + - PASS: The scale set is associated with a load balancer backend pool. + - FAIL: The scale set is not associated with any load balancer backend pool. + """ + + def execute(self): + findings = [] + for subscription, scale_sets in vm_client.vm_scale_sets.items(): + for scale_set in scale_sets.values(): + report = Check_Report_Azure( + metadata=self.metadata(), resource=scale_set + ) + report.subscription = subscription + report.resource_id = scale_set.resource_id + report.resource_name = scale_set.resource_name + report.location = scale_set.location + if scale_set.load_balancer_backend_pools: + report.status = "PASS" + backend_pool_names = [ + 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)}." + 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." + 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 d5d0dde5c7..ea7d252acc 100644 --- a/prowler/providers/azure/services/vm/vm_service.py +++ b/prowler/providers/azure/services/vm/vm_service.py @@ -1,7 +1,9 @@ from dataclasses import dataclass +from enum import Enum from typing import List, Optional from azure.mgmt.compute import ComputeManagementClient +from pydantic import BaseModel from prowler.lib.logger import logger from prowler.providers.azure.azure_provider import AzureProvider @@ -13,6 +15,7 @@ class VirtualMachines(AzureService): super().__init__(ComputeManagementClient, provider) self.virtual_machines = self._get_virtual_machines() self.disks = self._get_disks() + self.vm_scale_sets = self._get_vm_scale_sets() def _get_virtual_machines(self): logger.info("VirtualMachines - Getting virtual machines...") @@ -30,27 +33,48 @@ class VirtualMachines(AzureService): if storage_profile else None ) - data_disks = [] + if storage_profile and getattr(storage_profile, "data_disks", []): data_disks = [ DataDisk( lun=data_disk.lun, name=data_disk.name, - managed_disk=data_disk.managed_disk, + managed_disk=ManagedDiskParameters( + id=( + getattr( + getattr(data_disk, "managed_disk", None), + "id", + None, + ) + if data_disk.managed_disk + else None + ) + ), ) for data_disk in getattr(storage_profile, "data_disks", []) - if data_disk ] extensions = [] if getattr(vm, "resources", []): extensions = [ VirtualMachineExtension(id=extension.id) - for extension in getattr(vm, "resources", []) + for extension in vm.resources if extension ] + # Collect LinuxConfiguration.disablePasswordAuthentication if available + linux_configuration = None + os_profile = getattr(vm, "os_profile", None) + if os_profile: + linux_conf = getattr(os_profile, "linux_configuration", None) + if linux_conf: + linux_configuration = LinuxConfiguration( + disable_password_authentication=getattr( + linux_conf, "disable_password_authentication", False + ) + ) + virtual_machines[subscription_name].update( { vm.id: VirtualMachine( @@ -60,8 +84,17 @@ class VirtualMachines(AzureService): StorageProfile( os_disk=OSDisk( name=getattr(os_disk, "name", None), - managed_disk=getattr( - os_disk, "managed_disk", None + operating_system_type=getattr( + os_disk, "os_type", None + ), + managed_disk=ManagedDiskParameters( + id=getattr( + getattr( + os_disk, "managed_disk", None + ), + "id", + None, + ) ), ), data_disks=data_disks, @@ -72,6 +105,12 @@ class VirtualMachines(AzureService): location=vm.location, security_profile=getattr(vm, "security_profile", None), extensions=extensions, + image_reference=getattr( + getattr(storage_profile, "image_reference", None), + "id", + None, + ), + linux_configuration=linux_configuration, ) } ) @@ -117,6 +156,69 @@ class VirtualMachines(AzureService): return disks + def _get_vm_scale_sets(self) -> dict[str, dict]: + """ + Get all needed information about VM scale sets. + + Returns: + A nested dictionary with the following structure: + { + "subscription_name": { + "vm_scale_set_id": VirtualMachineScaleSet() + } + } + """ + logger.info( + "VirtualMachines - Getting VM scale sets and their load balancer associations..." + ) + vm_scale_sets = {} + for subscription_name, client in self.clients.items(): + try: + scale_sets = client.virtual_machine_scale_sets.list_all() + vm_scale_sets[subscription_name] = {} + for scale_set in scale_sets: + backend_pools = [] + nic_configs = [] + virtual_machine_profile = getattr( + scale_set, "virtual_machine_profile", None + ) + if virtual_machine_profile: + network_profile = getattr( + virtual_machine_profile, "network_profile", None + ) + if network_profile: + nic_configs = ( + getattr( + network_profile, + "network_interface_configurations", + [], + ) + or [] + ) + for nic in nic_configs: + ip_confs = getattr(nic, "ip_configurations", []) + for ipconf in ip_confs: + pools = getattr( + ipconf, "load_balancer_backend_address_pools", [] + ) + if pools: + for pool in pools: + if getattr(pool, "id", None): + backend_pools.append(pool.id) + vm_scale_sets[subscription_name][scale_set.id] = ( + VirtualMachineScaleSet( + resource_id=scale_set.id, + resource_name=scale_set.name, + location=scale_set.location, + load_balancer_backend_pools=backend_pools, + ) + ) + except Exception as error: + logger.error( + f"Subscription name: {subscription_name} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return vm_scale_sets + @dataclass class UefiSettings: @@ -130,44 +232,61 @@ class SecurityProfile: uefi_settings: Optional[UefiSettings] -@dataclass -class OSDisk: - name: Optional[str] - managed_disk: Optional[bool] +class OperatingSystemType(Enum): + WINDOWS = "Windows" + LINUX = "Linux" -@dataclass -class DataDisk: +class ManagedDiskParameters(BaseModel): + id: str + + +class OSDisk(BaseModel): + name: str + operating_system_type: OperatingSystemType + managed_disk: Optional[ManagedDiskParameters] + + +class DataDisk(BaseModel): lun: int name: str - managed_disk: bool + managed_disk: Optional[ManagedDiskParameters] -@dataclass -class StorageProfile: +class StorageProfile(BaseModel): os_disk: Optional[OSDisk] data_disks: List[DataDisk] -@dataclass -class VirtualMachineExtension: +class VirtualMachineExtension(BaseModel): id: str -@dataclass -class VirtualMachine: +class LinuxConfiguration(BaseModel): + disable_password_authentication: bool + + +class VirtualMachine(BaseModel): resource_id: str resource_name: str location: str security_profile: Optional[SecurityProfile] extensions: list[VirtualMachineExtension] storage_profile: Optional[StorageProfile] = None + image_reference: Optional[str] = None + linux_configuration: Optional[LinuxConfiguration] = None -@dataclass -class Disk: +class Disk(BaseModel): resource_id: str resource_name: str vms_attached: list[str] encryption_type: str location: str + + +class VirtualMachineScaleSet(BaseModel): + resource_id: str + resource_name: str + location: str + load_balancer_backend_pools: list[str] diff --git a/prowler/providers/common/provider.py b/prowler/providers/common/provider.py index e7f4a74c2f..841bd0f1ed 100644 --- a/prowler/providers/common/provider.py +++ b/prowler/providers/common/provider.py @@ -246,6 +246,8 @@ class Provider(ABC): elif "iac" in provider_class_name.lower(): provider_class( scan_path=arguments.scan_path, + frameworks=arguments.frameworks, + exclude_path=arguments.exclude_path, config_path=arguments.config_file, fixer_config=fixer_config, ) diff --git a/prowler/providers/gcp/services/compute/compute_instance_block_project_wide_ssh_keys_disabled/compute_instance_block_project_wide_ssh_keys_disabled.py b/prowler/providers/gcp/services/compute/compute_instance_block_project_wide_ssh_keys_disabled/compute_instance_block_project_wide_ssh_keys_disabled.py index 4ee3c9f029..d49aed45bd 100644 --- a/prowler/providers/gcp/services/compute/compute_instance_block_project_wide_ssh_keys_disabled/compute_instance_block_project_wide_ssh_keys_disabled.py +++ b/prowler/providers/gcp/services/compute/compute_instance_block_project_wide_ssh_keys_disabled/compute_instance_block_project_wide_ssh_keys_disabled.py @@ -13,7 +13,7 @@ class compute_instance_block_project_wide_ssh_keys_disabled(Check): for item in instance.metadata["items"]: if ( item["key"] == "block-project-ssh-keys" - and item["value"] == "true" + and item["value"].lower() == "true" ): report.status = "PASS" report.status_extended = f"The VM Instance {instance.name} is not making use of common/shared project-wide SSH key(s)." diff --git a/prowler/providers/github/lib/arguments/arguments.py b/prowler/providers/github/lib/arguments/arguments.py index 6e7598c8d7..530be5b012 100644 --- a/prowler/providers/github/lib/arguments/arguments.py +++ b/prowler/providers/github/lib/arguments/arguments.py @@ -10,6 +10,7 @@ def init_parser(self): nargs="?", help="Personal Access Token to log in against GitHub", default=None, + metavar="GITHUB_PERSONAL_ACCESS_TOKEN", ) github_auth_subparser.add_argument( @@ -17,6 +18,7 @@ def init_parser(self): nargs="?", help="OAuth App Token to log in against GitHub", default=None, + metavar="GITHUB_OAUTH_APP_TOKEN", ) # GitHub App Authentication @@ -25,10 +27,12 @@ def init_parser(self): nargs="?", help="GitHub App ID to log in against GitHub", default=None, + metavar="GITHUB_APP_ID", ) github_auth_subparser.add_argument( "--github-app-key", nargs="?", help="GitHub App Key Path to log in against GitHub", default=None, + metavar="GITHUB_APP_KEY", ) diff --git a/prowler/providers/iac/iac_provider.py b/prowler/providers/iac/iac_provider.py index 540bb827d0..1abd00f4c5 100644 --- a/prowler/providers/iac/iac_provider.py +++ b/prowler/providers/iac/iac_provider.py @@ -1,8 +1,39 @@ import json -import subprocess import sys from typing import List +from checkov.ansible.runner import Runner as AnsibleRunner +from checkov.argo_workflows.runner import Runner as ArgoWorkflowsRunner +from checkov.arm.runner import Runner as ArmRunner +from checkov.azure_pipelines.runner import Runner as AzurePipelinesRunner +from checkov.bicep.runner import Runner as BicepRunner +from checkov.bitbucket.runner import Runner as BitbucketRunner +from checkov.bitbucket_pipelines.runner import Runner as BitbucketPipelinesRunner +from checkov.cdk.runner import CdkRunner +from checkov.circleci_pipelines.runner import Runner as CircleciPipelinesRunner +from checkov.cloudformation.runner import Runner as CfnRunner +from checkov.common.output.record import Record +from checkov.common.output.report import Report +from checkov.common.runners.runner_registry import RunnerRegistry +from checkov.dockerfile.runner import Runner as DockerfileRunner +from checkov.github.runner import Runner as GithubRunner +from checkov.github_actions.runner import Runner as GithubActionsRunner +from checkov.gitlab.runner import Runner as GitlabRunner +from checkov.gitlab_ci.runner import Runner as GitlabCiRunner +from checkov.helm.runner import Runner as HelmRunner +from checkov.json_doc.runner import Runner as JsonDocRunner +from checkov.kubernetes.runner import Runner as K8sRunner +from checkov.kustomize.runner import Runner as KustomizeRunner +from checkov.openapi.runner import Runner as OpenapiRunner +from checkov.runner_filter import RunnerFilter +from checkov.sast.runner import Runner as SastRunner +from checkov.sca_image.runner import Runner as ScaImageRunner +from checkov.sca_package_2.runner import Runner as ScaPackage2Runner +from checkov.secrets.runner import Runner as SecretsRunner +from checkov.serverless.runner import Runner as ServerlessRunner +from checkov.terraform.runner import Runner as TerraformRunner +from checkov.terraform_json.runner import TerraformJsonRunner +from checkov.yaml_doc.runner import Runner as YamlDocRunner from colorama import Fore, Style from prowler.config.config import ( @@ -23,6 +54,8 @@ class IacProvider(Provider): def __init__( self, scan_path: str = ".", + frameworks: list[str] = ["all"], + exclude_path: list[str] = [], config_path: str = None, config_content: dict = None, fixer_config: dict = {}, @@ -30,6 +63,8 @@ class IacProvider(Provider): logger.info("Instantiating IAC Provider...") self.scan_path = scan_path + self.frameworks = frameworks + self.exclude_path = exclude_path self.region = "global" self.audited_account = "local-iac" self._session = None @@ -86,7 +121,9 @@ class IacProvider(Provider): """IAC provider doesn't need a session since it uses Checkov directly""" return None - def _process_check(self, finding: dict, check: dict, status: str) -> CheckReportIAC: + def _process_check( + self, finding: Report, check: Record, status: str + ) -> CheckReportIAC: """ Process a single check (failed or passed) and create a CheckReportIAC object. @@ -98,109 +135,110 @@ class IacProvider(Provider): Returns: CheckReportIAC: The processed check report """ - metadata_dict = { - "Provider": "iac", - "CheckID": check.get("check_id", ""), - "CheckTitle": check.get("check_name", ""), - "CheckType": ["Infrastructure as Code"], - "ServiceName": finding["check_type"], - "SubServiceName": "", - "ResourceIdTemplate": "", - "Severity": ( - check.get("severity", "low").lower() if check.get("severity") else "low" - ), - "ResourceType": "iac", - "Description": check.get("check_name", ""), - "Risk": "", - "RelatedUrl": ( - check.get("guideline", "") if check.get("guideline") else "" - ), - "Remediation": { - "Code": { - "NativeIaC": "", - "Terraform": "", - "CLI": "", - "Other": "", + try: + metadata_dict = { + "Provider": "iac", + "CheckID": check.check_id, + "CheckTitle": check.check_name, + "CheckType": ["Infrastructure as Code"], + "ServiceName": finding.check_type, + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": (check.severity.lower() if check.severity else "low"), + "ResourceType": finding.check_type, + "Description": check.check_name, + "Risk": "", + "RelatedUrl": (check.guideline if check.guideline else ""), + "Remediation": { + "Code": { + "NativeIaC": "", + "Terraform": "", + "CLI": "", + "Other": "", + }, + "Recommendation": { + "Text": "", + "Url": (check.guideline if check.guideline else ""), + }, }, - "Recommendation": { - "Text": "", - "Url": ( - check.get("guideline", "") if check.get("guideline") else "" - ), - }, - }, - "Categories": [], - "DependsOn": [], - "RelatedTo": [], - "Notes": "", - } + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + } - # Convert metadata dict to JSON string - metadata = json.dumps(metadata_dict) + # Convert metadata dict to JSON string + metadata = json.dumps(metadata_dict) - report = CheckReportIAC(metadata=metadata, finding=check) - report.status = status - report.resource_tags = check.get("entity_tags", {}) - report.status_extended = check.get("check_name", "") - if status == "MUTED": - report.muted = True - return report + report = CheckReportIAC(metadata=metadata, resource=check) + report.status = status + report.resource_tags = check.entity_tags + report.status_extended = check.check_name + if status == "MUTED": + report.muted = True + return report + except Exception as error: + logger.critical( + f"{error.__class__.__name__}:{error.__traceback__.tb_lineno} -- {error}" + ) + sys.exit(1) def run(self) -> List[CheckReportIAC]: - return self.run_scan(self.scan_path) + return self.run_scan(self.scan_path, self.frameworks, self.exclude_path) - def run_scan(self, directory: str) -> List[CheckReportIAC]: + def run_scan( + self, directory: str, frameworks: list[str], exclude_path: list[str] + ) -> List[CheckReportIAC]: try: logger.info(f"Running IaC scan on {directory}...") - - # Run Checkov with JSON output - process = subprocess.run( - ["checkov", "-d", directory, "-o", "json"], - capture_output=True, - text=True, + runners = [ + TerraformRunner(), + CfnRunner(), + K8sRunner(), + ArmRunner(), + ServerlessRunner(), + DockerfileRunner(), + YamlDocRunner(), + OpenapiRunner(), + SastRunner(), + ScaImageRunner(), + ScaPackage2Runner(), + SecretsRunner(), + AnsibleRunner(), + ArgoWorkflowsRunner(), + BitbucketRunner(), + BitbucketPipelinesRunner(), + CdkRunner(), + CircleciPipelinesRunner(), + GithubRunner(), + GithubActionsRunner(), + GitlabRunner(), + GitlabCiRunner(), + HelmRunner(), + JsonDocRunner(), + TerraformJsonRunner(), + KustomizeRunner(), + AzurePipelinesRunner(), + BicepRunner(), + ] + runner_filter = RunnerFilter( + framework=frameworks, excluded_paths=exclude_path ) - # Log Checkov's error output if any - if process.stderr: - logger.error(process.stderr) - try: - output = json.loads(process.stdout) - if not output: - logger.warning("No findings returned from Checkov scan") - return [] - except Exception as error: - logger.critical( - f"{error.__class__.__name__}:{error.__traceback__.tb_lineno} -- {error}" - ) - sys.exit(1) + registry = RunnerRegistry("", runner_filter, *runners) + checkov_reports = registry.run(root_folder=directory) - reports = [] + reports: List[CheckReportIAC] = [] + for report in checkov_reports: - # If only one framework has findings, the output is a dict, otherwise it's a list of dicts - if isinstance(output, dict): - output = [output] + for failed in report.failed_checks: + reports.append(self._process_check(report, failed, "FAIL")) - # Process all frameworks findings - for finding in output: - results = finding.get("results", {}) + for passed in report.passed_checks: + reports.append(self._process_check(report, passed, "PASS")) - # Process failed checks - failed_checks = results.get("failed_checks", []) - for failed_check in failed_checks: - report = self._process_check(finding, failed_check, "FAIL") - reports.append(report) - - # Process passed checks - passed_checks = results.get("passed_checks", []) - for passed_check in passed_checks: - report = self._process_check(finding, passed_check, "PASS") - reports.append(report) - - # Process skipped checks (muted) - skipped_checks = results.get("skipped_checks", []) - for skipped_check in skipped_checks: - report = self._process_check(finding, skipped_check, "MUTED") - reports.append(report) + for skipped in report.skipped_checks: + reports.append(self._process_check(report, skipped, "MUTED")) return reports @@ -214,5 +252,12 @@ class IacProvider(Provider): report_lines = [ f"Directory: {Fore.YELLOW}{self.scan_path}{Style.RESET_ALL}", ] + if self.exclude_path: + report_lines.append( + f"Excluded paths: {Fore.YELLOW}{', '.join(self.exclude_path)}{Style.RESET_ALL}" + ) + report_lines.append( + f"Frameworks: {Fore.YELLOW}{', '.join(self.frameworks)}{Style.RESET_ALL}" + ) report_title = f"{Style.BRIGHT}Scanning local IaC directory:{Style.RESET_ALL}" print_boxes(report_lines, report_title) diff --git a/prowler/providers/iac/lib/arguments/arguments.py b/prowler/providers/iac/lib/arguments/arguments.py index 87401aecf1..bc478bc4d1 100644 --- a/prowler/providers/iac/lib/arguments/arguments.py +++ b/prowler/providers/iac/lib/arguments/arguments.py @@ -1,3 +1,36 @@ +FRAMEWORK_CHOICES = [ + "ansible", + "argo_workflows", + "arm", + "azure_pipelines", + "bicep", + "bitbucket", + "bitbucket_pipelines", + "cdk", + "circleci_pipelines", + "cloudformation", + "dockerfile", + "github", + "github_actions", + "gitlab", + "gitlab_ci", + "helm", + "json_doc", + "kubernetes", + "kustomize", + "openapi", + "policies_3d", + "sast", + "sca_image", + "sca_package_2", + "secrets", + "serverless", + "terraform", + "terraform_json", + "yaml_doc", +] + + def init_parser(self): """Init the IAC Provider CLI parser""" iac_parser = self.subparsers.add_parser( @@ -13,3 +46,20 @@ def init_parser(self): default=".", help="Path to the folder containing your infrastructure-as-code files. Default: current directory", ) + iac_scan_subparser.add_argument( + "--frameworks", + "-f", + "--framework", + dest="frameworks", + nargs="+", + default=["all"], + choices=FRAMEWORK_CHOICES, + help="Comma-separated list of frameworks to scan. Default: all", + ) + iac_scan_subparser.add_argument( + "--exclude-path", + dest="exclude_path", + nargs="+", + default=[], + help="Comma-separated list of paths to exclude from the scan. Default: none", + ) diff --git a/prowler/providers/m365/exceptions/exceptions.py b/prowler/providers/m365/exceptions/exceptions.py index fe2b8b442c..7f9dae2c31 100644 --- a/prowler/providers/m365/exceptions/exceptions.py +++ b/prowler/providers/m365/exceptions/exceptions.py @@ -114,6 +114,18 @@ class M365BaseException(ProwlerException): "message": "The provided User does not belong to the specified tenant.", "remediation": "Check the User email domain and ensure it belongs to the specified tenant.", }, + (6027, "M365GraphConnectionError"): { + "message": "Failed to establish connection to Microsoft Graph API.", + "remediation": "Check your Microsoft Application credentials and ensure the app has proper permissions.", + }, + (6028, "M365TeamsConnectionError"): { + "message": "Failed to establish connection to Microsoft Teams API.", + "remediation": "Ensure the application has proper permission granted to access Microsoft Teams.", + }, + (6029, "M365ExchangeConnectionError"): { + "message": "Failed to establish connection to Exchange Online API.", + "remediation": "Ensure the application has proper permission granted to access Exchange Online.", + }, } def __init__(self, code, file=None, original_exception=None, message=None): @@ -324,3 +336,24 @@ class M365UserNotBelongingToTenantError(M365CredentialsError): super().__init__( 6026, file=file, original_exception=original_exception, message=message ) + + +class M365GraphConnectionError(M365CredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 6027, file=file, original_exception=original_exception, message=message + ) + + +class M365TeamsConnectionError(M365CredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 6028, file=file, original_exception=original_exception, message=message + ) + + +class M365ExchangeConnectionError(M365CredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 6029, file=file, original_exception=original_exception, message=message + ) diff --git a/prowler/providers/m365/lib/powershell/m365_powershell.py b/prowler/providers/m365/lib/powershell/m365_powershell.py index 937069b690..d532eba58c 100644 --- a/prowler/providers/m365/lib/powershell/m365_powershell.py +++ b/prowler/providers/m365/lib/powershell/m365_powershell.py @@ -6,6 +6,9 @@ import msal from prowler.lib.logger import logger from prowler.lib.powershell.powershell import PowerShellSession from prowler.providers.m365.exceptions.exceptions import ( + M365ExchangeConnectionError, + M365GraphConnectionError, + M365TeamsConnectionError, M365UserNotBelongingToTenantError, ) from prowler.providers.m365.models import M365Credentials, M365IdentityInfo @@ -65,21 +68,33 @@ class M365PowerShell(PowerShellSession): The credentials are sanitized to prevent command injection and stored securely in the PowerShell session. """ + # User Auth (Will be deprecated in September 2025) + if credentials.user and credentials.passwd: + credentials.encrypted_passwd = self.encrypt_password(credentials.passwd) - credentials.encrypted_passwd = self.encrypt_password(credentials.passwd) + # Sanitize user and password + sanitized_user = self.sanitize(credentials.user) + sanitized_encrypted_passwd = self.sanitize(credentials.encrypted_passwd) - # Sanitize user and password - sanitized_user = self.sanitize(credentials.user) - sanitized_encrypted_passwd = self.sanitize(credentials.encrypted_passwd) - - # Securely convert encrypted password to SecureString - self.execute(f'$user = "{sanitized_user}"') - self.execute( - f'$secureString = "{sanitized_encrypted_passwd}" | ConvertTo-SecureString' - ) - self.execute( - "$credential = New-Object System.Management.Automation.PSCredential ($user, $secureString)" - ) + # Securely convert encrypted password to SecureString + self.execute(f'$user = "{sanitized_user}"') + self.execute( + f'$secureString = "{sanitized_encrypted_passwd}" | ConvertTo-SecureString' + ) + self.execute( + "$credential = New-Object System.Management.Automation.PSCredential ($user, $secureString)" + ) + else: + # Application Auth + self.execute(f'$clientID = "{credentials.client_id}"') + self.execute(f'$clientSecret = "{credentials.client_secret}"') + self.execute(f'$tenantID = "{credentials.tenant_id}"') + self.execute( + '$graphtokenBody = @{ Grant_Type = "client_credentials"; Scope = "https://graph.microsoft.com/.default"; Client_Id = $clientID; Client_Secret = $clientSecret }' + ) + self.execute( + '$graphToken = Invoke-RestMethod -Uri "https://login.microsoftonline.com/$tenantID/oauth2/v2.0/token" -Method POST -Body $graphtokenBody | Select-Object -ExpandProperty Access_Token' + ) def encrypt_password(self, password: str) -> str: """ @@ -129,46 +144,151 @@ class M365PowerShell(PowerShellSession): Returns: bool: True if credentials are valid and authentication succeeds, False otherwise. """ - self.execute( - f'$securePassword = "{credentials.encrypted_passwd}" | ConvertTo-SecureString' # encrypted password already sanitized - ) - self.execute( - f'$credential = New-Object System.Management.Automation.PSCredential("{self.sanitize(credentials.user)}", $securePassword)' - ) + if credentials.user and credentials.passwd: + self.execute( + f'$securePassword = "{credentials.encrypted_passwd}" | ConvertTo-SecureString' # encrypted password already sanitized + ) + self.execute( + f'$credential = New-Object System.Management.Automation.PSCredential("{self.sanitize(credentials.user)}", $securePassword)' + ) - # Validate user belongs to tenant - user_domain = credentials.user.split("@")[1] - if not any( - user_domain.endswith(domain) - for domain in self.tenant_identity.tenant_domains - ): - raise M365UserNotBelongingToTenantError( + user_domain = credentials.user.split("@")[1] + if not any( + user_domain.endswith(domain) + for domain in self.tenant_identity.tenant_domains + ): + raise M365UserNotBelongingToTenantError( + file=os.path.basename(__file__), + message=f"The user domain {user_domain} does not match any of the tenant domains: {', '.join(self.tenant_identity.tenant_domains)}", + ) + + app = msal.ConfidentialClientApplication( + client_id=credentials.client_id, + client_credential=credentials.client_secret, + authority=f"https://login.microsoftonline.com/{credentials.tenant_id}", + ) + + # Validate credentials + result = app.acquire_token_by_username_password( + username=credentials.user, + password=credentials.passwd, + scopes=["https://graph.microsoft.com/.default"], + ) + + if result is None: + raise Exception( + "Unexpected error: Acquiring token in behalf of user did not return a result." + ) + + if "access_token" not in result: + raise Exception(f"MsGraph Error {result.get('error_description')}") + + return True + + else: + # Test Microsoft Graph connection + try: + logger.info("Testing Microsoft Graph connection...") + self.test_graph_connection() + logger.info("Microsoft Graph connection successful") + except Exception as e: + logger.error(f"Microsoft Graph connection failed: {e}") + raise M365GraphConnectionError( + file=os.path.basename(__file__), + original_exception=e, + message="Check your Microsoft Application credentials and ensure the app has proper permissions", + ) + + # Test Microsoft Teams connection + try: + logger.info("Testing Microsoft Teams connection...") + self.test_teams_connection() + logger.info("Microsoft Teams connection successful") + except Exception as e: + logger.error(f"Microsoft Teams connection failed: {e}") + raise M365TeamsConnectionError( + file=os.path.basename(__file__), + original_exception=e, + message="Ensure the application has proper permission granted to access Microsoft Teams.", + ) + + # Test Exchange Online connection + try: + logger.info("Testing Exchange Online connection...") + self.test_exchange_connection() + logger.info("Exchange Online connection successful") + except Exception as e: + logger.error(f"Exchange Online connection failed: {e}") + raise M365ExchangeConnectionError( + file=os.path.basename(__file__), + original_exception=e, + message="Ensure the application has proper permission granted to access Exchange Online.", + ) + + return True + + def test_graph_connection(self) -> bool: + """Test Microsoft Graph API connection and raise exception if it fails.""" + try: + if self.execute("Write-Output $graphToken") == "": + raise M365GraphConnectionError( + file=os.path.basename(__file__), + message="Microsoft Graph token is empty or invalid.", + ) + return True + except Exception as e: + logger.error(f"Microsoft Graph connection failed: {e}") + raise M365GraphConnectionError( file=os.path.basename(__file__), - message=f"The user domain {user_domain} does not match any of the tenant domains: {', '.join(self.tenant_identity.tenant_domains)}", + original_exception=e, + message=f"Failed to connect to Microsoft Graph API: {str(e)}", ) - app = msal.ConfidentialClientApplication( - client_id=credentials.client_id, - client_credential=credentials.client_secret, - authority=f"https://login.microsoftonline.com/{credentials.tenant_id}", - ) - - # Validate credentials - result = app.acquire_token_by_username_password( - username=credentials.user, - password=credentials.passwd, - scopes=["https://graph.microsoft.com/.default"], - ) - - if result is None: - raise Exception( - "Unexpected error: Acquiring token in behalf of user did not return a result." + def test_teams_connection(self) -> bool: + """Test Microsoft Teams API connection and raise exception if it fails.""" + try: + self.execute( + '$teamstokenBody = @{ Grant_Type = "client_credentials"; Scope = "48ac35b8-9aa8-4d74-927d-1f4a14a0b239/.default"; Client_Id = $clientID; Client_Secret = $clientSecret }' + ) + self.execute( + '$teamsToken = Invoke-RestMethod -Uri "https://login.microsoftonline.com/$tenantID/oauth2/v2.0/token" -Method POST -Body $teamstokenBody | Select-Object -ExpandProperty Access_Token' + ) + if self.execute("Write-Output $teamsToken") == "": + raise M365TeamsConnectionError( + file=os.path.basename(__file__), + message="Microsoft Teams token is empty or invalid.", + ) + return True + except Exception as e: + logger.error(f"Microsoft Teams connection failed: {e}") + raise M365TeamsConnectionError( + file=os.path.basename(__file__), + original_exception=e, + message=f"Failed to connect to Microsoft Teams API: {str(e)}", ) - if "access_token" not in result: - raise Exception(f"MsGraph Error {result.get('error_description')}") - - return True + def test_exchange_connection(self) -> bool: + """Test Exchange Online API connection and raise exception if it fails.""" + try: + self.execute( + '$SecureSecret = ConvertTo-SecureString "$clientSecret" -AsPlainText -Force' + ) + self.execute( + '$exchangeToken = Get-MsalToken -clientID "$clientID" -tenantID "$tenantID" -clientSecret $SecureSecret -Scopes "https://outlook.office365.com/.default"' + ) + if self.execute("Write-Output $exchangeToken") == "": + raise M365ExchangeConnectionError( + file=os.path.basename(__file__), + message="Exchange Online token is empty or invalid.", + ) + return True + except Exception as e: + logger.error(f"Exchange Online connection failed: {e}") + raise M365ExchangeConnectionError( + file=os.path.basename(__file__), + original_exception=e, + message=f"Failed to connect to Exchange Online API: {str(e)}", + ) def connect_microsoft_teams(self) -> dict: """ @@ -182,7 +302,18 @@ class M365PowerShell(PowerShellSession): Note: This method requires the Microsoft Teams PowerShell module to be installed. """ - return self.execute("Connect-MicrosoftTeams -Credential $credential") + if self.execute("Write-Output $credential") != "": # User Auth + return self.execute("Connect-MicrosoftTeams -Credential $credential") + else: # Application Auth + self.execute( + '$teamstokenBody = @{ Grant_Type = "client_credentials"; Scope = "48ac35b8-9aa8-4d74-927d-1f4a14a0b239/.default"; Client_Id = $clientID; Client_Secret = $clientSecret }' + ) + self.execute( + '$teamsToken = Invoke-RestMethod -Uri "https://login.microsoftonline.com/$tenantID/oauth2/v2.0/token" -Method POST -Body $teamstokenBody | Select-Object -ExpandProperty Access_Token' + ) + return self.execute( + 'Connect-MicrosoftTeams -AccessTokens @("$graphToken","$teamsToken")' + ) def get_teams_settings(self) -> dict: """ @@ -276,7 +407,18 @@ class M365PowerShell(PowerShellSession): Note: This method requires the Exchange Online PowerShell module to be installed. """ - return self.execute("Connect-ExchangeOnline -Credential $credential") + if self.execute("Write-Output $credential") != "": # User Auth + return self.execute("Connect-ExchangeOnline -Credential $credential") + else: # Application Auth + self.execute( + '$SecureSecret = ConvertTo-SecureString "$clientSecret" -AsPlainText -Force' + ) + self.execute( + '$exchangeToken = Get-MsalToken -clientID "$clientID" -tenantID "$tenantID" -clientSecret $SecureSecret -Scopes "https://outlook.office365.com/.default"' + ) + return self.execute( + 'Connect-ExchangeOnline -AccessToken $exchangeToken.AccessToken -Organization "$tenantID"' + ) def get_audit_log_config(self) -> dict: """ @@ -772,25 +914,20 @@ def initialize_m365_powershell_modules(): bool: True if all modules were successfully initialized, False otherwise """ - REQUIRED_MODULES = [ - "ExchangeOnlineManagement", - "MicrosoftTeams", - ] + REQUIRED_MODULES = ["ExchangeOnlineManagement", "MicrosoftTeams", "MSAL.PS"] pwsh = PowerShellSession() try: for module in REQUIRED_MODULES: try: # Check if module is already installed - result = pwsh.execute( - f"Get-Module -ListAvailable -Name {module}", timeout=5 - ) + result = pwsh.execute(f"Get-Module -ListAvailable {module}", timeout=5) # Install module if not installed if not result: install_result = pwsh.execute( - f'Install-Module -Name "{module}" -Force -AllowClobber -Scope CurrentUser', - timeout=30, + f'Install-Module "{module}" -Force -AllowClobber -Scope CurrentUser', + timeout=60, ) if install_result: logger.warning( @@ -800,7 +937,7 @@ def initialize_m365_powershell_modules(): logger.info(f"Successfully installed module {module}") # Import module - pwsh.execute(f'Import-Module -Name "{module}" -Force', timeout=1) + pwsh.execute(f'Import-Module "{module}" -Force', timeout=1) except Exception as error: logger.error(f"Failed to initialize module {module}: {str(error)}") diff --git a/prowler/providers/m365/m365_provider.py b/prowler/providers/m365/m365_provider.py index bdd5a0b98a..263fc67ef5 100644 --- a/prowler/providers/m365/m365_provider.py +++ b/prowler/providers/m365/m365_provider.py @@ -43,9 +43,7 @@ from prowler.providers.m365.exceptions.exceptions import ( M365NotTenantIdButClientIdAndClientSecretError, M365NotValidClientIdError, M365NotValidClientSecretError, - M365NotValidPasswordError, M365NotValidTenantIdError, - M365NotValidUserError, M365SetUpRegionConfigError, M365SetUpSessionError, M365TenantIdAndClientIdNotBelongingToClientSecretError, @@ -172,7 +170,7 @@ class M365Provider(Provider): # Get the dict from the static credentials m365_credentials = None - if tenant_id and client_id and client_secret and user and password: + if tenant_id and client_id and client_secret: m365_credentials = self.validate_static_credentials( tenant_id=tenant_id, client_id=client_id, @@ -204,6 +202,7 @@ class M365Provider(Provider): # Set up PowerShell session credentials self._credentials = self.setup_powershell( env_auth=env_auth, + sp_env_auth=sp_env_auth, m365_credentials=m365_credentials, identity=self.identity, init_modules=init_modules, @@ -381,6 +380,7 @@ class M365Provider(Provider): @staticmethod def setup_powershell( env_auth: bool = False, + sp_env_auth: bool = False, m365_credentials: dict = {}, identity: M365IdentityInfo = None, init_modules: bool = False, @@ -395,12 +395,13 @@ class M365Provider(Provider): If env_auth is True, retrieves from environment variables. If False, returns empty credentials. """ + logger.info("M365 provider: Setting up PowerShell session...") credentials = None if m365_credentials: credentials = M365Credentials( - user=m365_credentials.get("user", ""), - passwd=m365_credentials.get("password", ""), + user=m365_credentials.get("user", None), + passwd=m365_credentials.get("password", None), client_id=m365_credentials.get("client_id", ""), client_secret=m365_credentials.get("client_secret", ""), tenant_id=m365_credentials.get("tenant_id", ""), @@ -421,6 +422,7 @@ class M365Provider(Provider): file=os.path.basename(__file__), message="Missing M365_USER or M365_PASSWORD environment variables required for credentials authentication.", ) + credentials = M365Credentials( client_id=client_id, client_secret=client_secret, @@ -430,14 +432,25 @@ class M365Provider(Provider): passwd=m365_password, ) + elif sp_env_auth: + client_id = getenv("AZURE_CLIENT_ID") + client_secret = getenv("AZURE_CLIENT_SECRET") + tenant_id = getenv("AZURE_TENANT_ID") + credentials = M365Credentials( + client_id=client_id, + client_secret=client_secret, + tenant_id=tenant_id, + tenant_domains=identity.tenant_domains, + ) + if credentials: - if identity: + if identity and credentials.user: identity.user = credentials.user test_session = M365PowerShell(credentials, identity) try: + if init_modules: + initialize_m365_powershell_modules() if test_session.test_credentials(credentials): - if init_modules: - initialize_m365_powershell_modules() return credentials raise M365UserCredentialsError( file=os.path.basename(__file__), @@ -526,6 +539,8 @@ class M365Provider(Provider): tenant_id=m365_credentials["tenant_id"], client_id=m365_credentials["client_id"], client_secret=m365_credentials["client_secret"], + user=m365_credentials["user"], + password=m365_credentials["password"], ) return credentials except ClientAuthenticationError as error: @@ -691,8 +706,8 @@ class M365Provider(Provider): tenant_id=tenant_id, client_id=client_id, client_secret=client_secret, - user="user", - password="password", + user=None, + password=None, ) else: m365_credentials = M365Provider.validate_static_credentials( @@ -742,17 +757,13 @@ class M365Provider(Provider): logger.info("M365 provider: Identity retrieved successfully") # Set up PowerShell credentials - if user and password: - M365Provider.setup_powershell( - env_auth, - m365_credentials, - identity, - ) - logger.info("M365 provider: Connection to PowerShell successful") - else: - logger.info( - "M365 provider: Connection to PowerShell has not been requested" - ) + M365Provider.setup_powershell( + env_auth, + sp_env_auth, + m365_credentials, + identity, + ) + logger.info("M365 provider: Connection to PowerShell successful") return Connection(is_connected=True) @@ -1033,20 +1044,6 @@ class M365Provider(Provider): message="The provided Client Secret is not valid.", ) - # Validate the User - if not user: - raise M365NotValidUserError( - file=os.path.basename(__file__), - message="The provided User is not valid.", - ) - - # Validate the Password - if not password: - raise M365NotValidPasswordError( - file=os.path.basename(__file__), - message="The provided Password is not valid.", - ) - try: M365Provider.verify_client(tenant_id, client_id, client_secret) return { diff --git a/prowler/providers/m365/models.py b/prowler/providers/m365/models.py index efc745f4f6..89cfa8672d 100644 --- a/prowler/providers/m365/models.py +++ b/prowler/providers/m365/models.py @@ -24,9 +24,9 @@ class M365RegionConfig(BaseModel): class M365Credentials(BaseModel): - user: str = "" - passwd: str = "" - encrypted_passwd: str = "" + user: Optional[str] = None + passwd: Optional[str] = None + encrypted_passwd: Optional[str] = None client_id: str = "" client_secret: str = "" tenant_id: str = "" diff --git a/prowler/providers/m365/services/exchange/exchange_service.py b/prowler/providers/m365/services/exchange/exchange_service.py index d8b0fbfa3a..07bbbe2ae6 100644 --- a/prowler/providers/m365/services/exchange/exchange_service.py +++ b/prowler/providers/m365/services/exchange/exchange_service.py @@ -260,7 +260,7 @@ class ExternalMailConfig(BaseModel): class TransportRule(BaseModel): name: str scl: Optional[int] - sender_domain_is: list[str] + sender_domain_is: Optional[list[str]] redirect_message_to: Optional[list[str]] diff --git a/pyproject.toml b/pyproject.toml index 401e88d341..0bc29b065c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,8 @@ dependencies = [ "azure-mgmt-monitor==6.0.2", "azure-mgmt-network==28.1.0", "azure-mgmt-rdbms==10.1.0", + "azure-mgmt-recoveryservices==3.1.0", + "azure-mgmt-recoveryservicesbackup==9.2.0", "azure-mgmt-resource==23.3.0", "azure-mgmt-search==9.1.0", "azure-mgmt-security==7.0.0", @@ -38,8 +40,8 @@ dependencies = [ "botocore==1.35.99", "colorama==0.4.6", "cryptography==44.0.1", - "dash==2.18.2", - "dash-bootstrap-components==1.6.0", + "dash==3.1.1", + "dash-bootstrap-components==2.0.3", "detect-secrets==1.5.0", "google-api-python-client==2.163.0", "google-auth-httplib2>=0.1,<0.3", @@ -59,7 +61,7 @@ dependencies = [ "slack-sdk==3.34.0", "tabulate==0.9.0", "tzlocal==5.3.1", - "checkov (>=3.2.434,<4.0.0)", + "checkov==3.2.445", "py-iam-expand==0.1.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." @@ -68,7 +70,7 @@ maintainers = [{name = "Prowler Engineering", email = "engineering@prowler.com"} name = "prowler" readme = "README.md" requires-python = ">3.9.1,<3.13" -version = "5.8.0" +version = "5.9.0" [project.scripts] prowler = "prowler.__main__:prowler" diff --git a/tests/providers/aws/lib/mutelist/aws_mutelist_test.py b/tests/providers/aws/lib/mutelist/aws_mutelist_test.py index 29a987e752..3e3afd98e9 100644 --- a/tests/providers/aws/lib/mutelist/aws_mutelist_test.py +++ b/tests/providers/aws/lib/mutelist/aws_mutelist_test.py @@ -7,6 +7,7 @@ import yaml from boto3 import client, resource from mock import MagicMock, patch from moto import mock_aws +import pytest from prowler.config.config import encoding_format_utf_8 from prowler.providers.aws.lib.mutelist.mutelist import AWSMutelist @@ -321,6 +322,30 @@ class TestAWSMutelist: assert mutelist.mutelist == {} assert mutelist.mutelist_file_path is None + def test_validate_mutelist_raise_on_exception(self): + mutelist_path = MUTELIST_FIXTURE_PATH + with open(mutelist_path) as f: + mutelist_fixture = yaml.safe_load(f)["Mutelist"] + + # Create an invalid mutelist by adding an invalid key + invalid_mutelist = mutelist_fixture.copy() + invalid_mutelist["Accounts1"] = invalid_mutelist["Accounts"] + del invalid_mutelist["Accounts"] + + mutelist = AWSMutelist(mutelist_content=mutelist_fixture) + + # Test that it raises an exception when raise_on_exception=True + with pytest.raises(Exception): + mutelist.validate_mutelist(invalid_mutelist, raise_on_exception=True) + + # Test that it doesn't raise an exception when raise_on_exception=False (default) + result = mutelist.validate_mutelist(invalid_mutelist, raise_on_exception=False) + assert result == {} + + # Test that it doesn't raise an exception when raise_on_exception is not specified + result = mutelist.validate_mutelist(invalid_mutelist) + assert result == {} + def test_mutelist_findings_only_wildcard(self): # Mutelist mutelist_content = { diff --git a/tests/providers/aws/lib/s3/s3_test.py b/tests/providers/aws/lib/s3/s3_test.py index e32b7ad1ec..d6797bb1b9 100644 --- a/tests/providers/aws/lib/s3/s3_test.py +++ b/tests/providers/aws/lib/s3/s3_test.py @@ -11,6 +11,7 @@ from prowler.lib.outputs.html.html import HTML from prowler.lib.outputs.ocsf.ocsf import OCSF from prowler.providers.aws.lib.s3.exceptions.exceptions import S3InvalidBucketNameError from prowler.providers.aws.lib.s3.s3 import S3 +from prowler.providers.common.models import Connection from tests.lib.outputs.compliance.fixtures import ISO27001_2013_AWS from tests.lib.outputs.fixtures.fixtures import generate_finding_output from tests.providers.aws.utils import AWS_REGION_US_EAST_1 @@ -318,32 +319,59 @@ class TestS3: @mock_aws def test_test_connection_S3(self): - current_session = boto3.session.Session(region_name=AWS_REGION_US_EAST_1) + # Create a mock IAM user + iam_client = boto3.client("iam", region_name=AWS_REGION_US_EAST_1) + iam_user = iam_client.create_user(UserName="test-user")["User"] + # Create a mock IAM access keys + access_key = iam_client.create_access_key(UserName=iam_user["UserName"])[ + "AccessKey" + ] + + # Create bucket + current_session = boto3.session.Session( + aws_access_key_id=access_key["AccessKeyId"], + aws_secret_access_key=access_key["SecretAccessKey"], + region_name=AWS_REGION_US_EAST_1, + ) s3_client = current_session.client("s3") s3_client.create_bucket(Bucket=S3_BUCKET_NAME) - s3 = S3.test_connection( - session=current_session, + + connection = S3.test_connection( + aws_region=AWS_REGION_US_EAST_1, bucket_name=S3_BUCKET_NAME, + aws_access_key_id=access_key["AccessKeyId"], + aws_secret_access_key=access_key["SecretAccessKey"], ) - assert s3 is not None - assert s3.is_connected is True - assert s3.error is None + assert isinstance(connection, Connection) + assert connection.is_connected is True + assert connection.error is None @mock_aws def test_test_connection_S3_bucket_invalid_name(self): - current_session = boto3.session.Session(region_name=AWS_REGION_US_EAST_1) + # Create a mock IAM user + iam_client = boto3.client("iam", region_name=AWS_REGION_US_EAST_1) + iam_user = iam_client.create_user(UserName="test-user")["User"] + # Create a mock IAM access keys + access_key = iam_client.create_access_key(UserName=iam_user["UserName"])[ + "AccessKey" + ] + + # Create bucket (with valid name) + current_session = boto3.session.Session( + aws_access_key_id=access_key["AccessKeyId"], + aws_secret_access_key=access_key["SecretAccessKey"], + region_name=AWS_REGION_US_EAST_1, + ) s3_client = current_session.client("s3") - s3_client.create_bucket(Bucket=S3_BUCKET_NAME) - with pytest.raises(S3InvalidBucketNameError): - s3 = S3.test_connection( - session=current_session, - bucket_name="invalid_bucket", - ) - assert s3 is not None - assert s3.is_connected is False - assert s3.error is not None + with pytest.raises(S3InvalidBucketNameError): + S3.test_connection( + aws_region=AWS_REGION_US_EAST_1, + bucket_name="invalid_bucket", + aws_access_key_id=access_key["AccessKeyId"], + aws_secret_access_key=access_key["SecretAccessKey"], + ) @mock_aws def test_init_without_session(self): diff --git a/tests/providers/aws/services/codebuild/codebuild_project_not_publicly_accessible/__init__.py b/tests/providers/aws/services/codebuild/codebuild_project_not_publicly_accessible/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/providers/aws/services/codebuild/codebuild_project_not_publicly_accessible/codebuild_project_not_publicly_accessible_test.py b/tests/providers/aws/services/codebuild/codebuild_project_not_publicly_accessible/codebuild_project_not_publicly_accessible_test.py new file mode 100644 index 0000000000..f94146d4fc --- /dev/null +++ b/tests/providers/aws/services/codebuild/codebuild_project_not_publicly_accessible/codebuild_project_not_publicly_accessible_test.py @@ -0,0 +1,176 @@ +from unittest import mock + +from prowler.providers.aws.services.codebuild.codebuild_service import Project + +AWS_REGION = "eu-west-1" +AWS_ACCOUNT_NUMBER = "123456789012" + + +class Test_codebuild_project_not_publicly_accessible: + def test_project_public(self): + codebuild_client = mock.MagicMock + project_name = "test-project" + project_arn = f"arn:aws:codebuild:{AWS_REGION}:{AWS_ACCOUNT_NUMBER}:project/{project_name}" + codebuild_client.projects = { + project_arn: Project( + name=project_name, + arn=project_arn, + region="eu-west-1", + project_visibility="PUBLIC", + tags=[], + ) + } + + with ( + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_service.Codebuild", + codebuild_client, + ), + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_project_not_publicly_accessible.codebuild_project_not_publicly_accessible.codebuild_client", + codebuild_client, + ), + ): + from prowler.providers.aws.services.codebuild.codebuild_project_not_publicly_accessible.codebuild_project_not_publicly_accessible import ( + codebuild_project_not_publicly_accessible, + ) + + check = codebuild_project_not_publicly_accessible() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"CodeBuild project {project_name} is public." + ) + assert result[0].resource_id == project_name + assert result[0].resource_arn == project_arn + assert result[0].resource_tags == [] + assert result[0].region == AWS_REGION + + def test_project_private(self): + codebuild_client = mock.MagicMock + project_name = "test-project" + project_arn = f"arn:aws:codebuild:{AWS_REGION}:{AWS_ACCOUNT_NUMBER}:project/{project_name}" + codebuild_client.projects = { + project_arn: Project( + name=project_name, + arn=project_arn, + region="eu-west-1", + project_visibility="PRIVATE", + tags=[], + ) + } + + with ( + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_service.Codebuild", + codebuild_client, + ), + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_project_not_publicly_accessible.codebuild_project_not_publicly_accessible.codebuild_client", + codebuild_client, + ), + ): + from prowler.providers.aws.services.codebuild.codebuild_project_not_publicly_accessible.codebuild_project_not_publicly_accessible import ( + codebuild_project_not_publicly_accessible, + ) + + check = codebuild_project_not_publicly_accessible() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"CodeBuild project {project_name} is private." + ) + assert result[0].resource_id == project_name + assert result[0].resource_arn == project_arn + assert result[0].resource_tags == [] + assert result[0].region == AWS_REGION + + def test_project_no_visibility_set(self): + codebuild_client = mock.MagicMock + project_name = "test-project" + project_arn = f"arn:aws:codebuild:{AWS_REGION}:{AWS_ACCOUNT_NUMBER}:project/{project_name}" + codebuild_client.projects = { + project_arn: Project( + name=project_name, + arn=project_arn, + region="eu-west-1", + project_visibility=None, + tags=[], + ) + } + + with ( + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_service.Codebuild", + codebuild_client, + ), + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_project_not_publicly_accessible.codebuild_project_not_publicly_accessible.codebuild_client", + codebuild_client, + ), + ): + from prowler.providers.aws.services.codebuild.codebuild_project_not_publicly_accessible.codebuild_project_not_publicly_accessible import ( + codebuild_project_not_publicly_accessible, + ) + + check = codebuild_project_not_publicly_accessible() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"CodeBuild project {project_name} is public." + ) + assert result[0].resource_id == project_name + assert result[0].resource_arn == project_arn + assert result[0].resource_tags == [] + assert result[0].region == AWS_REGION + + def test_project_empty_visibility(self): + codebuild_client = mock.MagicMock + project_name = "test-project" + project_arn = f"arn:aws:codebuild:{AWS_REGION}:{AWS_ACCOUNT_NUMBER}:project/{project_name}" + codebuild_client.projects = { + project_arn: Project( + name=project_name, + arn=project_arn, + region="eu-west-1", + project_visibility="", + tags=[], + ) + } + + with ( + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_service.Codebuild", + codebuild_client, + ), + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_project_not_publicly_accessible.codebuild_project_not_publicly_accessible.codebuild_client", + codebuild_client, + ), + ): + from prowler.providers.aws.services.codebuild.codebuild_project_not_publicly_accessible.codebuild_project_not_publicly_accessible import ( + codebuild_project_not_publicly_accessible, + ) + + check = codebuild_project_not_publicly_accessible() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"CodeBuild project {project_name} is public." + ) + assert result[0].resource_id == project_name + assert result[0].resource_arn == project_arn + assert result[0].resource_tags == [] + assert result[0].region == AWS_REGION diff --git a/tests/providers/aws/services/codebuild/codebuild_service_test.py b/tests/providers/aws/services/codebuild/codebuild_service_test.py index f35c55ed58..9884b3a09f 100644 --- a/tests/providers/aws/services/codebuild/codebuild_service_test.py +++ b/tests/providers/aws/services/codebuild/codebuild_service_test.py @@ -28,6 +28,7 @@ build_id = "test:93f838a7-cd20-48ae-90e5-c10fbbc78ca6" last_invoked_time = datetime.now() - timedelta(days=2) bitbucket_url = "https://bitbucket.org/example/repo.git" secondary_bitbucket_url = "https://bitbucket.org/example/secondary-repo.git" +project_visibility = "PRIVATE" report_group_arn = f"arn:{AWS_COMMERCIAL_PARTITION}:codebuild:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:report-group/{project_name}" @@ -71,6 +72,7 @@ def mock_make_api_call(self, operation_name, kwarg): }, }, "tags": [{"key": "Name", "value": project_name}], + "projectVisibility": project_visibility, } ] } @@ -152,6 +154,7 @@ class Test_Codebuild_Service: ) assert codebuild.projects[project_arn].tags[0]["key"] == "Name" assert codebuild.projects[project_arn].tags[0]["value"] == project_name + assert codebuild.projects[project_arn].project_visibility == project_visibility # Asserttions related with report groups assert len(codebuild.report_groups) == 1 assert isinstance(codebuild.report_groups, dict) diff --git a/tests/providers/aws/services/ec2/ec2_launch_template_imdsv2_required/ec2_launch_template_imdsv2_required_test.py b/tests/providers/aws/services/ec2/ec2_launch_template_imdsv2_required/ec2_launch_template_imdsv2_required_test.py index 22ff04f349..fd1a5924ec 100644 --- a/tests/providers/aws/services/ec2/ec2_launch_template_imdsv2_required/ec2_launch_template_imdsv2_required_test.py +++ b/tests/providers/aws/services/ec2/ec2_launch_template_imdsv2_required/ec2_launch_template_imdsv2_required_test.py @@ -27,6 +27,24 @@ def mock_make_api_call(self, operation_name, kwarg): return make_api_call(self, operation_name, kwarg) +def mock_make_api_call_empty(self, operation_name, kwarg): + if operation_name == "DescribeLaunchTemplateVersions": + return { + "LaunchTemplateVersions": [ + { + "VersionNumber": 1, + "LaunchTemplateData": { + "MetadataOptions": { + "HttpEndpoint": "", + "HttpTokens": "required", + } + }, + } + ] + } + return make_api_call(self, operation_name, kwarg) + + def mock_make_api_call_not_required(self, operation_name, kwarg): if operation_name == "DescribeLaunchTemplateVersions": return { @@ -134,6 +152,66 @@ class Test_ec2_launch_template_imdsv2_required: ) assert result[0].resource_tags == [] + @mock_aws + def test_launch_template_imdsv2_required_empty(self): + with mock.patch( + "botocore.client.BaseClient._make_api_call", + new=mock_make_api_call_empty, + ): + ec2_client = client("ec2", region_name=AWS_REGION_US_EAST_1) + launch_template_name = "test-imdsv2-required-empty" + ec2_client.create_launch_template( + LaunchTemplateName=launch_template_name, + VersionDescription="Launch Template with IMDSv2 required", + LaunchTemplateData={ + "InstanceType": "t1.micro", + "MetadataOptions": { + "HttpEndpoint": "", + "HttpTokens": "required", + }, + }, + ) + + launch_template_id = ec2_client.describe_launch_templates( + LaunchTemplateNames=[launch_template_name] + )["LaunchTemplates"][0]["LaunchTemplateId"] + + from prowler.providers.aws.services.ec2.ec2_service import EC2 + + 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.ec2.ec2_launch_template_imdsv2_required.ec2_launch_template_imdsv2_required.ec2_client", + new=EC2(aws_provider), + ), + ): + # Test Check + from prowler.providers.aws.services.ec2.ec2_launch_template_imdsv2_required.ec2_launch_template_imdsv2_required import ( + ec2_launch_template_imdsv2_required, + ) + + check = ec2_launch_template_imdsv2_required() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"EC2 Launch Template {launch_template_name} has metadata service disabled in the following versions: 1." + ) + assert result[0].resource_id == launch_template_id + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:ec2:{AWS_REGION_US_EAST_1}:123456789012:launch-template/{launch_template_id}" + ) + assert result[0].resource_tags == [] + @mock_aws def test_launch_template_imdsv2_not_required(self): with mock.patch( diff --git a/tests/providers/aws/services/ecs/ecs_cluster_container_insights_enabled/ecs_cluster_container_insights_enabled_test.py b/tests/providers/aws/services/ecs/ecs_cluster_container_insights_enabled/ecs_cluster_container_insights_enabled_test.py index 45483453c7..7f3fc68add 100644 --- a/tests/providers/aws/services/ecs/ecs_cluster_container_insights_enabled/ecs_cluster_container_insights_enabled_test.py +++ b/tests/providers/aws/services/ecs/ecs_cluster_container_insights_enabled/ecs_cluster_container_insights_enabled_test.py @@ -109,6 +109,45 @@ class Test_ecs_clusters_container_insights_enabled: == f"ECS cluster {CLUSTER_NAME} has container insights enabled." ) + @mock_aws + def test_cluster_enhanced_container_insights(self): + ecs_client = client("ecs", region_name=AWS_REGION_US_EAST_1) + cluster_settings = [ + {"name": "containerInsights", "value": "enhanced"}, + ] + cluster_arn = ecs_client.create_cluster( + clusterName=CLUSTER_NAME, + settings=cluster_settings, + )["cluster"]["clusterArn"] + + from prowler.providers.aws.services.ecs.ecs_service import ECS + + 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.ecs.ecs_cluster_container_insights_enabled.ecs_cluster_container_insights_enabled.ecs_client", + new=ECS(aws_provider), + ), + ): + from prowler.providers.aws.services.ecs.ecs_cluster_container_insights_enabled.ecs_cluster_container_insights_enabled import ( + ecs_cluster_container_insights_enabled, + ) + + check = ecs_cluster_container_insights_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].resource_arn == cluster_arn + assert ( + result[0].status_extended + == f"ECS cluster {CLUSTER_NAME} has container insights enhanced." + ) + @mock_aws def test_cluster_disabled_container_insights(self): ecs_client = client("ecs", region_name=AWS_REGION_US_EAST_1) diff --git a/tests/providers/aws/services/firehose/firehose_service_test.py b/tests/providers/aws/services/firehose/firehose_service_test.py index 9d0d72cfbd..340cfd3173 100644 --- a/tests/providers/aws/services/firehose/firehose_service_test.py +++ b/tests/providers/aws/services/firehose/firehose_service_test.py @@ -2,8 +2,13 @@ from boto3 import client from moto import mock_aws from prowler.providers.aws.services.firehose.firehose_service import ( + DatabaseSourceDescription, + DirectPutSourceDescription, EncryptionStatus, Firehose, + KinesisStreamSourceDescription, + MSKSourceDescription, + Source, ) from tests.providers.aws.utils import ( AWS_ACCOUNT_NUMBER, @@ -152,3 +157,102 @@ class Test_Firehose_Service: firehose.delivery_streams[arn].kms_key_arn == f"arn:aws:kms:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:key/test-kms-key-id" ) + + @mock_aws + def test_describe_delivery_stream_source_direct_put(self): + # Generate S3 client + s3_client = client("s3", region_name=AWS_REGION_EU_WEST_1) + s3_client.create_bucket( + Bucket="test-bucket", + CreateBucketConfiguration={"LocationConstraint": AWS_REGION_EU_WEST_1}, + ) + + # Generate Firehose client + firehose_client = client("firehose", region_name=AWS_REGION_EU_WEST_1) + delivery_stream = firehose_client.create_delivery_stream( + DeliveryStreamName="test-delivery-stream", + DeliveryStreamType="DirectPut", + S3DestinationConfiguration={ + "RoleARN": "arn:aws:iam::012345678901:role/firehose-role", + "BucketARN": "arn:aws:s3:::test-bucket", + "Prefix": "", + "BufferingHints": {"IntervalInSeconds": 300, "SizeInMBs": 5}, + "CompressionFormat": "UNCOMPRESSED", + }, + Tags=[{"Key": "key", "Value": "value"}], + ) + arn = delivery_stream["DeliveryStreamARN"] + + # Firehose Client for this test class + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + firehose = Firehose(aws_provider) + + assert len(firehose.delivery_streams) == 1 + assert firehose.delivery_streams[arn].delivery_stream_type == "DirectPut" + + # Test Source structure + assert isinstance(firehose.delivery_streams[arn].source, Source) + assert isinstance( + firehose.delivery_streams[arn].source.direct_put, DirectPutSourceDescription + ) + assert isinstance( + firehose.delivery_streams[arn].source.kinesis_stream, + KinesisStreamSourceDescription, + ) + assert isinstance( + firehose.delivery_streams[arn].source.msk, MSKSourceDescription + ) + assert isinstance( + firehose.delivery_streams[arn].source.database, DatabaseSourceDescription + ) + + @mock_aws + def test_describe_delivery_stream_source_kinesis_stream(self): + # Generate Kinesis client + kinesis_client = client("kinesis", region_name=AWS_REGION_EU_WEST_1) + kinesis_client.create_stream( + StreamName="test-kinesis-stream", + ShardCount=1, + ) + kinesis_stream_arn = f"arn:aws:kinesis:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:stream/test-kinesis-stream" + + # Generate Firehose client + firehose_client = client("firehose", region_name=AWS_REGION_EU_WEST_1) + delivery_stream = firehose_client.create_delivery_stream( + DeliveryStreamName="test-delivery-stream", + DeliveryStreamType="KinesisStreamAsSource", + KinesisStreamSourceConfiguration={ + "KinesisStreamARN": kinesis_stream_arn, + "RoleARN": "arn:aws:iam::012345678901:role/firehose-role", + }, + S3DestinationConfiguration={ + "RoleARN": "arn:aws:iam::012345678901:role/firehose-role", + "BucketARN": "arn:aws:s3:::test-bucket", + "Prefix": "", + "BufferingHints": {"IntervalInSeconds": 300, "SizeInMBs": 5}, + "CompressionFormat": "UNCOMPRESSED", + }, + Tags=[{"Key": "key", "Value": "value"}], + ) + arn = delivery_stream["DeliveryStreamARN"] + + # Firehose Client for this test class + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + firehose = Firehose(aws_provider) + + assert len(firehose.delivery_streams) == 1 + assert ( + firehose.delivery_streams[arn].delivery_stream_type + == "KinesisStreamAsSource" + ) + + # Test Source structure + assert isinstance(firehose.delivery_streams[arn].source, Source) + assert isinstance( + firehose.delivery_streams[arn].source.kinesis_stream, + KinesisStreamSourceDescription, + ) + assert ( + firehose.delivery_streams[arn].source.kinesis_stream.kinesis_stream_arn + == kinesis_stream_arn + ) diff --git a/tests/providers/aws/services/firehose/firehose_stream_encrypted_at_rest/firehose_stream_encrypted_at_rest_test.py b/tests/providers/aws/services/firehose/firehose_stream_encrypted_at_rest/firehose_stream_encrypted_at_rest_test.py index da00aa8900..39ce80d031 100644 --- a/tests/providers/aws/services/firehose/firehose_stream_encrypted_at_rest/firehose_stream_encrypted_at_rest_test.py +++ b/tests/providers/aws/services/firehose/firehose_stream_encrypted_at_rest/firehose_stream_encrypted_at_rest_test.py @@ -198,7 +198,7 @@ class Test_firehose_stream_encrypted_at_rest: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Firehose Stream {stream_name} does not have at rest encryption enabled." + == f"Firehose Stream {stream_name} does not have at rest encryption enabled or the source stream is not encrypted." ) @mock_aws @@ -253,5 +253,74 @@ class Test_firehose_stream_encrypted_at_rest: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Firehose Stream {stream_name} does not have at rest encryption enabled." + == f"Firehose Stream {stream_name} does not have at rest encryption enabled or the source stream is not encrypted." ) + + @mock_aws + def test_stream_kinesis_source_encrypted(self): + # Generate Kinesis client + kinesis_client = client("kinesis", region_name=AWS_REGION_EU_WEST_1) + kinesis_client.create_stream( + StreamName="test-kinesis-stream", + ShardCount=1, + ) + kinesis_stream_arn = f"arn:aws:kinesis:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:stream/test-kinesis-stream" + + # Enable encryption on the Kinesis stream + kinesis_client.start_stream_encryption( + StreamName="test-kinesis-stream", + EncryptionType="KMS", + KeyId=f"arn:aws:kms:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:key/test-kms-key-id", + ) + + # Generate Firehose client + firehose_client = client("firehose", region_name=AWS_REGION_EU_WEST_1) + delivery_stream = firehose_client.create_delivery_stream( + DeliveryStreamName="test-delivery-stream", + DeliveryStreamType="KinesisStreamAsSource", + KinesisStreamSourceConfiguration={ + "KinesisStreamARN": kinesis_stream_arn, + "RoleARN": "arn:aws:iam::012345678901:role/firehose-role", + }, + S3DestinationConfiguration={ + "RoleARN": "arn:aws:iam::012345678901:role/firehose-role", + "BucketARN": "arn:aws:s3:::test-bucket", + "Prefix": "", + "BufferingHints": {"IntervalInSeconds": 300, "SizeInMBs": 5}, + "CompressionFormat": "UNCOMPRESSED", + }, + Tags=[{"Key": "key", "Value": "value"}], + ) + arn = delivery_stream["DeliveryStreamARN"] + stream_name = arn.split("/")[-1] + + from prowler.providers.aws.services.firehose.firehose_service import Firehose + from prowler.providers.aws.services.kinesis.kinesis_service import Kinesis + + 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, + ): + with mock.patch( + "prowler.providers.aws.services.firehose.firehose_stream_encrypted_at_rest.firehose_stream_encrypted_at_rest.firehose_client", + new=Firehose(aws_provider), + ): + with mock.patch( + "prowler.providers.aws.services.firehose.firehose_stream_encrypted_at_rest.firehose_stream_encrypted_at_rest.kinesis_client", + new=Kinesis(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.firehose.firehose_stream_encrypted_at_rest.firehose_stream_encrypted_at_rest import ( + firehose_stream_encrypted_at_rest, + ) + + check = firehose_stream_encrypted_at_rest() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Firehose Stream {stream_name} does not have at rest encryption enabled but the source stream test-kinesis-stream has at rest encryption enabled." + ) 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 78e3b38553..6a9576050c 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 @@ -232,3 +232,179 @@ class Test_iam_no_custom_policy_permissive_role_assumption: result[1].status_extended, ) assert result[1].resource_id == policy_name_permissive + + @mock_aws + def test_policy_resource_with_embedded_wildcard_in_arn(self): + iam_client = client("iam") + policy_name = "policy_with_wildcard_in_arn" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "sts:AssumeRole", + "Resource": "arn:aws:iam::*:role/eks-terraform-*", + } + ], + } + arn = iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document) + )["Policy"]["Arn"] + + from prowler.providers.aws.services.iam.iam_service import IAM + + 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, + ): + 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[0].status == "FAIL" + assert result[0].resource_arn == arn + assert search( + "allows permissive STS Role assumption", result[0].status_extended + ) + assert result[0].resource_id == policy_name + + @mock_aws + def test_policy_resource_list_containing_wildcard(self): + iam_client = client("iam") + policy_name = "policy_with_resource_list" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "sts:AssumeRole", + "Resource": ["arn:aws:iam::123456789012:role/SomeRole", "*"], + } + ], + } + arn = iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document) + )["Policy"]["Arn"] + + from prowler.providers.aws.services.iam.iam_service import IAM + + 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, + ): + 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[0].status == "FAIL" + assert result[0].resource_arn == arn + assert search( + "allows permissive STS Role assumption", result[0].status_extended + ) + assert result[0].resource_id == policy_name + + @mock_aws + def test_policy_resource_list_with_arn_wildcards_should_fail(self): + iam_client = client("iam") + policy_name = "policy_list_wildcard_arn" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "sts:AssumeRole", + "Resource": [ + "arn:aws:iam::123456789012:role/eks-admin", + "arn:aws:iam::*:role/eks-*", + ], + } + ], + } + arn = iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document) + )["Policy"]["Arn"] + + from prowler.providers.aws.services.iam.iam_service import IAM + + 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, + ): + 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[0].status == "FAIL" + assert result[0].resource_arn == arn + assert search( + "allows permissive STS Role assumption", result[0].status_extended + ) + + @mock_aws + def test_policy_resource_list_with_only_wildcarded_arns_should_fail(self): + iam_client = client("iam") + policy_name = "policy_list_scoped_wildcards_only" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "sts:AssumeRole", + "Resource": [ + "arn:aws:iam::*:role/team-*", + "arn:aws:iam::*:role/dev-*", + ], + } + ], + } + arn = iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document) + )["Policy"]["Arn"] + + from prowler.providers.aws.services.iam.iam_service import IAM + + 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, + ): + 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[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/organizations/organizations_scp_check_deny_regions/organizations_scp_check_deny_regions_test.py b/tests/providers/aws/services/organizations/organizations_scp_check_deny_regions/organizations_scp_check_deny_regions_test.py index c575e4abfd..d6d9941960 100644 --- a/tests/providers/aws/services/organizations/organizations_scp_check_deny_regions/organizations_scp_check_deny_regions_test.py +++ b/tests/providers/aws/services/organizations/organizations_scp_check_deny_regions/organizations_scp_check_deny_regions_test.py @@ -17,6 +17,10 @@ def scp_restrict_regions_with_deny(): return '{"Version":"2012-10-17","Statement":{"Effect":"Deny","NotAction":"s3:*","Resource":"*","Condition":{"StringNotEquals":{"aws:RequestedRegion":["eu-central-1","eu-west-1"]}}}}' +def scp_restrict_regions_without_statement(): + return '{"Version":"2012-10-17"}' + + class Test_organizations_scp_check_deny_regions: @mock_aws def test_no_organization(self): @@ -277,3 +281,74 @@ class Test_organizations_scp_check_deny_regions: result = check.execute() assert len(result) == 0 + + @mock_aws + def test_organizations_scp_check_deny_regions_without_statement(self): + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + # Create Organization + conn = client("organizations", region_name=AWS_REGION_EU_WEST_1) + response = conn.describe_organization() + # Delete the default FullAWSAccess policy created by Moto + policies = conn.list_policies(Filter="SERVICE_CONTROL_POLICY")["Policies"] + for policy in policies: + if policy["Name"] == "FullAWSAccess": + policy_id = policy["Id"] + # Detach from all roots + roots = conn.list_roots()["Roots"] + for root in roots: + conn.detach_policy(PolicyId=policy_id, TargetId=root["Id"]) + # Detach from all OUs + ous = conn.list_organizational_units_for_parent( + ParentId=roots[0]["Id"] + )["OrganizationalUnits"] + for ou in ous: + conn.detach_policy(PolicyId=policy_id, TargetId=ou["Id"]) + # Detach from all accounts + accounts = conn.list_accounts()["Accounts"] + for account in accounts: + conn.detach_policy(PolicyId=policy_id, TargetId=account["Id"]) + # Now delete + conn.delete_policy(PolicyId=policy_id) + break + # Create Policy + response_policy = conn.create_policy( + Content=scp_restrict_regions_without_statement(), + Description="Test", + Name="Test", + Type="SERVICE_CONTROL_POLICY", + ) + org_id = response["Organization"]["Id"] + policy_id = response_policy["Policy"]["PolicySummary"]["Id"] + + # Set config variable + aws_provider._audit_config = {"organizations_enabled_regions": ["us-east-1"]} + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.organizations.organizations_scp_check_deny_regions.organizations_scp_check_deny_regions.organizations_client", + new=Organizations(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.organizations.organizations_scp_check_deny_regions.organizations_scp_check_deny_regions import ( + organizations_scp_check_deny_regions, + ) + + check = organizations_scp_check_deny_regions() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].resource_id == response["Organization"]["Id"] + assert ( + "arn:aws:organizations::123456789012:organization/o-" + in result[0].resource_arn + ) + assert ( + result[0].status_extended + == f"AWS Organization {org_id} has SCP policies but don't restrict AWS Regions." + ) + assert result[0].region == AWS_REGION_EU_WEST_1 diff --git a/tests/providers/aws/services/vpc/vpc_endpoint_multi_az_enabled/vpc_endpoint_multi_az_enabled_test.py b/tests/providers/aws/services/vpc/vpc_endpoint_multi_az_enabled/vpc_endpoint_multi_az_enabled_test.py index 9a2e0f2b1b..61e40de6c0 100644 --- a/tests/providers/aws/services/vpc/vpc_endpoint_multi_az_enabled/vpc_endpoint_multi_az_enabled_test.py +++ b/tests/providers/aws/services/vpc/vpc_endpoint_multi_az_enabled/vpc_endpoint_multi_az_enabled_test.py @@ -87,7 +87,7 @@ class Test_vpc_endpoint_for_multi_az: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"VPC Endpoint {vpc_endpoint['VpcEndpointId']} in VPC {vpc['VpcId']} has subnets in different AZs." + == f"VPC Endpoint {vpc_endpoint['VpcEndpointId']} in VPC {vpc['VpcId']} does not have subnets in different AZs." ) assert ( result[0].resource_arn @@ -158,7 +158,7 @@ class Test_vpc_endpoint_for_multi_az: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"VPC Endpoint {vpc_endpoint['VpcEndpointId']} in VPC {vpc['VpcId']} does not have subnets in different AZs." + == f"VPC Endpoint {vpc_endpoint['VpcEndpointId']} in VPC {vpc['VpcId']} has subnets in different AZs." ) assert ( result[0].resource_arn diff --git a/tests/providers/azure/azure_provider_test.py b/tests/providers/azure/azure_provider_test.py index e5e93bad6b..8967142bc5 100644 --- a/tests/providers/azure/azure_provider_test.py +++ b/tests/providers/azure/azure_provider_test.py @@ -110,7 +110,6 @@ class TestAzureProvider: return_value={}, ), ): - with pytest.raises(AzureNoAuthenticationMethodError) as exception: _ = AzureProvider( az_cli_auth, @@ -151,7 +150,6 @@ class TestAzureProvider: return_value={}, ), ): - with pytest.raises(AzureBrowserAuthNoTenantIDError) as exception: _ = AzureProvider( az_cli_auth, @@ -193,7 +191,6 @@ class TestAzureProvider: return_value={}, ), ): - with pytest.raises(AzureTenantIDNoBrowserAuthError) as exception: _ = AzureProvider( az_cli_auth, @@ -224,7 +221,6 @@ class TestAzureProvider: "prowler.providers.azure.azure_provider.SubscriptionClient" ) as mock_resource_client, ): - # Mock the return value of DefaultAzureCredential mock_credentials = MagicMock() mock_credentials.get_token.return_value = AccessToken( @@ -266,7 +262,6 @@ class TestAzureProvider: "prowler.providers.azure.azure_provider.AzureProvider.validate_static_credentials" ) as mock_validate_static_credentials, ): - # Mock the return value of DefaultAzureCredential mock_credentials = MagicMock() mock_credentials.get_token.return_value = AccessToken( @@ -317,7 +312,6 @@ class TestAzureProvider: "prowler.providers.azure.azure_provider.AzureProvider.validate_static_credentials" ) as mock_validate_static_credentials, ): - # Mock the return value of DefaultAzureCredential mock_default_credential.return_value = { "client_id": str(uuid4()), @@ -368,7 +362,6 @@ class TestAzureProvider: "prowler.providers.azure.azure_provider.AzureProvider.validate_static_credentials" ) as mock_validate_static_credentials, ): - # Mock the return value of DefaultAzureCredential mock_default_credential.return_value = { "client_id": str(uuid4()), @@ -442,7 +435,6 @@ class TestAzureProvider: "prowler.providers.azure.azure_provider.AzureProvider.setup_session" ) as mock_setup_session, ): - mock_setup_session.side_effect = AzureHTTPResponseError( file="test_file", original_exception="Simulated HttpResponseError" ) @@ -463,7 +455,6 @@ class TestAzureProvider: with patch( "prowler.providers.azure.azure_provider.AzureProvider.setup_session" ) as mock_setup_session: - mock_setup_session.side_effect = Exception("Simulated Exception") with pytest.raises(Exception) as exception: 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 bb63fd7c5b..3ef2114d4c 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 @@ -296,3 +296,49 @@ class Test_defender_additional_email_configured_with_a_security_contact: result[0].resource_id == f"/subscriptions/{AZURE_SUBSCRIPTION_ID}/providers/Microsoft.Security/securityContacts/default" ) + + def test_defender_default_security_contact_not_found_empty_name(self): + resource_id = f"/subscriptions/{AZURE_SUBSCRIPTION_ID}/providers/Microsoft.Security/securityContacts/default" + defender_client = mock.MagicMock + defender_client.security_contacts = { + AZURE_SUBSCRIPTION_ID: { + resource_id: SecurityContacts( + resource_id=resource_id, + name="", + emails="", + phone="", + alert_notifications_minimal_severity="", + alert_notifications_state="", + notified_roles=[""], + notified_roles_state="", + ) + } + } + contact = defender_client.security_contacts[AZURE_SUBSCRIPTION_ID][resource_id] + contact.name = getattr(contact, "name", "default") or "default" + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.defender.defender_additional_email_configured_with_a_security_contact.defender_additional_email_configured_with_a_security_contact.defender_client", + new=defender_client, + ), + ): + from prowler.providers.azure.services.defender.defender_additional_email_configured_with_a_security_contact.defender_additional_email_configured_with_a_security_contact import ( + defender_additional_email_configured_with_a_security_contact, + ) + + check = defender_additional_email_configured_with_a_security_contact() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"There is not another correct email configured for subscription {AZURE_SUBSCRIPTION_ID}." + ) + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == "default" + assert result[0].resource_id == resource_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 23aadef795..229ae4259e 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 @@ -208,3 +208,50 @@ class Test_defender_ensure_notify_alerts_severity_is_high: result[0].resource_id == f"/subscriptions/{AZURE_SUBSCRIPTION_ID}/providers/Microsoft.Security/securityContacts/default" ) + + def test_defender_default_security_contact_not_found_empty_name(self): + resource_id = f"/subscriptions/{AZURE_SUBSCRIPTION_ID}/providers/Microsoft.Security/securityContacts/default" + defender_client = mock.MagicMock + defender_client.security_contacts = { + AZURE_SUBSCRIPTION_ID: { + resource_id: SecurityContacts( + resource_id=resource_id, + name="", + emails="", + phone="", + alert_notifications_minimal_severity="", + alert_notifications_state="", + notified_roles=[""], + notified_roles_state="", + ) + } + } + + contact = defender_client.security_contacts[AZURE_SUBSCRIPTION_ID][resource_id] + contact.name = getattr(contact, "name", "default") or "default" + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.defender.defender_ensure_notify_alerts_severity_is_high.defender_ensure_notify_alerts_severity_is_high.defender_client", + new=defender_client, + ), + ): + from prowler.providers.azure.services.defender.defender_ensure_notify_alerts_severity_is_high.defender_ensure_notify_alerts_severity_is_high import ( + defender_ensure_notify_alerts_severity_is_high, + ) + + check = defender_ensure_notify_alerts_severity_is_high() + result = check.execute() + assert len(result) == 1 + 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}." + ) + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == "default" + assert result[0].resource_id == resource_id 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 5eff665838..092a3a81a0 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 @@ -208,3 +208,49 @@ class Test_defender_ensure_notify_emails_to_owners: result[0].resource_id == f"/subscriptions/{AZURE_SUBSCRIPTION_ID}/providers/Microsoft.Security/securityContacts/default" ) + + def test_defender_default_security_contact_not_found_empty_name(self): + defender_client = mock.MagicMock() + resource_id = f"/subscriptions/{AZURE_SUBSCRIPTION_ID}/providers/Microsoft.Security/securityContacts/default" + defender_client.security_contacts = { + AZURE_SUBSCRIPTION_ID: { + resource_id: SecurityContacts( + resource_id=resource_id, + name="", + emails="", + phone="", + alert_notifications_minimal_severity="", + alert_notifications_state="", + notified_roles=[""], + notified_roles_state="", + ) + } + } + contact = defender_client.security_contacts[AZURE_SUBSCRIPTION_ID][resource_id] + contact.name = getattr(contact, "name", "default") or "default" + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.defender.defender_ensure_notify_emails_to_owners.defender_ensure_notify_emails_to_owners.defender_client", + new=defender_client, + ), + ): + from prowler.providers.azure.services.defender.defender_ensure_notify_emails_to_owners.defender_ensure_notify_emails_to_owners import ( + defender_ensure_notify_emails_to_owners, + ) + + check = defender_ensure_notify_emails_to_owners() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"The Owner role is not notified for subscription {AZURE_SUBSCRIPTION_ID}." + ) + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == "default" + assert result[0].resource_id == resource_id 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 new file mode 100644 index 0000000000..793b3b4912 --- /dev/null +++ b/tests/providers/azure/services/keyvault/keyvault_access_only_through_private_endpoints/keyvault_access_only_through_private_endpoints_test.py @@ -0,0 +1,193 @@ +from unittest import mock +from uuid import uuid4 + +from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_ID, + set_mocked_azure_provider, +) + + +class Test_keyvault_access_only_through_private_endpoints: + def test_no_key_vaults(self): + keyvault_client = mock.MagicMock + keyvault_client.key_vaults = {} + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.keyvault.keyvault_access_only_through_private_endpoints.keyvault_access_only_through_private_endpoints.keyvault_client", + new=keyvault_client, + ), + ): + from prowler.providers.azure.services.keyvault.keyvault_access_only_through_private_endpoints.keyvault_access_only_through_private_endpoints import ( + keyvault_access_only_through_private_endpoints, + ) + + check = keyvault_access_only_through_private_endpoints() + result = check.execute() + assert len(result) == 0 + + def test_key_vaults_no_private_endpoints(self): + keyvault_client = mock.MagicMock + keyvault_name = "Keyvault Name" + keyvault_id = str(uuid4()) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.keyvault.keyvault_access_only_through_private_endpoints.keyvault_access_only_through_private_endpoints.keyvault_client", + new=keyvault_client, + ), + ): + from prowler.providers.azure.services.keyvault.keyvault_access_only_through_private_endpoints.keyvault_access_only_through_private_endpoints import ( + keyvault_access_only_through_private_endpoints, + ) + from prowler.providers.azure.services.keyvault.keyvault_service import ( + KeyVaultInfo, + VaultProperties, + ) + + keyvault_client.key_vaults = { + AZURE_SUBSCRIPTION_ID: [ + KeyVaultInfo( + id=keyvault_id, + name=keyvault_name, + location="westeurope", + resource_group="resource_group", + properties=VaultProperties( + tenant_id="tenantid", + enable_rbac_authorization=False, + private_endpoint_connections=[], + enable_soft_delete=True, + enable_purge_protection=True, + public_network_access_disabled=False, + ), + ) + ] + } + + check = keyvault_access_only_through_private_endpoints() + result = check.execute() + assert len(result) == 0 + + def test_key_vaults_with_private_endpoints_public_access_enabled(self): + keyvault_client = mock.MagicMock + keyvault_name = "Keyvault Name" + keyvault_id = str(uuid4()) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.keyvault.keyvault_access_only_through_private_endpoints.keyvault_access_only_through_private_endpoints.keyvault_client", + new=keyvault_client, + ), + ): + from prowler.providers.azure.services.keyvault.keyvault_access_only_through_private_endpoints.keyvault_access_only_through_private_endpoints import ( + keyvault_access_only_through_private_endpoints, + ) + from prowler.providers.azure.services.keyvault.keyvault_service import ( + KeyVaultInfo, + PrivateEndpointConnection, + VaultProperties, + ) + + keyvault_client.key_vaults = { + AZURE_SUBSCRIPTION_ID: [ + KeyVaultInfo( + id=keyvault_id, + name=keyvault_name, + location="westeurope", + resource_group="resource_group", + properties=VaultProperties( + tenant_id="tenantid", + enable_rbac_authorization=True, + private_endpoint_connections=[ + PrivateEndpointConnection(id="id1") + ], + enable_soft_delete=True, + enable_purge_protection=True, + public_network_access_disabled=False, + ), + ) + ] + } + + check = keyvault_access_only_through_private_endpoints() + result = check.execute() + assert len(result) == 1 + 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." + ) + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == keyvault_name + assert result[0].resource_id == keyvault_id + assert result[0].location == "westeurope" + + def test_key_vaults_with_private_endpoints_public_access_disabled(self): + keyvault_client = mock.MagicMock + keyvault_name = "Keyvault Name" + keyvault_id = str(uuid4()) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.keyvault.keyvault_access_only_through_private_endpoints.keyvault_access_only_through_private_endpoints.keyvault_client", + new=keyvault_client, + ), + ): + from prowler.providers.azure.services.keyvault.keyvault_access_only_through_private_endpoints.keyvault_access_only_through_private_endpoints import ( + keyvault_access_only_through_private_endpoints, + ) + from prowler.providers.azure.services.keyvault.keyvault_service import ( + KeyVaultInfo, + PrivateEndpointConnection, + VaultProperties, + ) + + keyvault_client.key_vaults = { + AZURE_SUBSCRIPTION_ID: [ + KeyVaultInfo( + id=keyvault_id, + name=keyvault_name, + location="westeurope", + resource_group="resource_group", + properties=VaultProperties( + tenant_id="tenantid", + enable_rbac_authorization=True, + private_endpoint_connections=[ + PrivateEndpointConnection(id="id1") + ], + enable_soft_delete=True, + enable_purge_protection=True, + public_network_access_disabled=True, + ), + ) + ] + } + + check = keyvault_access_only_through_private_endpoints() + result = check.execute() + assert len(result) == 1 + 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." + ) + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == keyvault_name + assert result[0].resource_id == keyvault_id + assert result[0].location == "westeurope" 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 new file mode 100644 index 0000000000..1d7dbb12b5 --- /dev/null +++ b/tests/providers/azure/services/monitor/monitor_alert_service_health_exists/monitor_alert_service_health_exists_test.py @@ -0,0 +1,164 @@ +from unittest import mock + +from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_ID, + set_mocked_azure_provider, +) + + +class Test_monitor_alert_service_health_exists: + def test_monitor_alert_service_health_exists_no_subscriptions(self): + monitor_client = mock.MagicMock() + monitor_client.alert_rules = {} + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.monitor.monitor_alert_service_health_exists.monitor_alert_service_health_exists.monitor_client", + new=monitor_client, + ), + ): + from prowler.providers.azure.services.monitor.monitor_alert_service_health_exists.monitor_alert_service_health_exists import ( + monitor_alert_service_health_exists, + ) + + check = monitor_alert_service_health_exists() + result = check.execute() + assert len(result) == 0 + + def test_no_alert_rules(self): + monitor_client = mock.MagicMock() + monitor_client.alert_rules = {AZURE_SUBSCRIPTION_ID: []} + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.monitor.monitor_alert_service_health_exists.monitor_alert_service_health_exists.monitor_client", + new=monitor_client, + ), + ): + from prowler.providers.azure.services.monitor.monitor_alert_service_health_exists.monitor_alert_service_health_exists import ( + monitor_alert_service_health_exists, + ) + + check = monitor_alert_service_health_exists() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == "Monitor" + assert result[0].resource_id == "Monitor" + assert ( + result[0].status_extended + == f"There is no activity log alert for Service Health in subscription {AZURE_SUBSCRIPTION_ID}." + ) + + def test_alert_rules_configured(self): + monitor_client = mock.MagicMock() + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.monitor.monitor_alert_service_health_exists.monitor_alert_service_health_exists.monitor_client", + new=monitor_client, + ), + ): + from prowler.providers.azure.services.monitor.monitor_alert_service_health_exists.monitor_alert_service_health_exists import ( + monitor_alert_service_health_exists, + ) + from prowler.providers.azure.services.monitor.monitor_service import ( + AlertRule, + AlertRuleAllOfCondition, + AlertRuleAnyOfOrLeafCondition, + ) + + monitor_client.alert_rules = { + AZURE_SUBSCRIPTION_ID: [ + AlertRule( + id="id1", + name="name1", + condition=AlertRuleAllOfCondition( + all_of=[ + AlertRuleAnyOfOrLeafCondition( + field="category", equals="ServiceHealth" + ), + AlertRuleAnyOfOrLeafCondition( + field="properties.incidentType", equals="Incident" + ), + ] + ), + enabled=True, + description="desc1", + ), + ] + } + check = monitor_alert_service_health_exists() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == "name1" + 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}." + ) + + def test_alert_rules_configured_but_disabled(self): + monitor_client = mock.MagicMock() + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.monitor.monitor_alert_service_health_exists.monitor_alert_service_health_exists.monitor_client", + new=monitor_client, + ), + ): + from prowler.providers.azure.services.monitor.monitor_alert_service_health_exists.monitor_alert_service_health_exists import ( + monitor_alert_service_health_exists, + ) + from prowler.providers.azure.services.monitor.monitor_service import ( + AlertRule, + AlertRuleAllOfCondition, + AlertRuleAnyOfOrLeafCondition, + ) + + monitor_client.alert_rules = { + AZURE_SUBSCRIPTION_ID: [ + AlertRule( + id="id1", + name="name1", + condition=AlertRuleAllOfCondition( + all_of=[ + AlertRuleAnyOfOrLeafCondition( + field="category", equals="ServiceHealth" + ), + AlertRuleAnyOfOrLeafCondition( + field="properties.incidentType", equals="Incident" + ), + ] + ), + enabled=False, + description="desc1", + ), + ] + } + check = monitor_alert_service_health_exists() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == "Monitor" + assert result[0].resource_id == "Monitor" + assert ( + result[0].status_extended + == f"There is no activity log alert for Service Health in subscription {AZURE_SUBSCRIPTION_ID}." + ) 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 9aca95c44e..1faff2d267 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 @@ -91,6 +91,21 @@ class Test_monitor_diagnostic_settings_exists: ) from prowler.providers.azure.services.storage.storage_service import ( Account, + BlobProperties, + DeleteRetentionPolicy, + NetworkRuleSet, + ) + + # Create a valid BlobProperties instance + valid_blob_properties = BlobProperties( + id="id", + name="name", + type="type", + default_service_version="default_service_version", + container_delete_retention_policy=DeleteRetentionPolicy( + enabled=False, days=0 + ), + versioning_enabled=True, ) monitor_client.diagnostics_settings = { @@ -138,42 +153,34 @@ class Test_monitor_diagnostic_settings_exists: name="storageaccountname1", resouce_group_name="rg", enable_https_traffic_only=True, - infrastructure_encryption="Enabled", + infrastructure_encryption=True, allow_blob_public_access=True, - network_rule_set="AllowAll", + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), encryption_type="Microsoft.CustomerManagedKeyVault", minimum_tls_version="TLS1_2", private_endpoint_connections=[], - key_expiration_period_in_days=365, + key_expiration_period_in_days="365", location="euwest", - blob_properties=mock.MagicMock( - id="id", - name="name", - type="type", - default_service_version="default_service_version", - container_delete_retention_policy="container_delete_retention_policy", - ), + blob_properties=valid_blob_properties, ), Account( id="/subscriptions/1224a5-123a-123a-123a-1234567890ab/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/storageaccountname2", name="storageaccountname2", resouce_group_name="rg", enable_https_traffic_only=False, - infrastructure_encryption="Enabled", + infrastructure_encryption=True, allow_blob_public_access=False, - network_rule_set="AllowAll", + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), encryption_type="Microsoft.Storage", minimum_tls_version="TLS1_2", private_endpoint_connections=[], - key_expiration_period_in_days=365, + key_expiration_period_in_days="365", location="euwest", - blob_properties=mock.MagicMock( - id="id", - name="name", - type="type", - default_service_version="default_service_version", - container_delete_retention_policy="container_delete_retention_policy", - ), + blob_properties=valid_blob_properties, ), ] } 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 ea1a90638c..707fd11af2 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 @@ -78,6 +78,9 @@ class Test_monitor_storage_account_with_activity_logs_cmk_encrypted: ) from prowler.providers.azure.services.storage.storage_service import ( Account, + BlobProperties, + DeleteRetentionPolicy, + NetworkRuleSet, ) monitor_client.diagnostics_settings = { @@ -125,20 +128,25 @@ class Test_monitor_storage_account_with_activity_logs_cmk_encrypted: name="storageaccountname1", resouce_group_name="rg", enable_https_traffic_only=True, - infrastructure_encryption="Enabled", + infrastructure_encryption=True, # bool allow_blob_public_access=True, - network_rule_set="AllowAll", + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), encryption_type="Microsoft.CustomerManagedKeyVault", minimum_tls_version="TLS1_2", private_endpoint_connections=[], - key_expiration_period_in_days=365, + key_expiration_period_in_days="365", # str location="euwest", - blob_properties=mock.MagicMock( + blob_properties=BlobProperties( id="id", name="name", type="type", default_service_version="default_service_version", - container_delete_retention_policy="container_delete_retention_policy", + container_delete_retention_policy=DeleteRetentionPolicy( + enabled=True, days=7 + ), + versioning_enabled=True, ), ), Account( @@ -146,20 +154,25 @@ class Test_monitor_storage_account_with_activity_logs_cmk_encrypted: name="storageaccountname2", resouce_group_name="rg", enable_https_traffic_only=False, - infrastructure_encryption="Enabled", + infrastructure_encryption=True, # bool allow_blob_public_access=False, - network_rule_set="AllowAll", + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), encryption_type="Microsoft.Storage", minimum_tls_version="TLS1_2", private_endpoint_connections=[], - key_expiration_period_in_days=365, + key_expiration_period_in_days="365", # str location="euwest", - blob_properties=mock.MagicMock( + blob_properties=BlobProperties( id="id", name="name", type="type", default_service_version="default_service_version", - container_delete_retention_policy="container_delete_retention_policy", + container_delete_retention_policy=DeleteRetentionPolicy( + enabled=True, days=7 + ), + versioning_enabled=False, ), ), ] 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 2e71cae298..debcab0321 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 @@ -78,6 +78,9 @@ class Test_monitor_storage_account_with_activity_logs_is_private: ) from prowler.providers.azure.services.storage.storage_service import ( Account, + BlobProperties, + DeleteRetentionPolicy, + NetworkRuleSet, ) monitor_client.diagnostics_settings = { @@ -125,20 +128,25 @@ class Test_monitor_storage_account_with_activity_logs_is_private: name="storageaccountname1", resouce_group_name="rg", enable_https_traffic_only=True, - infrastructure_encryption="Enabled", + infrastructure_encryption=True, allow_blob_public_access=True, - network_rule_set="AllowAll", + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), encryption_type="Microsoft.Storage", minimum_tls_version="TLS1_2", private_endpoint_connections=[], key_expiration_period_in_days=365, location="euwest", - blob_properties=mock.MagicMock( + blob_properties=BlobProperties( id="id", name="name", type="type", default_service_version="default_service_version", - container_delete_retention_policy="container_delete_retention_policy", + container_delete_retention_policy=DeleteRetentionPolicy( + enabled=True, days=7 + ), + versioning_enabled=True, ), ), Account( @@ -146,20 +154,25 @@ class Test_monitor_storage_account_with_activity_logs_is_private: name="storageaccountname2", resouce_group_name="rg", enable_https_traffic_only=False, - infrastructure_encryption="Enabled", + infrastructure_encryption=True, allow_blob_public_access=False, - network_rule_set="AllowAll", + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), encryption_type="Microsoft.Storage", minimum_tls_version="TLS1_2", private_endpoint_connections=[], key_expiration_period_in_days=365, location="euwest", - blob_properties=mock.MagicMock( + blob_properties=BlobProperties( id="id", name="name", type="type", default_service_version="default_service_version", - container_delete_retention_policy="container_delete_retention_policy", + container_delete_retention_policy=DeleteRetentionPolicy( + enabled=True, days=7 + ), + versioning_enabled=False, ), ), ] 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 c235a50d32..6593e90a6c 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 @@ -1,7 +1,10 @@ from unittest import mock from uuid import uuid4 -from prowler.providers.azure.services.storage.storage_service import Account +from prowler.providers.azure.services.storage.storage_service import ( + Account, + NetworkRuleSet, +) from tests.providers.azure.azure_fixtures import ( AZURE_SUBSCRIPTION_ID, set_mocked_azure_provider, @@ -40,16 +43,18 @@ class Test_storage_account_key_access_disabled: Account( id=storage_account_id, name=storage_account_name, - resouce_group_name=None, + resouce_group_name="rg", enable_https_traffic_only=False, infrastructure_encryption=False, - allow_blob_public_access=None, - network_rule_set=None, - encryption_type=None, - minimum_tls_version=None, + allow_blob_public_access=True, + 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", - private_endpoint_connections=None, allow_shared_key_access=True, ) ] @@ -91,16 +96,18 @@ class Test_storage_account_key_access_disabled: Account( id=storage_account_id, name=storage_account_name, - resouce_group_name=None, + resouce_group_name="rg", enable_https_traffic_only=False, infrastructure_encryption=False, - allow_blob_public_access=None, - network_rule_set=None, - encryption_type=None, - minimum_tls_version=None, + allow_blob_public_access=False, + 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", - private_endpoint_connections=None, allow_shared_key_access=False, ) ] 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 bba18c77dd..8aaa2768d5 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 @@ -1,7 +1,10 @@ from unittest import mock from uuid import uuid4 -from prowler.providers.azure.services.storage.storage_service import Account +from prowler.providers.azure.services.storage.storage_service import ( + Account, + NetworkRuleSet, +) from tests.providers.azure.azure_fixtures import ( AZURE_SUBSCRIPTION_ID, set_mocked_azure_provider, @@ -40,16 +43,18 @@ class Test_storage_blob_public_access_level_is_disabled: Account( id=storage_account_id, name=storage_account_name, - resouce_group_name=None, + resouce_group_name="rg", enable_https_traffic_only=False, infrastructure_encryption=False, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), + encryption_type="None", + minimum_tls_version="TLS1_2", + private_endpoint_connections=[], allow_blob_public_access=True, - network_rule_set=None, - encryption_type=None, - minimum_tls_version=None, key_expiration_period_in_days=None, location="westeurope", - private_endpoint_connections=None, ) ] } @@ -90,16 +95,18 @@ class Test_storage_blob_public_access_level_is_disabled: Account( id=storage_account_id, name=storage_account_name, - resouce_group_name=None, + resouce_group_name="rg", enable_https_traffic_only=False, infrastructure_encryption=False, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), + encryption_type="None", + minimum_tls_version="TLS1_2", + private_endpoint_connections=[], allow_blob_public_access=False, - network_rule_set=None, - encryption_type=None, - minimum_tls_version=None, key_expiration_period_in_days=None, location="westeurope", - private_endpoint_connections=None, ) ] } 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 ae0e89716a..357c63a935 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 @@ -45,23 +45,28 @@ class Test_storage_blob_versioning_is_enabled: new=storage_client, ), ): - from prowler.providers.azure.services.storage.storage_service import Account + from prowler.providers.azure.services.storage.storage_service import ( + Account, + NetworkRuleSet, + ) storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( id=storage_account_id, name=storage_account_name, - resouce_group_name=None, + resouce_group_name="rg", enable_https_traffic_only=False, infrastructure_encryption=False, - allow_blob_public_access=None, - network_rule_set=None, + allow_blob_public_access=False, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), encryption_type="None", - minimum_tls_version=None, + minimum_tls_version="TLS1_2", key_expiration_period_in_days=None, location="westeurope", - private_endpoint_connections=None, + private_endpoint_connections=[], blob_properties=storage_account_blob_properties, ) ] @@ -92,12 +97,13 @@ class Test_storage_blob_versioning_is_enabled: Account, BlobProperties, DeleteRetentionPolicy, + NetworkRuleSet, ) storage_account_blob_properties = BlobProperties( - id=None, - name=None, - type=None, + id="id", + name="name", + type="type", default_service_version=None, container_delete_retention_policy=DeleteRetentionPolicy( enabled=False, days=0 @@ -109,16 +115,18 @@ class Test_storage_blob_versioning_is_enabled: Account( id=storage_account_id, name=storage_account_name, - resouce_group_name=None, + resouce_group_name="rg", enable_https_traffic_only=False, infrastructure_encryption=False, - allow_blob_public_access=None, - network_rule_set=None, + allow_blob_public_access=False, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), encryption_type="None", - minimum_tls_version=None, + minimum_tls_version="TLS1_2", key_expiration_period_in_days=None, location="westeurope", - private_endpoint_connections=None, + private_endpoint_connections=[], blob_properties=storage_account_blob_properties, ) ] @@ -158,12 +166,13 @@ class Test_storage_blob_versioning_is_enabled: Account, BlobProperties, DeleteRetentionPolicy, + NetworkRuleSet, ) storage_account_blob_properties = BlobProperties( - id=None, - name=None, - type=None, + id="id", + name="name", + type="type", default_service_version=None, container_delete_retention_policy=DeleteRetentionPolicy( enabled=False, days=0 @@ -175,16 +184,18 @@ class Test_storage_blob_versioning_is_enabled: Account( id=storage_account_id, name=storage_account_name, - resouce_group_name=None, + resouce_group_name="rg", enable_https_traffic_only=False, infrastructure_encryption=False, - allow_blob_public_access=None, - network_rule_set=None, + allow_blob_public_access=False, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), encryption_type="None", - minimum_tls_version=None, + minimum_tls_version="TLS1_2", key_expiration_period_in_days=None, location="westeurope", - private_endpoint_connections=None, + private_endpoint_connections=[], blob_properties=storage_account_blob_properties, ) ] 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 803856572a..e90665d613 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 @@ -1,7 +1,10 @@ from unittest import mock from uuid import uuid4 -from prowler.providers.azure.services.storage.storage_service import Account +from prowler.providers.azure.services.storage.storage_service import ( + Account, + NetworkRuleSet, +) from tests.providers.azure.azure_fixtures import ( AZURE_SUBSCRIPTION_ID, set_mocked_azure_provider, @@ -40,16 +43,18 @@ class Test_storage_cross_tenant_replication_disabled: Account( id=storage_account_id, name=storage_account_name, - resouce_group_name=None, + resouce_group_name="rg", enable_https_traffic_only=False, infrastructure_encryption=False, - allow_blob_public_access=None, - network_rule_set=None, - encryption_type=None, - minimum_tls_version=None, + allow_blob_public_access=False, + 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", - private_endpoint_connections=None, allow_cross_tenant_replication=True, ) ] @@ -91,16 +96,18 @@ class Test_storage_cross_tenant_replication_disabled: Account( id=storage_account_id, name=storage_account_name, - resouce_group_name=None, + resouce_group_name="rg", enable_https_traffic_only=False, infrastructure_encryption=False, - allow_blob_public_access=None, - network_rule_set=None, - encryption_type=None, - minimum_tls_version=None, + allow_blob_public_access=False, + 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", - private_endpoint_connections=None, allow_cross_tenant_replication=False, ) ] 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 9ed07a4644..9c667b372d 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 @@ -43,18 +43,18 @@ class Test_storage_default_network_access_rule_is_denied: Account( id=storage_account_id, name=storage_account_name, - resouce_group_name=None, + resouce_group_name="rg", enable_https_traffic_only=False, infrastructure_encryption=False, - allow_blob_public_access=None, + allow_blob_public_access=False, network_rule_set=NetworkRuleSet( - default_action="Allow", bypass="AzureServices" + bypass="AzureServices", default_action="Allow" ), - encryption_type=None, - minimum_tls_version=None, + encryption_type="None", + minimum_tls_version="TLS1_2", key_expiration_period_in_days=None, location="westeurope", - private_endpoint_connections=None, + private_endpoint_connections=[], ) ] } @@ -95,18 +95,18 @@ class Test_storage_default_network_access_rule_is_denied: Account( id=storage_account_id, name=storage_account_name, - resouce_group_name=None, + resouce_group_name="rg", enable_https_traffic_only=False, infrastructure_encryption=False, - allow_blob_public_access=None, + allow_blob_public_access=False, network_rule_set=NetworkRuleSet( default_action="Deny", bypass="AzureServices" ), - encryption_type=None, - minimum_tls_version=None, + encryption_type="None", + minimum_tls_version="TLS1_2", key_expiration_period_in_days=None, location="westeurope", - private_endpoint_connections=None, + private_endpoint_connections=[], ) ] } 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 33de6038d8..99b7874250 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 @@ -1,7 +1,10 @@ from unittest import mock from uuid import uuid4 -from prowler.providers.azure.services.storage.storage_service import Account +from prowler.providers.azure.services.storage.storage_service import ( + Account, + NetworkRuleSet, +) from tests.providers.azure.azure_fixtures import ( AZURE_SUBSCRIPTION_ID, set_mocked_azure_provider, @@ -40,16 +43,18 @@ class Test_storage_default_to_entra_authorization_enabled: Account( id=storage_account_id, name=storage_account_name, - resouce_group_name=None, + resouce_group_name="rg", enable_https_traffic_only=False, infrastructure_encryption=False, allow_blob_public_access=False, - network_rule_set=None, - encryption_type=None, - minimum_tls_version=None, + 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", - private_endpoint_connections=None, default_to_entra_authorization=True, ) ] @@ -91,16 +96,18 @@ class Test_storage_default_to_entra_authorization_enabled: Account( id=storage_account_id, name=storage_account_name, - resouce_group_name=None, + resouce_group_name="rg", enable_https_traffic_only=False, infrastructure_encryption=False, allow_blob_public_access=False, - network_rule_set=None, - encryption_type=None, - minimum_tls_version=None, + 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", - private_endpoint_connections=None, default_to_entra_authorization=False, ) ] 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 7d8eb1b741..d65978c2ff 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 @@ -43,18 +43,18 @@ class Test_storage_ensure_azure_services_are_trusted_to_access_is_enabled: Account( id=storage_account_id, name=storage_account_name, - resouce_group_name=None, + resouce_group_name="rg", enable_https_traffic_only=False, infrastructure_encryption=False, - allow_blob_public_access=None, + allow_blob_public_access=False, network_rule_set=NetworkRuleSet( bypass="None", default_action="Deny" ), - encryption_type=None, - minimum_tls_version=None, + encryption_type="None", + minimum_tls_version="TLS1_2", key_expiration_period_in_days=None, location="westeurope", - private_endpoint_connections=None, + private_endpoint_connections=[], ) ] } @@ -95,18 +95,18 @@ class Test_storage_ensure_azure_services_are_trusted_to_access_is_enabled: Account( id=storage_account_id, name=storage_account_name, - resouce_group_name=None, + resouce_group_name="rg", enable_https_traffic_only=False, infrastructure_encryption=False, - allow_blob_public_access=None, + allow_blob_public_access=False, network_rule_set=NetworkRuleSet( bypass="AzureServices", default_action="Allow" ), - encryption_type=None, - minimum_tls_version=None, + encryption_type="None", + minimum_tls_version="TLS1_2", key_expiration_period_in_days=None, location="westeurope", - private_endpoint_connections=None, + private_endpoint_connections=[], ) ] } 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 edec8a84a4..7f9803800c 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 @@ -1,7 +1,10 @@ from unittest import mock from uuid import uuid4 -from prowler.providers.azure.services.storage.storage_service import Account +from prowler.providers.azure.services.storage.storage_service import ( + Account, + NetworkRuleSet, +) from tests.providers.azure.azure_fixtures import ( AZURE_SUBSCRIPTION_ID, set_mocked_azure_provider, @@ -40,16 +43,18 @@ class Test_storage_ensure_encryption_with_customer_managed_keys: Account( id=storage_account_id, name=storage_account_name, - resouce_group_name=None, + resouce_group_name="rg", enable_https_traffic_only=False, infrastructure_encryption=False, - allow_blob_public_access=None, - network_rule_set=None, + allow_blob_public_access=False, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), + minimum_tls_version="TLS1_2", + private_endpoint_connections=[], encryption_type="None", - minimum_tls_version=None, key_expiration_period_in_days=None, location="westeurope", - private_endpoint_connections=None, ) ] } @@ -90,16 +95,18 @@ class Test_storage_ensure_encryption_with_customer_managed_keys: Account( id=storage_account_id, name=storage_account_name, - resouce_group_name=None, + resouce_group_name="rg", enable_https_traffic_only=False, infrastructure_encryption=False, - allow_blob_public_access=None, - network_rule_set=None, + allow_blob_public_access=False, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), + minimum_tls_version="TLS1_2", + private_endpoint_connections=[], encryption_type="Microsoft.Keyvault", - minimum_tls_version=None, key_expiration_period_in_days=None, location="westeurope", - private_endpoint_connections=None, ) ] } 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 219673e8f0..e2c97b7e2e 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 @@ -1,7 +1,13 @@ from unittest import mock from uuid import uuid4 -from prowler.providers.azure.services.storage.storage_service import Account, FileShare +from prowler.providers.azure.services.storage.storage_service import ( + Account, + DeleteRetentionPolicy, + FileServiceProperties, + NetworkRuleSet, + SMBProtocolSettings, +) from tests.providers.azure.azure_fixtures import ( AZURE_SUBSCRIPTION_ID, set_mocked_azure_provider, @@ -31,7 +37,7 @@ class Test_storage_ensure_file_shares_soft_delete_is_enabled: result = check.execute() assert len(result) == 0 - def test_no_file_shares(self): + def test_storage_account_no_file_properties(self): storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account" storage_client = mock.MagicMock @@ -40,17 +46,19 @@ class Test_storage_ensure_file_shares_soft_delete_is_enabled: Account( id=storage_account_id, name=storage_account_name, - resouce_group_name=None, + resouce_group_name="rg", enable_https_traffic_only=False, infrastructure_encryption=False, - allow_blob_public_access=None, - network_rule_set=None, + allow_blob_public_access=False, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), encryption_type="None", - minimum_tls_version=None, + minimum_tls_version="TLS1_2", key_expiration_period_in_days=None, location="westeurope", - private_endpoint_connections=None, - file_shares=[], + private_endpoint_connections=[], + file_service_properties=None, ) ] } @@ -76,29 +84,35 @@ class Test_storage_ensure_file_shares_soft_delete_is_enabled: def test_file_share_soft_delete_disabled(self): storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account" - file_share = FileShare( - id="fs1", - name="share1", - soft_delete_enabled=False, - retention_days=0, - ) storage_client = mock.MagicMock + 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", + name="default", + type="Microsoft.Storage/storageAccounts/fileServices", + share_delete_retention_policy=retention_policy, + smb_protocol_settings=SMBProtocolSettings( + channel_encryption=[], supported_versions=[] + ), + ) storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( id=storage_account_id, name=storage_account_name, - resouce_group_name=None, + resouce_group_name="rg", enable_https_traffic_only=False, infrastructure_encryption=False, - allow_blob_public_access=None, - network_rule_set=None, + allow_blob_public_access=False, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), encryption_type="None", - minimum_tls_version=None, + minimum_tls_version="TLS1_2", key_expiration_period_in_days=None, location="westeurope", - private_endpoint_connections=None, - file_shares=[file_share], + private_endpoint_connections=[], + file_service_properties=file_service_properties, ) ] } @@ -123,39 +137,45 @@ class Test_storage_ensure_file_shares_soft_delete_is_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"File share {file_share.name} in storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} does not have soft delete enabled or has an invalid retention period." + == f"File share soft delete is not enabled for storage account {storage_account_name}." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID - assert result[0].resource_id == file_share.name - assert result[0].location == "westeurope" assert result[0].resource_name == storage_account_name + assert result[0].resource_id == file_service_properties.id + assert result[0].location == "westeurope" def test_file_share_soft_delete_enabled(self): storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account" - file_share = FileShare( - id="fs2", - name="share2", - soft_delete_enabled=True, - retention_days=7, - ) storage_client = mock.MagicMock + 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", + name="default", + type="Microsoft.Storage/storageAccounts/fileServices", + share_delete_retention_policy=retention_policy, + smb_protocol_settings=SMBProtocolSettings( + channel_encryption=[], supported_versions=[] + ), + ) storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( id=storage_account_id, name=storage_account_name, - resouce_group_name=None, + resouce_group_name="rg", enable_https_traffic_only=False, infrastructure_encryption=False, - allow_blob_public_access=None, - network_rule_set=None, + allow_blob_public_access=False, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), encryption_type="None", - minimum_tls_version=None, + minimum_tls_version="TLS1_2", key_expiration_period_in_days=None, location="westeurope", - private_endpoint_connections=None, - file_shares=[file_share], + private_endpoint_connections=[], + file_service_properties=file_service_properties, ) ] } @@ -180,9 +200,9 @@ class Test_storage_ensure_file_shares_soft_delete_is_enabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"File share {file_share.name} in storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} has soft delete enabled with a retention period of {file_share.retention_days} days." + == f"File share soft delete is enabled for storage account {storage_account_name} with a retention period of {retention_policy.days} days." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID - assert result[0].resource_id == file_share.name - assert result[0].location == "westeurope" assert result[0].resource_name == storage_account_name + assert result[0].resource_id == file_service_properties.id + assert result[0].location == "westeurope" 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 0db80267ba..16ffe488bb 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 @@ -1,7 +1,10 @@ from unittest import mock from uuid import uuid4 -from prowler.providers.azure.services.storage.storage_service import Account +from prowler.providers.azure.services.storage.storage_service import ( + Account, + NetworkRuleSet, +) from tests.providers.azure.azure_fixtures import ( AZURE_SUBSCRIPTION_ID, set_mocked_azure_provider, @@ -40,16 +43,18 @@ class Test_storage_ensure_minimum_tls_version_12: Account( id=storage_account_id, name=storage_account_name, - resouce_group_name=None, + resouce_group_name="rg", enable_https_traffic_only=False, infrastructure_encryption=False, - allow_blob_public_access=None, - network_rule_set=None, + allow_blob_public_access=False, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), encryption_type="None", minimum_tls_version="TLS1_1", key_expiration_period_in_days=None, location="westeurope", - private_endpoint_connections=None, + private_endpoint_connections=[], ) ] } @@ -90,16 +95,18 @@ class Test_storage_ensure_minimum_tls_version_12: Account( id=storage_account_id, name=storage_account_name, - resouce_group_name=None, + resouce_group_name="rg", enable_https_traffic_only=False, infrastructure_encryption=False, - allow_blob_public_access=None, - network_rule_set=None, + 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=None, + private_endpoint_connections=[], ) ] } 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 50f7dfc2d2..2652085907 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 @@ -3,6 +3,7 @@ from uuid import uuid4 from prowler.providers.azure.services.storage.storage_service import ( Account, + NetworkRuleSet, PrivateEndpointConnection, ) from tests.providers.azure.azure_fixtures import ( @@ -45,16 +46,18 @@ class Test_storage_ensure_private_endpoints_in_storage_accounts: Account( id=storage_account_id, name=storage_account_name, - resouce_group_name=None, + resouce_group_name="rg", enable_https_traffic_only=False, infrastructure_encryption=False, - allow_blob_public_access=None, - network_rule_set=None, + allow_blob_public_access=False, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), encryption_type="None", - minimum_tls_version=None, + minimum_tls_version="TLS1_2", key_expiration_period_in_days=None, location="westeurope", - private_endpoint_connections=None, + private_endpoint_connections=[], ) ] } @@ -97,22 +100,24 @@ class Test_storage_ensure_private_endpoints_in_storage_accounts: Account( id=storage_account_id, name=storage_account_name, - resouce_group_name=None, + resouce_group_name="rg", enable_https_traffic_only=False, infrastructure_encryption=False, - allow_blob_public_access=None, - network_rule_set=None, + allow_blob_public_access=False, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), encryption_type="None", - minimum_tls_version=None, + minimum_tls_version="TLS1_2", key_expiration_period_in_days=None, location="westeurope", - private_endpoint_connections=PrivateEndpointConnection( - id=str( - uuid4(), - ), - name="Test Private Endpoint Connection", - type="Test Type", - ), + private_endpoint_connections=[ + PrivateEndpointConnection( + id="f1ef2e48-978a-4b0e-b34f-e6c34a9e0724", + name="Test Private Endpoint Connection", + type="Test Type", + ) + ], ) ] } 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 3bac97b5b5..acb5920815 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 @@ -5,6 +5,7 @@ from prowler.providers.azure.services.storage.storage_service import ( Account, BlobProperties, DeleteRetentionPolicy, + NetworkRuleSet, ) from tests.providers.azure.azure_fixtures import ( AZURE_SUBSCRIPTION_ID, @@ -45,16 +46,18 @@ class Test_storage_ensure_soft_delete_is_enabled: Account( id=storage_account_id, name=storage_account_name, - resouce_group_name=None, + resouce_group_name="rg", enable_https_traffic_only=False, infrastructure_encryption=False, - allow_blob_public_access=None, - network_rule_set=None, + allow_blob_public_access=False, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), encryption_type="None", - minimum_tls_version=None, + minimum_tls_version="TLS1_2", key_expiration_period_in_days=None, location="westeurope", - private_endpoint_connections=None, + private_endpoint_connections=[], blob_properties=storage_account_blob_properties, ) ] @@ -85,29 +88,32 @@ class Test_storage_ensure_soft_delete_is_enabled: storage_account_name = "Test Storage Account" storage_client = mock.MagicMock storage_account_blob_properties = BlobProperties( - id=None, - name=None, - type=None, + id="id", + name="name", + type="type", default_service_version=None, container_delete_retention_policy=DeleteRetentionPolicy( enabled=False, days=7 ), + versioning_enabled=False, ) storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( id=storage_account_id, name=storage_account_name, - resouce_group_name=None, + resouce_group_name="rg", enable_https_traffic_only=False, infrastructure_encryption=False, - allow_blob_public_access=None, - network_rule_set=None, + allow_blob_public_access=False, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), encryption_type="None", - minimum_tls_version=None, + minimum_tls_version="TLS1_2", key_expiration_period_in_days=None, location="westeurope", - private_endpoint_connections=None, + private_endpoint_connections=[], blob_properties=storage_account_blob_properties, ) ] @@ -147,29 +153,32 @@ class Test_storage_ensure_soft_delete_is_enabled: storage_account_name = "Test Storage Account" storage_client = mock.MagicMock storage_account_blob_properties = BlobProperties( - id=None, - name=None, - type=None, + id="id", + name="name", + type="type", default_service_version=None, container_delete_retention_policy=DeleteRetentionPolicy( enabled=True, days=7 ), + versioning_enabled=True, ) storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( id=storage_account_id, name=storage_account_name, - resouce_group_name=None, + resouce_group_name="rg", enable_https_traffic_only=False, infrastructure_encryption=False, - allow_blob_public_access=None, - network_rule_set=None, + allow_blob_public_access=False, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), encryption_type="None", - minimum_tls_version=None, + minimum_tls_version="TLS1_2", key_expiration_period_in_days=None, location="westeurope", - private_endpoint_connections=None, + private_endpoint_connections=[], blob_properties=storage_account_blob_properties, ) ] 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 ce17cb80ed..7ae8dc32eb 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 @@ -3,6 +3,7 @@ from uuid import uuid4 from prowler.providers.azure.services.storage.storage_service import ( Account, + NetworkRuleSet, ReplicationSettings, ) from tests.providers.azure.azure_fixtures import ( @@ -43,16 +44,18 @@ class Test_storage_geo_redundant_enabled: Account( id=storage_account_id, name=storage_account_name, - resouce_group_name=None, + resouce_group_name="rg", enable_https_traffic_only=False, infrastructure_encryption=False, allow_blob_public_access=False, - network_rule_set=None, - encryption_type=None, - minimum_tls_version=None, + 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", - private_endpoint_connections=None, replication_settings=ReplicationSettings.STANDARD_GRS, ) ] @@ -94,16 +97,18 @@ class Test_storage_geo_redundant_enabled: Account( id=storage_account_id, name=storage_account_name, - resouce_group_name=None, + resouce_group_name="rg", enable_https_traffic_only=False, infrastructure_encryption=False, allow_blob_public_access=False, - network_rule_set=None, - encryption_type=None, - minimum_tls_version=None, + 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", - private_endpoint_connections=None, replication_settings=ReplicationSettings.STANDARD_LRS, ) ] 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 c354a92ded..c66fe2dcfd 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 @@ -1,7 +1,10 @@ from unittest import mock from uuid import uuid4 -from prowler.providers.azure.services.storage.storage_service import Account +from prowler.providers.azure.services.storage.storage_service import ( + Account, + NetworkRuleSet, +) from tests.providers.azure.azure_fixtures import ( AZURE_SUBSCRIPTION_ID, set_mocked_azure_provider, @@ -40,16 +43,18 @@ class Test_storage_infrastructure_encryption_is_enabled: Account( id=storage_account_id, name=storage_account_name, - resouce_group_name=None, + resouce_group_name="rg", enable_https_traffic_only=False, infrastructure_encryption=False, - allow_blob_public_access=None, - network_rule_set=None, + allow_blob_public_access=False, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), encryption_type="None", minimum_tls_version="TLS1_1", key_expiration_period_in_days=None, location="westeurope", - private_endpoint_connections=None, + private_endpoint_connections=[], ) ] } @@ -90,16 +95,18 @@ class Test_storage_infrastructure_encryption_is_enabled: Account( id=storage_account_id, name=storage_account_name, - resouce_group_name=None, + resouce_group_name="rg", enable_https_traffic_only=False, infrastructure_encryption=True, - allow_blob_public_access=None, - network_rule_set=None, + allow_blob_public_access=False, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), encryption_type="None", - minimum_tls_version="TLS1_1", + minimum_tls_version="TLS1_2", key_expiration_period_in_days=None, location="westeurope", - private_endpoint_connections=None, + private_endpoint_connections=[], ) ] } 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 c2c9d03e82..480a0737dc 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 @@ -1,7 +1,10 @@ from unittest import mock from uuid import uuid4 -from prowler.providers.azure.services.storage.storage_service import Account +from prowler.providers.azure.services.storage.storage_service import ( + Account, + NetworkRuleSet, +) from tests.providers.azure.azure_fixtures import ( AZURE_SUBSCRIPTION_ID, set_mocked_azure_provider, @@ -41,16 +44,18 @@ class Test_storage_key_rotation_90_dayss: Account( id=storage_account_id, name=storage_account_name, - resouce_group_name=None, + resouce_group_name="rg", enable_https_traffic_only=False, infrastructure_encryption=False, - allow_blob_public_access=None, - network_rule_set=None, + allow_blob_public_access=False, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), + key_expiration_period_in_days="91", encryption_type="None", - minimum_tls_version="TLS1_1", - key_expiration_period_in_days=expiration_days, + minimum_tls_version="TLS1_2", + private_endpoint_connections=[], location="westeurope", - private_endpoint_connections=None, ) ] } @@ -92,16 +97,18 @@ class Test_storage_key_rotation_90_dayss: Account( id=storage_account_id, name=storage_account_name, - resouce_group_name=None, + resouce_group_name="rg", enable_https_traffic_only=False, infrastructure_encryption=False, - allow_blob_public_access=None, - network_rule_set=None, + allow_blob_public_access=False, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), + key_expiration_period_in_days=90, encryption_type="None", minimum_tls_version="TLS1_2", - key_expiration_period_in_days=expiration_days, + private_endpoint_connections=[], location="westeurope", - private_endpoint_connections=None, ) ] } @@ -142,16 +149,18 @@ class Test_storage_key_rotation_90_dayss: Account( id=storage_account_id, name=storage_account_name, - resouce_group_name=None, + resouce_group_name="rg", enable_https_traffic_only=False, infrastructure_encryption=False, - allow_blob_public_access=None, - network_rule_set=None, + allow_blob_public_access=False, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), + key_expiration_period_in_days=None, encryption_type="None", minimum_tls_version="TLS1_2", - key_expiration_period_in_days=None, + private_endpoint_connections=[], location="westeurope", - private_endpoint_connections=None, ) ] } 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 d7a4f469e2..cd3c8ab408 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 @@ -1,7 +1,10 @@ from unittest import mock from uuid import uuid4 -from prowler.providers.azure.services.storage.storage_service import Account +from prowler.providers.azure.services.storage.storage_service import ( + Account, + NetworkRuleSet, +) from tests.providers.azure.azure_fixtures import ( AZURE_SUBSCRIPTION_ID, set_mocked_azure_provider, @@ -40,16 +43,18 @@ class Test_storage_secure_transfer_required_is_enabled: Account( id=storage_account_id, name=storage_account_name, - resouce_group_name=None, + resouce_group_name="rg", enable_https_traffic_only=False, infrastructure_encryption=False, - allow_blob_public_access=None, - network_rule_set=None, + allow_blob_public_access=False, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), encryption_type="None", - minimum_tls_version="TLS1_1", + minimum_tls_version="TLS1_2", key_expiration_period_in_days=None, location="westeurope", - private_endpoint_connections=None, + private_endpoint_connections=[], ) ] } @@ -90,16 +95,18 @@ class Test_storage_secure_transfer_required_is_enabled: Account( id=storage_account_id, name=storage_account_name, - resouce_group_name=None, + resouce_group_name="rg", enable_https_traffic_only=True, infrastructure_encryption=True, - allow_blob_public_access=None, - network_rule_set=None, + allow_blob_public_access=False, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), encryption_type="None", - minimum_tls_version="TLS1_1", + minimum_tls_version="TLS1_2", key_expiration_period_in_days=None, location="westeurope", - private_endpoint_connections=None, + private_endpoint_connections=[], ) ] } diff --git a/tests/providers/azure/services/storage/storage_service_test.py b/tests/providers/azure/services/storage/storage_service_test.py index 2ac6031322..6eb21052a2 100644 --- a/tests/providers/azure/services/storage/storage_service_test.py +++ b/tests/providers/azure/services/storage/storage_service_test.py @@ -3,8 +3,11 @@ from unittest.mock import patch from prowler.providers.azure.services.storage.storage_service import ( Account, BlobProperties, - FileShare, + DeleteRetentionPolicy, + FileServiceProperties, + NetworkRuleSet, ReplicationSettings, + SMBProtocolSettings, Storage, ) from tests.providers.azure.azure_fixtures import ( @@ -19,33 +22,41 @@ def mock_storage_get_storage_accounts(_): name="name", type="type", default_service_version=None, - container_delete_retention_policy=None, + container_delete_retention_policy=DeleteRetentionPolicy(enabled=True, days=7), + ) + retention_policy = DeleteRetentionPolicy(enabled=True, days=7) + file_service_properties = FileServiceProperties( + id="id", + name="name", + type="type", + share_delete_retention_policy=retention_policy, + smb_protocol_settings=SMBProtocolSettings( + channel_encryption=[], supported_versions=[] + ), ) - file_shares = [ - FileShare(id="fs1", name="share1", soft_delete_enabled=True, retention_days=7), - FileShare(id="fs2", name="share2", soft_delete_enabled=False, retention_days=0), - ] return { AZURE_SUBSCRIPTION_ID: [ Account( id="id", name="name", - resouce_group_name=None, + resouce_group_name="rg", enable_https_traffic_only=False, infrastructure_encryption=False, - allow_blob_public_access=None, - network_rule_set=None, + allow_blob_public_access=False, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), encryption_type="None", - minimum_tls_version=None, + minimum_tls_version="TLS1_2", key_expiration_period_in_days=None, - private_endpoint_connections=None, + private_endpoint_connections=[], location="westeurope", blob_properties=blob_properties, default_to_entra_authorization=True, replication_settings=ReplicationSettings.STANDARD_LRS, allow_cross_tenant_replication=True, allow_shared_key_access=True, - file_shares=file_shares, + file_service_properties=file_service_properties, ) ] } @@ -73,7 +84,7 @@ class Test_Storage_Service: assert storage.storage_accounts[AZURE_SUBSCRIPTION_ID][0].name == "name" assert ( storage.storage_accounts[AZURE_SUBSCRIPTION_ID][0].resouce_group_name - is None + == "rg" ) assert ( storage.storage_accounts[AZURE_SUBSCRIPTION_ID][0].enable_https_traffic_only @@ -85,10 +96,21 @@ class Test_Storage_Service: ) assert ( storage.storage_accounts[AZURE_SUBSCRIPTION_ID][0].allow_blob_public_access - is None + is False ) assert ( - storage.storage_accounts[AZURE_SUBSCRIPTION_ID][0].network_rule_set is None + storage.storage_accounts[AZURE_SUBSCRIPTION_ID][0].network_rule_set + is not None + ) + assert ( + storage.storage_accounts[AZURE_SUBSCRIPTION_ID][0].network_rule_set.bypass + == "AzureServices" + ) + assert ( + storage.storage_accounts[AZURE_SUBSCRIPTION_ID][ + 0 + ].network_rule_set.default_action + == "Allow" ) assert ( storage.storage_accounts[AZURE_SUBSCRIPTION_ID][0].encryption_type == "None" @@ -98,7 +120,7 @@ class Test_Storage_Service: ) assert ( storage.storage_accounts[AZURE_SUBSCRIPTION_ID][0].minimum_tls_version - is None + == "TLS1_2" ) assert ( storage.storage_accounts[AZURE_SUBSCRIPTION_ID][ @@ -110,7 +132,7 @@ class Test_Storage_Service: storage.storage_accounts[AZURE_SUBSCRIPTION_ID][ 0 ].private_endpoint_connections - is None + == [] ) assert storage.storage_accounts[AZURE_SUBSCRIPTION_ID][ 0 @@ -119,7 +141,9 @@ class Test_Storage_Service: name="name", type="type", default_service_version=None, - container_delete_retention_policy=None, + container_delete_retention_policy=DeleteRetentionPolicy( + enabled=True, days=7 + ), ) assert storage.storage_accounts[AZURE_SUBSCRIPTION_ID][ 0 @@ -169,17 +193,35 @@ class Test_Storage_Service: storage.storage_accounts[AZURE_SUBSCRIPTION_ID][ 0 ].blob_properties.container_delete_retention_policy - is None + is not None + ) + assert ( + storage.storage_accounts[AZURE_SUBSCRIPTION_ID][ + 0 + ].blob_properties.container_delete_retention_policy.enabled + is True + ) + assert ( + storage.storage_accounts[AZURE_SUBSCRIPTION_ID][ + 0 + ].blob_properties.container_delete_retention_policy.days + == 7 ) - def test_get_file_shares_properties(self): + def test_get_file_service_properties(self): storage = Storage(set_mocked_azure_provider()) account = storage.storage_accounts[AZURE_SUBSCRIPTION_ID][0] - assert hasattr(account, "file_shares") - assert len(account.file_shares) == 2 - assert account.file_shares[0].name == "share1" - assert account.file_shares[0].soft_delete_enabled is True - assert account.file_shares[0].retention_days == 7 - assert account.file_shares[1].name == "share2" - assert account.file_shares[1].soft_delete_enabled is False - assert account.file_shares[1].retention_days == 0 + assert hasattr(account, "file_service_properties") + assert ( + account.file_service_properties.share_delete_retention_policy.enabled + is True + ) + assert account.file_service_properties.share_delete_retention_policy.days == 7 + assert ( + account.file_service_properties.smb_protocol_settings.channel_encryption + == [] + ) + assert ( + account.file_service_properties.smb_protocol_settings.supported_versions + == [] + ) 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 new file mode 100644 index 0000000000..db82c09df4 --- /dev/null +++ b/tests/providers/azure/services/storage/storage_smb_channel_encryption_with_secure_algorithm/storage_smb_channel_encryption_with_secure_algorithm_test.py @@ -0,0 +1,248 @@ +from unittest import mock +from uuid import uuid4 + +from prowler.providers.azure.services.storage.storage_service import ( + Account, + DeleteRetentionPolicy, + FileServiceProperties, + NetworkRuleSet, + SMBProtocolSettings, +) +from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_ID, + set_mocked_azure_provider, +) + + +class Test_storage_smb_channel_encryption_with_secure_algorithm: + def test_no_storage_accounts(self): + storage_client = mock.MagicMock() + 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_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) == 0 + + def test_no_file_service_properties(self): + storage_account_id = str(uuid4()) + storage_account_name = "Test Storage Account" + storage_client = mock.MagicMock() + 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=None, + ) + ] + } + 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) == 0 + + def test_no_smb_protocol_settings(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=[], supported_versions=[] + ), + ) + storage_client = mock.MagicMock() + 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_ID} does not have SMB channel encryption enabled for file shares." + ) + + def test_not_recommended_encryption(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"], supported_versions=[] + ), + ) + storage_client = mock.MagicMock() + 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_ID} does not have SMB channel encryption with a secure algorithm for file shares since it supports AES-128-GCM." + ) + + def test_recommended_encryption(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-256-GCM"], supported_versions=[] + ), + ) + storage_client = mock.MagicMock() + 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_ID} has a secure algorithm for SMB channel encryption (AES-256-GCM) enabled for file shares since it supports 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 new file mode 100644 index 0000000000..4194c7ae55 --- /dev/null +++ b/tests/providers/azure/services/storage/storage_smb_protocol_version_is_latest/storage_smb_protocol_version_is_latest_test.py @@ -0,0 +1,303 @@ +from unittest import mock +from uuid import uuid4 + +from prowler.providers.azure.services.storage.storage_service import ( + Account, + DeleteRetentionPolicy, + FileServiceProperties, + NetworkRuleSet, + SMBProtocolSettings, +) +from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_ID, + set_mocked_azure_provider, +) + + +class Test_storage_smb_protocol_version_is_latest: + def test_no_storage_accounts(self): + storage_client = mock.MagicMock() + 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_smb_protocol_version_is_latest.storage_smb_protocol_version_is_latest.storage_client", + new=storage_client, + ), + ): + from prowler.providers.azure.services.storage.storage_smb_protocol_version_is_latest.storage_smb_protocol_version_is_latest import ( + storage_smb_protocol_version_is_latest, + ) + + check = storage_smb_protocol_version_is_latest() + result = check.execute() + assert len(result) == 0 + + def test_no_file_service_properties(self): + storage_account_id = str(uuid4()) + storage_account_name = "Test Storage Account" + storage_client = mock.MagicMock() + storage_client.storage_accounts = { + AZURE_SUBSCRIPTION_ID: [ + Account( + id=storage_account_id, + name=storage_account_name, + resouce_group_name="rg", + enable_https_traffic_only=True, + infrastructure_encryption=True, + allow_blob_public_access=False, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), + encryption_type="type", + minimum_tls_version="TLS1_2", + private_endpoint_connections=[], + key_expiration_period_in_days=None, + location="eastus", + file_service_properties=None, + ) + ] + } + 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_protocol_version_is_latest.storage_smb_protocol_version_is_latest.storage_client", + new=storage_client, + ), + ): + from prowler.providers.azure.services.storage.storage_smb_protocol_version_is_latest.storage_smb_protocol_version_is_latest import ( + storage_smb_protocol_version_is_latest, + ) + + check = storage_smb_protocol_version_is_latest() + result = check.execute() + assert len(result) == 0 + + def test_only_latest_smb_protocol_version(self): + storage_account_id = str(uuid4()) + storage_account_name = "Test Storage Account" + file_service_properties = FileServiceProperties( + id=storage_account_id, + name="default", + type="type", + share_delete_retention_policy=DeleteRetentionPolicy(enabled=True, days=7), + smb_protocol_settings=SMBProtocolSettings( + channel_encryption=[], supported_versions=["SMB3.1.1"] + ), + ) + storage_client = mock.MagicMock() + storage_client.storage_accounts = { + AZURE_SUBSCRIPTION_ID: [ + Account( + id=storage_account_id, + name=storage_account_name, + resouce_group_name="rg", + enable_https_traffic_only=True, + infrastructure_encryption=True, + allow_blob_public_access=False, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), + encryption_type="type", + minimum_tls_version="TLS1_2", + private_endpoint_connections=[], + key_expiration_period_in_days=None, + location="eastus", + 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_protocol_version_is_latest.storage_smb_protocol_version_is_latest.storage_client", + new=storage_client, + ), + ): + from prowler.providers.azure.services.storage.storage_smb_protocol_version_is_latest.storage_smb_protocol_version_is_latest import ( + storage_smb_protocol_version_is_latest, + ) + + check = storage_smb_protocol_version_is_latest() + result = check.execute() + 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." + in result[0].status_extended + ) + + def test_multiple_smb_protocol_versions(self): + storage_account_id = str(uuid4()) + storage_account_name = "Test Storage Account" + file_service_properties = FileServiceProperties( + id=storage_account_id, + name="default", + type="type", + share_delete_retention_policy=DeleteRetentionPolicy(enabled=True, days=7), + smb_protocol_settings=SMBProtocolSettings( + channel_encryption=[], supported_versions=["SMB2.1", "SMB3.1.1"] + ), + ) + storage_client = mock.MagicMock() + storage_client.storage_accounts = { + AZURE_SUBSCRIPTION_ID: [ + Account( + id=storage_account_id, + name=storage_account_name, + resouce_group_name="rg", + enable_https_traffic_only=True, + infrastructure_encryption=True, + allow_blob_public_access=False, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), + encryption_type="type", + minimum_tls_version="TLS1_2", + private_endpoint_connections=[], + key_expiration_period_in_days=None, + location="eastus", + 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_protocol_version_is_latest.storage_smb_protocol_version_is_latest.storage_client", + new=storage_client, + ), + ): + from prowler.providers.azure.services.storage.storage_smb_protocol_version_is_latest.storage_smb_protocol_version_is_latest import ( + storage_smb_protocol_version_is_latest, + ) + + check = storage_smb_protocol_version_is_latest() + result = check.execute() + 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." + in result[0].status_extended + ) + + def test_no_smb_protocol_versions(self): + storage_account_id = str(uuid4()) + storage_account_name = "Test Storage Account" + file_service_properties = FileServiceProperties( + id=storage_account_id, + name="default", + type="type", + share_delete_retention_policy=DeleteRetentionPolicy(enabled=True, days=7), + smb_protocol_settings=SMBProtocolSettings( + channel_encryption=[], supported_versions=[] + ), + ) + storage_client = mock.MagicMock() + storage_client.storage_accounts = { + AZURE_SUBSCRIPTION_ID: [ + Account( + id=storage_account_id, + name=storage_account_name, + resouce_group_name="rg", + enable_https_traffic_only=True, + infrastructure_encryption=True, + allow_blob_public_access=False, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), + encryption_type="type", + minimum_tls_version="TLS1_2", + private_endpoint_connections=[], + key_expiration_period_in_days=None, + location="eastus", + 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_protocol_version_is_latest.storage_smb_protocol_version_is_latest.storage_client", + new=storage_client, + ), + ): + from prowler.providers.azure.services.storage.storage_smb_protocol_version_is_latest.storage_smb_protocol_version_is_latest import ( + storage_smb_protocol_version_is_latest, + ) + + check = storage_smb_protocol_version_is_latest() + result = check.execute() + assert len(result) == 0 + + def test_multiple_required_versions_custom_config(self): + storage_account_id = str(uuid4()) + storage_account_name = "Test Storage Account" + file_service_properties = FileServiceProperties( + id=storage_account_id, + name="default", + type="type", + share_delete_retention_policy=DeleteRetentionPolicy(enabled=True, days=7), + smb_protocol_settings=SMBProtocolSettings( + channel_encryption=[], supported_versions=["SMB3.1.1", "SMB3.0"] + ), + ) + storage_client = mock.MagicMock() + storage_client.storage_accounts = { + AZURE_SUBSCRIPTION_ID: [ + Account( + id=storage_account_id, + name=storage_account_name, + resouce_group_name="rg", + enable_https_traffic_only=True, + infrastructure_encryption=True, + allow_blob_public_access=False, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), + encryption_type="type", + minimum_tls_version="TLS1_2", + private_endpoint_connections=[], + key_expiration_period_in_days=None, + location="eastus", + 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_protocol_version_is_latest.storage_smb_protocol_version_is_latest.storage_client", + new=storage_client, + ), + ): + from prowler.providers.azure.services.storage.storage_smb_protocol_version_is_latest.storage_smb_protocol_version_is_latest import ( + storage_smb_protocol_version_is_latest, + ) + + check = storage_smb_protocol_version_is_latest() + result = check.execute() + 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." + 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 new file mode 100644 index 0000000000..23394addca --- /dev/null +++ b/tests/providers/azure/services/vm/vm_backup_enabled/vm_backup_enabled_test.py @@ -0,0 +1,301 @@ +from unittest import mock +from uuid import uuid4 + +from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_ID, + set_mocked_azure_provider, +) + + +class Test_vm_backup_enabled: + def test_vm_backup_enabled_no_subscriptions(self): + vm_client = mock.MagicMock + recovery_client = mock.MagicMock + + vm_client.virtual_machines = {} + recovery_client.vaults = {} + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_backup_enabled.vm_backup_enabled.vm_client", + new=vm_client, + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_backup_enabled.vm_backup_enabled.recovery_client", + new=recovery_client, + ), + ): + from prowler.providers.azure.services.vm.vm_backup_enabled.vm_backup_enabled import ( + vm_backup_enabled, + ) + + check = vm_backup_enabled() + result = check.execute() + assert len(result) == 0 + + def test_no_vms(self): + mock_vm_client = mock.MagicMock() + mock_vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {}} + mock_recovery_client = mock.MagicMock() + mock_recovery_client.vaults = {AZURE_SUBSCRIPTION_ID: {}} + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_backup_enabled.vm_backup_enabled.vm_client", + new=mock_vm_client, + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_backup_enabled.vm_backup_enabled.recovery_client", + new=mock_recovery_client, + ), + ): + from prowler.providers.azure.services.vm.vm_backup_enabled.vm_backup_enabled import ( + vm_backup_enabled, + ) + + check = vm_backup_enabled() + result = check.execute() + assert len(result) == 0 + + def test_vm_protected_by_backup(self): + vm_id = str(uuid4()) + vm_name = "VMTest" + vault_id = str(uuid4()) + vault_name = "vault1" + mock_vm_client = mock.MagicMock() + mock_recovery_client = mock.MagicMock() + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_backup_enabled.vm_backup_enabled.vm_client", + new=mock_vm_client, + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_backup_enabled.vm_backup_enabled.recovery_client", + new=mock_recovery_client, + ), + ): + from azure.mgmt.recoveryservicesbackup.activestamp.models import ( + DataSourceType, + ) + + from prowler.providers.azure.services.recovery.recovery_service import ( + BackupItem, + BackupVault, + ) + from prowler.providers.azure.services.vm.vm_backup_enabled.vm_backup_enabled import ( + vm_backup_enabled, + ) + from prowler.providers.azure.services.vm.vm_service import ( + ManagedDiskParameters, + OSDisk, + StorageProfile, + VirtualMachine, + ) + + vm = VirtualMachine( + resource_id=vm_id, + resource_name=vm_name, + location="eastus", + security_profile=None, + extensions=[], + storage_profile=StorageProfile( + os_disk=OSDisk( + name="os_disk_name", + operating_system_type="Linux", + managed_disk=ManagedDiskParameters(id="managed_disk_id"), + ), + data_disks=[], + ), + ) + backup_item = BackupItem( + id=str(uuid4()), + name=f"someprefix;{vm_name}", + workload_type=DataSourceType.VM, + ) + vault = BackupVault( + id=vault_id, + name=vault_name, + location="eastus", + backup_protected_items={backup_item.id: backup_item}, + ) + mock_vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {vm_id: vm}} + mock_recovery_client.vaults = {AZURE_SUBSCRIPTION_ID: {vault_id: vault}} + check = vm_backup_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == vm_name + 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})." + ) + + def test_vm_not_protected_by_backup(self): + vm_id = str(uuid4()) + vm_name = "VMTest" + vault_id = str(uuid4()) + vault_name = "vault1" + mock_vm_client = mock.MagicMock() + mock_recovery_client = mock.MagicMock() + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_backup_enabled.vm_backup_enabled.vm_client", + new=mock_vm_client, + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_backup_enabled.vm_backup_enabled.recovery_client", + new=mock_recovery_client, + ), + ): + from azure.mgmt.recoveryservicesbackup.activestamp.models import ( + DataSourceType, + ) + + from prowler.providers.azure.services.recovery.recovery_service import ( + BackupItem, + BackupVault, + ) + from prowler.providers.azure.services.vm.vm_backup_enabled.vm_backup_enabled import ( + vm_backup_enabled, + ) + from prowler.providers.azure.services.vm.vm_service import ( + ManagedDiskParameters, + OSDisk, + StorageProfile, + VirtualMachine, + ) + + vm = VirtualMachine( + resource_id=vm_id, + resource_name=vm_name, + location="eastus", + security_profile=None, + extensions=[], + storage_profile=StorageProfile( + os_disk=OSDisk( + name="os_disk_name", + operating_system_type="Linux", + managed_disk=ManagedDiskParameters(id="managed_disk_id"), + ), + data_disks=[], + ), + ) + backup_item = BackupItem( + id=str(uuid4()), + name="someprefix;OtherVM", + workload_type=DataSourceType.VM, + ) + vault = BackupVault( + id=vault_id, + name=vault_name, + location="eastus", + backup_protected_items={backup_item.id: backup_item}, + ) + mock_vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {vm_id: vm}} + mock_recovery_client.vaults = {AZURE_SUBSCRIPTION_ID: {vault_id: vault}} + check = vm_backup_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == vm_name + 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." + ) + + def test_vm_protected_by_backup_non_vm_workload(self): + vm_id = str(uuid4()) + vm_name = "VMTest" + vault_id = str(uuid4()) + vault_name = "vault1" + mock_vm_client = mock.MagicMock() + mock_recovery_client = mock.MagicMock() + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_backup_enabled.vm_backup_enabled.vm_client", + new=mock_vm_client, + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_backup_enabled.vm_backup_enabled.recovery_client", + new=mock_recovery_client, + ), + ): + from azure.mgmt.recoveryservicesbackup.activestamp.models import ( + DataSourceType, + ) + + from prowler.providers.azure.services.recovery.recovery_service import ( + BackupItem, + BackupVault, + ) + from prowler.providers.azure.services.vm.vm_backup_enabled.vm_backup_enabled import ( + vm_backup_enabled, + ) + from prowler.providers.azure.services.vm.vm_service import ( + ManagedDiskParameters, + OSDisk, + StorageProfile, + VirtualMachine, + ) + + vm = VirtualMachine( + resource_id=vm_id, + resource_name=vm_name, + location="eastus", + security_profile=None, + extensions=[], + storage_profile=StorageProfile( + os_disk=OSDisk( + name="os_disk_name", + operating_system_type="Linux", + managed_disk=ManagedDiskParameters(id="managed_disk_id"), + ), + data_disks=[], + ), + ) + backup_item = BackupItem( + id=str(uuid4()), + name=f"someprefix;{vm_name}", + workload_type=DataSourceType.FILE_FOLDER, + ) + vault = BackupVault( + id=vault_id, + name=vault_name, + location="eastus", + backup_protected_items={backup_item.id: backup_item}, + ) + mock_vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {vm_id: vm}} + mock_recovery_client.vaults = {AZURE_SUBSCRIPTION_ID: {vault_id: vault}} + check = vm_backup_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == vm_name + 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." + ) 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 ccf6e8f175..1eb8da64c4 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 @@ -54,15 +54,15 @@ class Test_vm_ensure_attached_disks_encrypted_with_cmk: assert len(result) == 0 def test_vm_subscription_one_disk_attached_encrypt_pk(self): - disk_id = uuid4() - resource_id = uuid4() + disk_id = str(uuid4()) + resource_id = str(uuid4()) vm_client = mock.MagicMock vm_client.disks = { AZURE_SUBSCRIPTION_ID: { disk_id: Disk( resource_id=resource_id, resource_name="test-disk", - vms_attached=[uuid4()], + vms_attached=[str(uuid4())], encryption_type="EncryptionAtRestWithPlatformKey", location="location", ) @@ -97,15 +97,15 @@ class Test_vm_ensure_attached_disks_encrypted_with_cmk: ) def test_vm_subscription_one_disk_attached_encrypt_cmk(self): - disk_id = uuid4() - resource_id = uuid4() + disk_id = str(uuid4()) + resource_id = str(uuid4()) vm_client = mock.MagicMock vm_client.disks = { AZURE_SUBSCRIPTION_ID: { disk_id: Disk( resource_id=resource_id, resource_name="test-disk", - vms_attached=[uuid4()], + vms_attached=[str(uuid4())], encryption_type="EncryptionAtRestWithCustomerKey", location="location", ) @@ -140,24 +140,24 @@ class Test_vm_ensure_attached_disks_encrypted_with_cmk: ) def test_vm_subscription_two_disk_attached_encrypt_cmk_and_pk(self): - disk_id_1 = uuid4() - resource_id_1 = uuid4() - disk_id_2 = uuid4() - resource_id_2 = uuid4() + disk_id_1 = str(uuid4()) + resource_id_1 = str(uuid4()) + disk_id_2 = str(uuid4()) + resource_id_2 = str(uuid4()) vm_client = mock.MagicMock vm_client.disks = { AZURE_SUBSCRIPTION_ID: { disk_id_1: Disk( resource_id=resource_id_1, resource_name="test-disk", - vms_attached=[uuid4()], + vms_attached=[str(uuid4())], encryption_type="EncryptionAtRestWithPlatformKey", location="location", ), disk_id_2: Disk( resource_id=resource_id_2, resource_name="test-disk-2", - vms_attached=[uuid4(), uuid4()], + vms_attached=[str(uuid4()), str(uuid4())], encryption_type="EncryptionAtRestWithCustomerKey", location="location2", ), @@ -200,8 +200,8 @@ class Test_vm_ensure_attached_disks_encrypted_with_cmk: ) def test_vm_unattached_disk_encrypt_cmk(self): - disk_id = uuid4() - resource_id = uuid4() + disk_id = str(uuid4()) + resource_id = str(uuid4()) vm_client = mock.MagicMock vm_client.disks = { AZURE_SUBSCRIPTION_ID: { @@ -209,8 +209,8 @@ class Test_vm_ensure_attached_disks_encrypted_with_cmk: resource_id=resource_id, resource_name="test-disk", vms_attached=[], - location="location", encryption_type="EncryptionAtRestWithCustomerKey", + location="location", ) } } 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 44ec78b4ed..78d7920666 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 @@ -54,8 +54,8 @@ class Test_vm_ensure_unattached_disks_encrypted_with_cmk: assert len(result) == 0 def test_vm_one_unattached_disk_encrypt_pk(self): - disk_id = uuid4() - resource_id = uuid4() + disk_id = str(uuid4()) + resource_id = str(uuid4()) vm_client = mock.MagicMock vm_client.disks = { AZURE_SUBSCRIPTION_ID: { @@ -97,8 +97,8 @@ class Test_vm_ensure_unattached_disks_encrypted_with_cmk: ) def test_vm_one_unattached_disk_encrypt_cmk(self): - disk_id = uuid4() - resource_id = uuid4() + disk_id = str(uuid4()) + resource_id = str(uuid4()) vm_client = mock.MagicMock vm_client.disks = { AZURE_SUBSCRIPTION_ID: { @@ -140,10 +140,10 @@ class Test_vm_ensure_unattached_disks_encrypted_with_cmk: ) def test_vm_subscription_two_unattached_disk_encrypt_cmk_and_pk(self): - disk_id_1 = uuid4() - resource_id_1 = uuid4() - disk_id_2 = uuid4() - resource_id_2 = uuid4() + disk_id_1 = str(uuid4()) + resource_id_1 = str(uuid4()) + disk_id_2 = str(uuid4()) + resource_id_2 = str(uuid4()) vm_client = mock.MagicMock vm_client.disks = { AZURE_SUBSCRIPTION_ID: { @@ -200,17 +200,17 @@ class Test_vm_ensure_unattached_disks_encrypted_with_cmk: ) def test_vm_attached_disk_encrypt_cmk(self): - disk_id = uuid4() - resource_id = uuid4() + disk_id = str(uuid4()) + resource_id = str(uuid4()) vm_client = mock.MagicMock vm_client.disks = { AZURE_SUBSCRIPTION_ID: { disk_id: Disk( resource_id=resource_id, resource_name="test-disk", - location="location", - vms_attached=[uuid4()], + vms_attached=[str(uuid4())], encryption_type="EncryptionAtRestWithCustomerKey", + location="location", ) } } 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 new file mode 100644 index 0000000000..035ec5db3b --- /dev/null +++ b/tests/providers/azure/services/vm/vm_ensure_using_approved_images/vm_ensure_using_approved_images_test.py @@ -0,0 +1,165 @@ +from unittest import mock +from uuid import uuid4 + +from prowler.providers.azure.services.vm.vm_service import VirtualMachine +from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_ID, + set_mocked_azure_provider, +) + + +class Test_vm_ensure_using_approved_images: + def test_no_subscriptions(self): + vm_client = mock.MagicMock() + vm_client.virtual_machines = {} + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_ensure_using_approved_images.vm_ensure_using_approved_images.vm_client", + new=vm_client, + ), + ): + from prowler.providers.azure.services.vm.vm_ensure_using_approved_images.vm_ensure_using_approved_images import ( + vm_ensure_using_approved_images, + ) + + check = vm_ensure_using_approved_images() + result = check.execute() + assert len(result) == 0 + + def test_empty_vms_in_subscription(self): + vm_client = mock.MagicMock() + vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {}} + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_ensure_using_approved_images.vm_ensure_using_approved_images.vm_client", + new=vm_client, + ), + ): + from prowler.providers.azure.services.vm.vm_ensure_using_approved_images.vm_ensure_using_approved_images import ( + vm_ensure_using_approved_images, + ) + + check = vm_ensure_using_approved_images() + result = check.execute() + assert len(result) == 0 + + def test_vm_with_approved_image(self): + vm_id = str(uuid4()) + approved_image_id = f"/subscriptions/{AZURE_SUBSCRIPTION_ID}/resourceGroups/rg/providers/Microsoft.Compute/images/custom-image" + vm = VirtualMachine( + resource_id=vm_id, + resource_name="VMTestApproved", + location="westeurope", + security_profile=None, + extensions=[], + storage_profile=None, + image_reference=approved_image_id, + ) + vm_client = mock.MagicMock() + vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {vm_id: vm}} + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_ensure_using_approved_images.vm_ensure_using_approved_images.vm_client", + new=vm_client, + ), + ): + from prowler.providers.azure.services.vm.vm_ensure_using_approved_images.vm_ensure_using_approved_images import ( + vm_ensure_using_approved_images, + ) + + check = vm_ensure_using_approved_images() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + 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." + assert result[0].status_extended == expected_status_extended + + def test_vm_with_not_approved_image(self): + vm_id = str(uuid4()) + not_approved_image_id = "/subscriptions/other/resourceGroups/rg/providers/Microsoft.Compute/otherResource/other-image" + vm = VirtualMachine( + resource_id=vm_id, + resource_name="VMTestNotApproved", + location="westeurope", + security_profile=None, + extensions=[], + storage_profile=None, + image_reference=not_approved_image_id, + ) + vm_client = mock.MagicMock() + vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {vm_id: vm}} + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_ensure_using_approved_images.vm_ensure_using_approved_images.vm_client", + new=vm_client, + ), + ): + from prowler.providers.azure.services.vm.vm_ensure_using_approved_images.vm_ensure_using_approved_images import ( + vm_ensure_using_approved_images, + ) + + check = vm_ensure_using_approved_images() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + 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." + assert result[0].status_extended == expected_status_extended + + def test_vm_with_missing_image_reference(self): + vm_id = str(uuid4()) + vm = VirtualMachine( + resource_id=vm_id, + resource_name="VMTestNoImageRef", + location="westeurope", + security_profile=None, + extensions=[], + storage_profile=None, + image_reference=None, + ) + vm_client = mock.MagicMock() + vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {vm_id: vm}} + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_ensure_using_approved_images.vm_ensure_using_approved_images.vm_client", + new=vm_client, + ), + ): + from prowler.providers.azure.services.vm.vm_ensure_using_approved_images.vm_ensure_using_approved_images import ( + vm_ensure_using_approved_images, + ) + + check = vm_ensure_using_approved_images() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + 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." + 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 dbc04dd53f..3c494d861c 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 @@ -1,7 +1,15 @@ from unittest import mock from uuid import uuid4 -from prowler.providers.azure.services.vm.vm_service import VirtualMachine +from prowler.providers.azure.services.vm.vm_service import ( + DataDisk, + ManagedDiskParameters, + OSDisk, + SecurityProfile, + StorageProfile, + UefiSettings, + VirtualMachine, +) from tests.providers.azure.azure_fixtures import ( AZURE_SUBSCRIPTION_ID, set_mocked_azure_provider, @@ -62,21 +70,23 @@ class Test_vm_ensure_using_managed_disks: resource_id=vm_id, resource_name="VMTest", location="location", - security_profile=mock.MagicMock( + security_profile=SecurityProfile( security_type="TrustedLaunch", - uefi_settings=mock.MagicMock( + uefi_settings=UefiSettings( secure_boot_enabled=True, v_tpm_enabled=True, ), ), extensions=[], - storage_profile=mock.MagicMock( - os_disk=mock.MagicMock( - create_option="FromImage", - managed_disk=mock.MagicMock(id="managed_disk_id"), + storage_profile=StorageProfile( + os_disk=OSDisk( + name="os_disk_name", + operating_system_type="Linux", + managed_disk=ManagedDiskParameters(id="managed_disk_id"), ), data_disks=[], ), + linux_configuration=None, ), } } @@ -117,21 +127,23 @@ class Test_vm_ensure_using_managed_disks: resource_id=vm_id, resource_name="VMTest", location="location", - security_profile=mock.MagicMock( + security_profile=SecurityProfile( security_type="TrustedLaunch", - uefi_settings=mock.MagicMock( + uefi_settings=UefiSettings( secure_boot_enabled=True, v_tpm_enabled=True, ), ), extensions=[], - storage_profile=mock.MagicMock( - os_disk=mock.MagicMock( - create_option="FromImage", + storage_profile=StorageProfile( + os_disk=OSDisk( + name="os_disk_name", + operating_system_type="Linux", managed_disk=None, ), data_disks=[], ), + linux_configuration=None, ) } } @@ -172,21 +184,25 @@ class Test_vm_ensure_using_managed_disks: resource_id=vm_id, resource_name="VMTest", location="location", - security_profile=mock.MagicMock( + security_profile=SecurityProfile( security_type="TrustedLaunch", - uefi_settings=mock.MagicMock( + uefi_settings=UefiSettings( secure_boot_enabled=True, v_tpm_enabled=True, ), ), extensions=[], - storage_profile=mock.MagicMock( - os_disk=mock.MagicMock( - create_option="FromImage", - managed_disk=mock.MagicMock(id="managed_disk_id"), + storage_profile=StorageProfile( + os_disk=OSDisk( + name="os_disk_name", + operating_system_type="Linux", + managed_disk=ManagedDiskParameters(id="managed_disk_id"), ), - data_disks=[mock.MagicMock(managed_disk=None)], + data_disks=[ + DataDisk(lun=0, name="data_disk_1", managed_disk=None) + ], ), + linux_configuration=None, ) } } 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 new file mode 100644 index 0000000000..5d400ac7bf --- /dev/null +++ b/tests/providers/azure/services/vm/vm_linux_enforce_ssh_authentication/vm_linux_enforce_ssh_authentication_test.py @@ -0,0 +1,173 @@ +from unittest import mock +from uuid import uuid4 + +from prowler.providers.azure.services.vm.vm_service import ( + LinuxConfiguration, + VirtualMachine, +) +from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_ID, + set_mocked_azure_provider, +) + + +class Test_vm_linux_enforce_ssh_authentication: + def test_no_subscriptions(self): + vm_client = mock.MagicMock + vm_client.virtual_machines = {} + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_linux_enforce_ssh_authentication.vm_linux_enforce_ssh_authentication.vm_client", + new=vm_client, + ), + ): + from prowler.providers.azure.services.vm.vm_linux_enforce_ssh_authentication.vm_linux_enforce_ssh_authentication import ( + vm_linux_enforce_ssh_authentication, + ) + + check = vm_linux_enforce_ssh_authentication() + result = check.execute() + assert len(result) == 0 + + def test_empty_subscription(self): + vm_client = mock.MagicMock + vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {}} + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_linux_enforce_ssh_authentication.vm_linux_enforce_ssh_authentication.vm_client", + new=vm_client, + ), + ): + from prowler.providers.azure.services.vm.vm_linux_enforce_ssh_authentication.vm_linux_enforce_ssh_authentication import ( + vm_linux_enforce_ssh_authentication, + ) + + check = vm_linux_enforce_ssh_authentication() + result = check.execute() + assert len(result) == 0 + + def test_linux_vm_password_auth_disabled(self): + vm_id = str(uuid4()) + vm_client = mock.MagicMock + vm_client.virtual_machines = { + AZURE_SUBSCRIPTION_ID: { + vm_id: VirtualMachine( + resource_id=vm_id, + resource_name="LinuxVM", + location="westeurope", + security_profile=None, + extensions=[], + storage_profile=None, + linux_configuration=LinuxConfiguration( + disable_password_authentication=True + ), + ) + } + } + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_linux_enforce_ssh_authentication.vm_linux_enforce_ssh_authentication.vm_client", + new=vm_client, + ), + ): + from prowler.providers.azure.services.vm.vm_linux_enforce_ssh_authentication.vm_linux_enforce_ssh_authentication import ( + vm_linux_enforce_ssh_authentication, + ) + + check = vm_linux_enforce_ssh_authentication() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == "LinuxVM" + assert result[0].resource_id == vm_id + assert "password authentication disabled" in result[0].status_extended + + def test_linux_vm_password_auth_enabled(self): + vm_id = str(uuid4()) + vm_client = mock.MagicMock + vm_client.virtual_machines = { + AZURE_SUBSCRIPTION_ID: { + vm_id: VirtualMachine( + resource_id=vm_id, + resource_name="LinuxVM", + location="westeurope", + security_profile=None, + extensions=[], + storage_profile=None, + linux_configuration=LinuxConfiguration( + disable_password_authentication=False + ), + ) + } + } + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_linux_enforce_ssh_authentication.vm_linux_enforce_ssh_authentication.vm_client", + new=vm_client, + ), + ): + from prowler.providers.azure.services.vm.vm_linux_enforce_ssh_authentication.vm_linux_enforce_ssh_authentication import ( + vm_linux_enforce_ssh_authentication, + ) + + check = vm_linux_enforce_ssh_authentication() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == "LinuxVM" + assert result[0].resource_id == vm_id + assert "password authentication enabled" in result[0].status_extended + + def test_non_linux_vm(self): + vm_id = str(uuid4()) + vm_client = mock.MagicMock + vm_client.virtual_machines = { + AZURE_SUBSCRIPTION_ID: { + vm_id: VirtualMachine( + resource_id=vm_id, + resource_name="WindowsVM", + location="westeurope", + security_profile=None, + extensions=[], + storage_profile=None, + linux_configuration=None, # Not a Linux VM + ) + } + } + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_linux_enforce_ssh_authentication.vm_linux_enforce_ssh_authentication.vm_client", + new=vm_client, + ), + ): + from prowler.providers.azure.services.vm.vm_linux_enforce_ssh_authentication.vm_linux_enforce_ssh_authentication import ( + vm_linux_enforce_ssh_authentication, + ) + + check = vm_linux_enforce_ssh_authentication() + result = check.execute() + assert len(result) == 0 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 new file mode 100644 index 0000000000..d9d0717f4e --- /dev/null +++ b/tests/providers/azure/services/vm/vm_scaleset_associated_with_load_balancer/vm_scaleset_associated_with_load_balancer_test.py @@ -0,0 +1,216 @@ +from unittest import mock +from uuid import uuid4 + +from prowler.providers.azure.services.vm.vm_service import VirtualMachineScaleSet +from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_ID, + set_mocked_azure_provider, +) + + +class Test_vm_scaleset_associated_with_load_balancer: + def test_no_subscriptions(self): + vm_scale_sets = {} + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_client.vm_client.vm_scale_sets", + new=vm_scale_sets, + ), + ): + from prowler.providers.azure.services.vm.vm_scaleset_associated_with_load_balancer.vm_scaleset_associated_with_load_balancer import ( + vm_scaleset_associated_with_load_balancer, + ) + + check = vm_scaleset_associated_with_load_balancer() + result = check.execute() + assert len(result) == 0 + + def test_empty_scale_sets(self): + vm_scale_sets = {AZURE_SUBSCRIPTION_ID: {}} + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_client.vm_client.vm_scale_sets", + new=vm_scale_sets, + ), + ): + from prowler.providers.azure.services.vm.vm_scaleset_associated_with_load_balancer.vm_scaleset_associated_with_load_balancer import ( + vm_scaleset_associated_with_load_balancer, + ) + + check = vm_scaleset_associated_with_load_balancer() + result = check.execute() + assert len(result) == 0 + + def test_compliant_scale_set(self): + vmss_id = str(uuid4()) + backend_pool_id = f"/subscriptions/{AZURE_SUBSCRIPTION_ID}/resourceGroups/rg/providers/Microsoft.Network/loadBalancers/lb/backendAddressPools/bepool" + vm_scale_sets = { + AZURE_SUBSCRIPTION_ID: { + vmss_id: VirtualMachineScaleSet( + resource_id=vmss_id, + resource_name="compliant-vmss", + location="eastus", + load_balancer_backend_pools=[backend_pool_id], + ) + } + } + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_client.vm_client.vm_scale_sets", + new=vm_scale_sets, + ), + ): + from prowler.providers.azure.services.vm.vm_scaleset_associated_with_load_balancer.vm_scaleset_associated_with_load_balancer import ( + vm_scaleset_associated_with_load_balancer, + ) + + check = vm_scaleset_associated_with_load_balancer() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].resource_id == vmss_id + 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"is associated with load balancer backend pool(s): bepool." + ) + assert result[0].status_extended == expected_status_extended + + def test_noncompliant_scale_set(self): + vmss_id = str(uuid4()) + vm_scale_sets = { + AZURE_SUBSCRIPTION_ID: { + vmss_id: VirtualMachineScaleSet( + resource_id=vmss_id, + resource_name="noncompliant-vmss", + location="westeurope", + load_balancer_backend_pools=[], + ) + } + } + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_client.vm_client.vm_scale_sets", + new=vm_scale_sets, + ), + ): + from prowler.providers.azure.services.vm.vm_scaleset_associated_with_load_balancer.vm_scaleset_associated_with_load_balancer import ( + vm_scaleset_associated_with_load_balancer, + ) + + check = vm_scaleset_associated_with_load_balancer() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].resource_id == vmss_id + 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"is not associated with any load balancer backend pool." + ) + assert result[0].status_extended == expected_status_extended + + def test_multiple_scale_sets(self): + compliant_id = str(uuid4()) + noncompliant_id = str(uuid4()) + backend_pool_id = f"/subscriptions/{AZURE_SUBSCRIPTION_ID}/resourceGroups/rg/providers/Microsoft.Network/loadBalancers/lb/backendAddressPools/bepool" + vm_scale_sets = { + AZURE_SUBSCRIPTION_ID: { + compliant_id: VirtualMachineScaleSet( + resource_id=compliant_id, + resource_name="compliant-vmss", + location="eastus", + load_balancer_backend_pools=[backend_pool_id], + ), + noncompliant_id: VirtualMachineScaleSet( + resource_id=noncompliant_id, + resource_name="noncompliant-vmss", + location="westeurope", + load_balancer_backend_pools=[], + ), + } + } + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_client.vm_client.vm_scale_sets", + new=vm_scale_sets, + ), + ): + from prowler.providers.azure.services.vm.vm_scaleset_associated_with_load_balancer.vm_scaleset_associated_with_load_balancer import ( + vm_scaleset_associated_with_load_balancer, + ) + + check = vm_scaleset_associated_with_load_balancer() + result = check.execute() + assert len(result) == 2 + for r in result: + if r.resource_name == "compliant-vmss": + expected_status_extended = ( + f"Scale set 'compliant-vmss' in subscription '{AZURE_SUBSCRIPTION_ID}' " + 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"is not associated with any load balancer backend pool." + ) + assert r.status == "FAIL" + assert r.status_extended == expected_status_extended + + def test_missing_attributes(self): + # Simulate a scale set with missing optional attributes + vmss_id = str(uuid4()) + vm_scale_sets = { + AZURE_SUBSCRIPTION_ID: { + vmss_id: VirtualMachineScaleSet( + resource_id=vmss_id, + resource_name="", + location="", + load_balancer_backend_pools=[], + ) + } + } + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_client.vm_client.vm_scale_sets", + new=vm_scale_sets, + ), + ): + from prowler.providers.azure.services.vm.vm_scaleset_associated_with_load_balancer.vm_scaleset_associated_with_load_balancer import ( + 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." + assert result[0].status == "FAIL" + assert result[0].status_extended == expected_status_extended diff --git a/tests/providers/azure/services/vm/vm_service_test.py b/tests/providers/azure/services/vm/vm_service_test.py index 6e0f53e2e5..6a3144a5c9 100644 --- a/tests/providers/azure/services/vm/vm_service_test.py +++ b/tests/providers/azure/services/vm/vm_service_test.py @@ -1,10 +1,14 @@ -from unittest import mock from unittest.mock import patch -from azure.mgmt.compute.models import ManagedDiskParameters, OSDisk, StorageProfile - from prowler.providers.azure.services.vm.vm_service import ( Disk, + LinuxConfiguration, + ManagedDiskParameters, + OperatingSystemType, + OSDisk, + SecurityProfile, + StorageProfile, + UefiSettings, VirtualMachine, VirtualMachines, ) @@ -21,9 +25,9 @@ def mock_vm_get_virtual_machines(_): resource_id="/subscriptions/resource_id", resource_name="VMTest", location="location", - security_profile=mock.MagicMock( + security_profile=SecurityProfile( security_type="TrustedLaunch", - uefi_settings=mock.MagicMock( + uefi_settings=UefiSettings( secure_boot_enabled=True, v_tpm_enabled=True, ), @@ -31,11 +35,13 @@ def mock_vm_get_virtual_machines(_): extensions=[], storage_profile=StorageProfile( os_disk=OSDisk( - create_option="FromImage", + name="os_disk_name", + operating_system_type=OperatingSystemType.LINUX, managed_disk=ManagedDiskParameters(id="managed_disk_id"), ), data_disks=[], ), + linux_configuration=None, ) } } @@ -49,19 +55,21 @@ def mock_vm_get_virtual_machines_with_none(_): resource_name="VMWithNoneValues", location="location", security_profile=None, - extensions=None, + extensions=[], storage_profile=None, + linux_configuration=None, ), "vm_id-2": VirtualMachine( resource_id="/subscriptions/resource_id2", resource_name="VMWithPartialNone", location="location", security_profile=None, - extensions=None, + extensions=[], storage_profile=StorageProfile( os_disk=None, - data_disks=None, + data_disks=[], ), + linux_configuration=None, ), } } @@ -81,6 +89,24 @@ def mock_vm_get_disks(_): } +def mock_vm_get_virtual_machines_with_linux(_): + return { + AZURE_SUBSCRIPTION_ID: { + "vm_id-linux": VirtualMachine( + resource_id="/subscriptions/resource_id_linux", + resource_name="LinuxVM", + location="location", + security_profile=None, + extensions=[], + storage_profile=None, + linux_configuration=LinuxConfiguration( + disable_password_authentication=True + ), + ) + } + } + + @patch( "prowler.providers.azure.services.vm.vm_service.VirtualMachines._get_virtual_machines", new=mock_vm_get_virtual_machines, @@ -180,5 +206,16 @@ class Test_VirtualMachines_NoneCases: virtual_machines = VirtualMachines(set_mocked_azure_provider()) vm_2 = virtual_machines.virtual_machines[AZURE_SUBSCRIPTION_ID]["vm_id-2"] assert vm_2.storage_profile.os_disk is None - assert vm_2.storage_profile.data_disks is None + assert vm_2.storage_profile.data_disks == [] assert vm_2.resource_name == "VMWithPartialNone" + + +@patch( + "prowler.providers.azure.services.vm.vm_service.VirtualMachines._get_virtual_machines", + new=mock_vm_get_virtual_machines_with_linux, +) +def test_virtual_machine_with_linux_configuration(): + virtual_machines = VirtualMachines(set_mocked_azure_provider()) + vm = virtual_machines.virtual_machines[AZURE_SUBSCRIPTION_ID]["vm_id-linux"] + assert vm.linux_configuration is not None + assert vm.linux_configuration.disable_password_authentication is True 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 05e96a5c1d..83ab63acce 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 @@ -1,6 +1,14 @@ from unittest import mock from uuid import uuid4 +from prowler.providers.azure.services.vm.vm_service import ( + ManagedDiskParameters, + OSDisk, + SecurityProfile, + StorageProfile, + UefiSettings, + VirtualMachine, +) from tests.providers.azure.azure_fixtures import ( AZURE_SUBSCRIPTION_ID, set_mocked_azure_provider, @@ -63,7 +71,6 @@ class Test_vm_trusted_launch_enabled: new=vm_client, ), ): - from prowler.providers.azure.services.vm.vm_service import VirtualMachine from prowler.providers.azure.services.vm.vm_trusted_launch_enabled.vm_trusted_launch_enabled import ( vm_trusted_launch_enabled, ) @@ -74,18 +81,21 @@ class Test_vm_trusted_launch_enabled: resource_id=vm_id, resource_name="VMTest", location="location", - security_profile=mock.MagicMock( + security_profile=SecurityProfile( security_type="TrustedLaunch", - uefi_settings=mock.MagicMock( + uefi_settings=UefiSettings( secure_boot_enabled=True, v_tpm_enabled=True, ), ), extensions=[], - storage_profile=mock.MagicMock( - os_disk=mock.MagicMock( - create_option="FromImage", - managed_disk=mock.MagicMock(id="managed_disk_id"), + storage_profile=StorageProfile( + os_disk=OSDisk( + name="os_disk_name", + operating_system_type="Linux", + managed_disk=ManagedDiskParameters( + id="managed_disk_id" + ), ), data_disks=[], ), @@ -117,7 +127,6 @@ class Test_vm_trusted_launch_enabled: new=vm_client, ), ): - from prowler.providers.azure.services.vm.vm_service import VirtualMachine from prowler.providers.azure.services.vm.vm_trusted_launch_enabled.vm_trusted_launch_enabled import ( vm_trusted_launch_enabled, ) @@ -128,18 +137,21 @@ class Test_vm_trusted_launch_enabled: resource_id=vm_id, resource_name="VMTest", location="location", - security_profile=mock.MagicMock( + security_profile=SecurityProfile( security_type="TrustedLaunch", - uefi_settings=mock.MagicMock( + uefi_settings=UefiSettings( secure_boot_enabled=False, v_tpm_enabled=False, ), ), extensions=[], - storage_profile=mock.MagicMock( - os_disk=mock.MagicMock( - create_option="FromImage", - managed_disk=mock.MagicMock(id="managed_disk_id"), + storage_profile=StorageProfile( + os_disk=OSDisk( + name="os_disk_name", + operating_system_type="Linux", + managed_disk=ManagedDiskParameters( + id="managed_disk_id" + ), ), data_disks=[], ), @@ -172,7 +184,6 @@ class Test_vm_trusted_launch_enabled: new=vm_client, ), ): - from prowler.providers.azure.services.vm.vm_service import VirtualMachine from prowler.providers.azure.services.vm.vm_trusted_launch_enabled.vm_trusted_launch_enabled import ( vm_trusted_launch_enabled, ) @@ -185,10 +196,13 @@ class Test_vm_trusted_launch_enabled: location="location", security_profile=None, extensions=[], - storage_profile=mock.MagicMock( - os_disk=mock.MagicMock( - create_option="FromImage", - managed_disk=mock.MagicMock(id="managed_disk_id"), + storage_profile=StorageProfile( + os_disk=OSDisk( + name="os_disk_name", + operating_system_type="Linux", + managed_disk=ManagedDiskParameters( + id="managed_disk_id" + ), ), data_disks=[], ), diff --git a/tests/providers/gcp/services/compute/compute_instance_block_project_wide_ssh_keys_disabled/compute_instance_block_project_wide_ssh_keys_disabled_test.py b/tests/providers/gcp/services/compute/compute_instance_block_project_wide_ssh_keys_disabled/compute_instance_block_project_wide_ssh_keys_disabled_test.py index 657e9a6489..8bc01c7eae 100644 --- a/tests/providers/gcp/services/compute/compute_instance_block_project_wide_ssh_keys_disabled/compute_instance_block_project_wide_ssh_keys_disabled_test.py +++ b/tests/providers/gcp/services/compute/compute_instance_block_project_wide_ssh_keys_disabled/compute_instance_block_project_wide_ssh_keys_disabled_test.py @@ -77,6 +77,55 @@ class Test_compute_instance_block_project_wide_ssh_keys_disabled: assert result[0].resource_id == instance.id assert result[0].location == "us-central1" + def test_one_compliant_instance_with_block_project_ssh_keys_true_uppercase(self): + from prowler.providers.gcp.services.compute.compute_service import Instance + + instance = Instance( + name="test", + id="1234567890", + zone="us-central1-a", + region="us-central1", + public_ip=True, + metadata={"items": [{"key": "block-project-ssh-keys", "value": "TRUE"}]}, + shielded_enabled_vtpm=True, + shielded_enabled_integrity_monitoring=True, + confidential_computing=True, + service_accounts=[], + ip_forward=False, + disks_encryption=[("disk1", False), ("disk2", False)], + project_id=GCP_PROJECT_ID, + ) + + compute_client = mock.MagicMock() + compute_client.project_ids = [GCP_PROJECT_ID] + compute_client.instances = [instance] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.compute.compute_instance_block_project_wide_ssh_keys_disabled.compute_instance_block_project_wide_ssh_keys_disabled.compute_client", + new=compute_client, + ), + ): + from prowler.providers.gcp.services.compute.compute_instance_block_project_wide_ssh_keys_disabled.compute_instance_block_project_wide_ssh_keys_disabled import ( + compute_instance_block_project_wide_ssh_keys_disabled, + ) + + check = compute_instance_block_project_wide_ssh_keys_disabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert search( + f"The VM Instance {instance.name} is not making use of common/shared project-wide SSH key", + result[0].status_extended, + ) + assert result[0].resource_id == instance.id + assert result[0].location == "us-central1" + def test_one_instance_without_metadata(self): from prowler.providers.gcp.services.compute.compute_service import Instance diff --git a/tests/providers/gcp/services/compute/compute_project_os_login_enabled/compute_project_os_login_enabled_test.py b/tests/providers/gcp/services/compute/compute_project_os_login_enabled/compute_project_os_login_enabled_test.py index abfd4ea361..0dcacf5513 100644 --- a/tests/providers/gcp/services/compute/compute_project_os_login_enabled/compute_project_os_login_enabled_test.py +++ b/tests/providers/gcp/services/compute/compute_project_os_login_enabled/compute_project_os_login_enabled_test.py @@ -128,3 +128,103 @@ class Test_compute_project_os_login_enabled: assert result[0].resource_name == "test" assert result[0].location == "global" assert result[0].project_id == GCP_PROJECT_ID + + def test_one_compliant_project_empty_project_name(self): + from prowler.providers.gcp.services.compute.compute_service import Project + + project = Project( + id=GCP_PROJECT_ID, + enable_oslogin=True, + ) + + compute_client = mock.MagicMock() + compute_client.project_ids = [GCP_PROJECT_ID] + compute_client.compute_projects = [project] + compute_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="", + labels={}, + lifecycle_state="ACTIVE", + ) + } + compute_client.region = "global" + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.compute.compute_project_os_login_enabled.compute_project_os_login_enabled.compute_client", + new=compute_client, + ), + ): + from prowler.providers.gcp.services.compute.compute_project_os_login_enabled.compute_project_os_login_enabled import ( + compute_project_os_login_enabled, + ) + + check = compute_project_os_login_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert search( + f"Project {project.id} has OS Login enabled", + result[0].status_extended, + ) + assert result[0].resource_id == project.id + assert result[0].resource_name == GCP_PROJECT_ID + assert result[0].location == "global" + assert result[0].project_id == GCP_PROJECT_ID + + def test_one_non_compliant_project_empty_project_name(self): + from prowler.providers.gcp.services.compute.compute_service import Project + + project = Project( + id=GCP_PROJECT_ID, + enable_oslogin=False, + ) + + compute_client = mock.MagicMock() + compute_client.project_ids = [GCP_PROJECT_ID] + compute_client.compute_projects = [project] + compute_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="", + labels={}, + lifecycle_state="ACTIVE", + ) + } + compute_client.region = "global" + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.compute.compute_project_os_login_enabled.compute_project_os_login_enabled.compute_client", + new=compute_client, + ), + ): + from prowler.providers.gcp.services.compute.compute_project_os_login_enabled.compute_project_os_login_enabled import ( + compute_project_os_login_enabled, + ) + + check = compute_project_os_login_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert search( + f"Project {project.id} does not have OS Login enabled", + result[0].status_extended, + ) + assert result[0].resource_id == project.id + assert result[0].resource_name == GCP_PROJECT_ID + assert result[0].location == "global" + assert result[0].project_id == GCP_PROJECT_ID diff --git a/tests/providers/gcp/services/iam/iam_audit_logs_enabled/iam_audit_logs_enabled_test.py b/tests/providers/gcp/services/iam/iam_audit_logs_enabled/iam_audit_logs_enabled_test.py index 1056b4f055..df1185cac3 100644 --- a/tests/providers/gcp/services/iam/iam_audit_logs_enabled/iam_audit_logs_enabled_test.py +++ b/tests/providers/gcp/services/iam/iam_audit_logs_enabled/iam_audit_logs_enabled_test.py @@ -129,3 +129,103 @@ class Test_iam_audit_logs_enabled: assert r.resource_name == "test" assert r.project_id == GCP_PROJECT_ID assert r.location == cloudresourcemanager_client.region + + def test_compliant_project_empty_project_name(self): + from prowler.providers.gcp.services.cloudresourcemanager.cloudresourcemanager_service import ( + Project, + ) + + project1 = Project(id=GCP_PROJECT_ID, audit_logging=True) + + cloudresourcemanager_client = mock.MagicMock() + cloudresourcemanager_client.project_ids = [GCP_PROJECT_ID] + cloudresourcemanager_client.cloud_resource_manager_projects = [project1] + cloudresourcemanager_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="", + labels={}, + lifecycle_state="ACTIVE", + ) + } + cloudresourcemanager_client.region = "global" + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.iam.iam_audit_logs_enabled.iam_audit_logs_enabled.cloudresourcemanager_client", + new=cloudresourcemanager_client, + ), + ): + from prowler.providers.gcp.services.iam.iam_audit_logs_enabled.iam_audit_logs_enabled import ( + iam_audit_logs_enabled, + ) + + check = iam_audit_logs_enabled() + result = check.execute() + + assert len(result) == 1 + for idx, r in enumerate(result): + assert r.status == "PASS" + assert search( + "Audit Logs are enabled for project", + r.status_extended, + ) + assert r.resource_id == GCP_PROJECT_ID + assert r.resource_name == GCP_PROJECT_ID + assert r.project_id == GCP_PROJECT_ID + assert r.location == cloudresourcemanager_client.region + + def test_uncompliant_project_empty_project_name(self): + from prowler.providers.gcp.services.cloudresourcemanager.cloudresourcemanager_service import ( + Project, + ) + + project1 = Project(id=GCP_PROJECT_ID, audit_logging=False) + + cloudresourcemanager_client = mock.MagicMock() + cloudresourcemanager_client.project_ids = [GCP_PROJECT_ID] + cloudresourcemanager_client.cloud_resource_manager_projects = [project1] + cloudresourcemanager_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="", + labels={}, + lifecycle_state="ACTIVE", + ) + } + cloudresourcemanager_client.region = "global" + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.iam.iam_audit_logs_enabled.iam_audit_logs_enabled.cloudresourcemanager_client", + new=cloudresourcemanager_client, + ), + ): + from prowler.providers.gcp.services.iam.iam_audit_logs_enabled.iam_audit_logs_enabled import ( + iam_audit_logs_enabled, + ) + + check = iam_audit_logs_enabled() + result = check.execute() + + assert len(result) == 1 + for idx, r in enumerate(result): + assert r.status == "FAIL" + assert search( + "Audit Logs are not enabled for project", + r.status_extended, + ) + assert r.resource_id == GCP_PROJECT_ID + assert r.resource_name == GCP_PROJECT_ID + assert r.project_id == GCP_PROJECT_ID + assert r.location == cloudresourcemanager_client.region diff --git a/tests/providers/gcp/services/iam/iam_no_service_roles_at_project_level/iam_no_service_roles_at_project_level_test.py b/tests/providers/gcp/services/iam/iam_no_service_roles_at_project_level/iam_no_service_roles_at_project_level_test.py index ee74496f27..66744d7ef6 100644 --- a/tests/providers/gcp/services/iam/iam_no_service_roles_at_project_level/iam_no_service_roles_at_project_level_test.py +++ b/tests/providers/gcp/services/iam/iam_no_service_roles_at_project_level/iam_no_service_roles_at_project_level_test.py @@ -212,3 +212,49 @@ class Test_iam_no_service_roles_at_project_level: assert result[0].resource_name == binding.role assert result[0].project_id == GCP_PROJECT_ID assert result[0].location == cloudresourcemanager_client.region + + def test_iam_no_bindings_empty_project_name(self): + cloudresourcemanager_client = mock.MagicMock() + cloudresourcemanager_client.bindings = [] + cloudresourcemanager_client.project_ids = [GCP_PROJECT_ID] + cloudresourcemanager_client.region = "global" + cloudresourcemanager_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="", + labels={}, + lifecycle_state="ACTIVE", + ) + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.cloudresourcemanager.cloudresourcemanager_service.CloudResourceManager", + new=cloudresourcemanager_client, + ), + mock.patch( + "prowler.providers.gcp.services.iam.iam_no_service_roles_at_project_level.iam_no_service_roles_at_project_level.cloudresourcemanager_client", + new=cloudresourcemanager_client, + ), + ): + from prowler.providers.gcp.services.iam.iam_no_service_roles_at_project_level.iam_no_service_roles_at_project_level import ( + iam_no_service_roles_at_project_level, + ) + + check = iam_no_service_roles_at_project_level() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert search( + "No IAM Users assigned to service roles at project level", + result[0].status_extended, + ) + assert result[0].resource_id == GCP_PROJECT_ID + assert result[0].resource_name == GCP_PROJECT_ID + assert result[0].project_id == GCP_PROJECT_ID + assert result[0].location == cloudresourcemanager_client.region diff --git a/tests/providers/gcp/services/iam/iam_role_kms_enforce_separation_of_duties/iam_role_kms_enforce_separation_of_duties_test.py b/tests/providers/gcp/services/iam/iam_role_kms_enforce_separation_of_duties/iam_role_kms_enforce_separation_of_duties_test.py index 6354340e52..95948eb8a6 100644 --- a/tests/providers/gcp/services/iam/iam_role_kms_enforce_separation_of_duties/iam_role_kms_enforce_separation_of_duties_test.py +++ b/tests/providers/gcp/services/iam/iam_role_kms_enforce_separation_of_duties/iam_role_kms_enforce_separation_of_duties_test.py @@ -173,3 +173,110 @@ class Test_iam_role_kms_enforce_separation_of_duties: assert r.resource_id == GCP_PROJECT_ID assert r.project_id == GCP_PROJECT_ID assert r.location == cloudresourcemanager_client.region + + def test_iam_no_bindings_empty_project_name(self): + cloudresourcemanager_client = mock.MagicMock() + cloudresourcemanager_client.bindings = [] + cloudresourcemanager_client.project_ids = [GCP_PROJECT_ID] + cloudresourcemanager_client.region = "global" + cloudresourcemanager_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="", + labels={}, + lifecycle_state="ACTIVE", + ) + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.iam.iam_role_kms_enforce_separation_of_duties.iam_role_kms_enforce_separation_of_duties.cloudresourcemanager_client", + new=cloudresourcemanager_client, + ), + ): + from prowler.providers.gcp.services.iam.iam_role_kms_enforce_separation_of_duties.iam_role_kms_enforce_separation_of_duties import ( + iam_role_kms_enforce_separation_of_duties, + ) + + check = iam_role_kms_enforce_separation_of_duties() + result = check.execute() + assert len(result) == 1 + for idx, r in enumerate(result): + assert r.status == "PASS" + assert search( + "Principle of separation of duties was enforced for KMS-Related Roles", + r.status_extended, + ) + assert r.resource_id == GCP_PROJECT_ID + assert r.resource_name == GCP_PROJECT_ID + assert r.project_id == GCP_PROJECT_ID + assert r.location == cloudresourcemanager_client.region + + def test_uncompliant_binding_empty_project_name(self): + from prowler.providers.gcp.services.cloudresourcemanager.cloudresourcemanager_service import ( + Binding, + ) + + binding1 = Binding( + role="roles/cloudkms.admin", + members=["serviceAccount:685829395199@cloudbuild.gserviceaccount.com"], + project_id=GCP_PROJECT_ID, + ) + binding2 = Binding( + role="roles/cloudkms.cryptoKeyEncrypterDecrypter", + members=["serviceAccount:685829395199@cloudbuild.gserviceaccount.com"], + project_id=GCP_PROJECT_ID, + ) + binding3 = Binding( + role="roles/connectors.managedZoneViewer", + members=["serviceAccount:685829395199@cloudbuild.gserviceaccount.com"], + project_id=GCP_PROJECT_ID, + ) + + cloudresourcemanager_client = mock.MagicMock() + cloudresourcemanager_client.project_ids = [GCP_PROJECT_ID] + cloudresourcemanager_client.bindings = [binding1, binding2, binding3] + cloudresourcemanager_client.region = "global" + cloudresourcemanager_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="", + labels={}, + lifecycle_state="ACTIVE", + ) + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.iam.iam_role_kms_enforce_separation_of_duties.iam_role_kms_enforce_separation_of_duties.cloudresourcemanager_client", + new=cloudresourcemanager_client, + ), + ): + from prowler.providers.gcp.services.iam.iam_role_kms_enforce_separation_of_duties.iam_role_kms_enforce_separation_of_duties import ( + iam_role_kms_enforce_separation_of_duties, + ) + + check = iam_role_kms_enforce_separation_of_duties() + result = check.execute() + + assert len(result) == 1 + for idx, r in enumerate(result): + assert r.status == "FAIL" + assert search( + "Principle of separation of duties was not enforced for KMS-Related Roles", + r.status_extended, + ) + assert r.resource_id == GCP_PROJECT_ID + assert r.resource_name == GCP_PROJECT_ID + assert r.project_id == GCP_PROJECT_ID + assert r.location == cloudresourcemanager_client.region diff --git a/tests/providers/gcp/services/iam/iam_role_sa_enforce_separation_of_duties/iam_role_sa_enforce_separation_of_duties_test.py b/tests/providers/gcp/services/iam/iam_role_sa_enforce_separation_of_duties/iam_role_sa_enforce_separation_of_duties_test.py index 479f540889..5672fc78ad 100644 --- a/tests/providers/gcp/services/iam/iam_role_sa_enforce_separation_of_duties/iam_role_sa_enforce_separation_of_duties_test.py +++ b/tests/providers/gcp/services/iam/iam_role_sa_enforce_separation_of_duties/iam_role_sa_enforce_separation_of_duties_test.py @@ -173,3 +173,110 @@ class Test_iam_role_sa_enforce_separation_of_duties: assert r.resource_id == GCP_PROJECT_ID assert r.project_id == GCP_PROJECT_ID assert r.location == cloudresourcemanager_client.region + + def test_iam_no_bindings_empty_project_name(self): + cloudresourcemanager_client = mock.MagicMock() + cloudresourcemanager_client.bindings = [] + cloudresourcemanager_client.project_ids = [GCP_PROJECT_ID] + cloudresourcemanager_client.region = "global" + cloudresourcemanager_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="", + labels={}, + lifecycle_state="ACTIVE", + ) + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.iam.iam_role_sa_enforce_separation_of_duties.iam_role_sa_enforce_separation_of_duties.cloudresourcemanager_client", + new=cloudresourcemanager_client, + ), + ): + from prowler.providers.gcp.services.iam.iam_role_sa_enforce_separation_of_duties.iam_role_sa_enforce_separation_of_duties import ( + iam_role_sa_enforce_separation_of_duties, + ) + + check = iam_role_sa_enforce_separation_of_duties() + result = check.execute() + assert len(result) == 1 + for idx, r in enumerate(result): + assert r.status == "PASS" + assert search( + "Principle of separation of duties was enforced for Service-Account Related Roles", + r.status_extended, + ) + assert r.resource_id == GCP_PROJECT_ID + assert r.resource_name == GCP_PROJECT_ID + assert r.project_id == GCP_PROJECT_ID + assert r.location == cloudresourcemanager_client.region + + def test_one_uncompliant_binding_empty_project_name(self): + from prowler.providers.gcp.services.cloudresourcemanager.cloudresourcemanager_service import ( + Binding, + ) + + binding1 = Binding( + role="roles/iam.serviceAccountUser", + members=["serviceAccount:685829395199@cloudbuild.gserviceaccount.com"], + project_id=GCP_PROJECT_ID, + ) + binding2 = Binding( + role="roles/compute.serviceAgent", + members=["serviceAccount:685829395199@cloudbuild.gserviceaccount.com"], + project_id=GCP_PROJECT_ID, + ) + binding3 = Binding( + role="roles/connectors.managedZoneViewer", + members=["serviceAccount:685829395199@cloudbuild.gserviceaccount.com"], + project_id=GCP_PROJECT_ID, + ) + + cloudresourcemanager_client = mock.MagicMock() + cloudresourcemanager_client.project_ids = [GCP_PROJECT_ID] + cloudresourcemanager_client.bindings = [binding1, binding2, binding3] + cloudresourcemanager_client.region = "global" + cloudresourcemanager_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="", + labels={}, + lifecycle_state="ACTIVE", + ) + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.iam.iam_role_sa_enforce_separation_of_duties.iam_role_sa_enforce_separation_of_duties.cloudresourcemanager_client", + new=cloudresourcemanager_client, + ), + ): + from prowler.providers.gcp.services.iam.iam_role_sa_enforce_separation_of_duties.iam_role_sa_enforce_separation_of_duties import ( + iam_role_sa_enforce_separation_of_duties, + ) + + check = iam_role_sa_enforce_separation_of_duties() + result = check.execute() + + assert len(result) == 1 + for idx, r in enumerate(result): + assert r.status == "FAIL" + assert search( + "Principle of separation of duties was not enforced for Service-Account Related Roles", + r.status_extended, + ) + assert r.resource_id == GCP_PROJECT_ID + assert r.resource_name == GCP_PROJECT_ID + assert r.project_id == GCP_PROJECT_ID + assert r.location == cloudresourcemanager_client.region diff --git a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled_test.py b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled_test.py index 7dd0a4ca56..99fbd43d5e 100644 --- a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled_test.py +++ b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled_test.py @@ -93,6 +93,58 @@ class Test_logging_log_metric_filter_and_alert_for_audit_configuration_changes_e assert result[0].project_id == GCP_PROJECT_ID assert result[0].location == GCP_EU1_LOCATION + def test_no_log_metric_filters_no_alerts_one_project_empty_name(self): + logging_client = MagicMock() + monitoring_client = MagicMock() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled.logging_client", + new=logging_client, + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled.monitoring_client", + new=monitoring_client, + ), + ): + from prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled import ( + logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled, + ) + + logging_client.metrics = [] + logging_client.project_ids = [GCP_PROJECT_ID] + logging_client.region = GCP_EU1_LOCATION + logging_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="", + labels={}, + lifecycle_state="ACTIVE", + ) + } + + monitoring_client.alert_policies = [] + + check = ( + logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled() + ) + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"There are no log metric filters or alerts associated in project {GCP_PROJECT_ID}." + ) + assert result[0].resource_id == GCP_PROJECT_ID + assert result[0].resource_name == GCP_PROJECT_ID + assert result[0].project_id == GCP_PROJECT_ID + assert result[0].location == GCP_EU1_LOCATION + def test_log_metric_filters_no_alerts(self): logging_client = MagicMock() monitoring_client = MagicMock() diff --git a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled_test.py b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled_test.py index 17cf202c76..3faf9c6309 100644 --- a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled_test.py +++ b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled_test.py @@ -93,6 +93,58 @@ class Test_logging_log_metric_filter_and_alert_for_bucket_permission_changes_ena assert result[0].project_id == GCP_PROJECT_ID assert result[0].location == GCP_EU1_LOCATION + def test_no_log_metric_filters_no_alerts_one_project_empty_name(self): + logging_client = MagicMock() + monitoring_client = MagicMock() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.logging_client", + new=logging_client, + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.monitoring_client", + new=monitoring_client, + ), + ): + from prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled import ( + logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled, + ) + + logging_client.metrics = [] + logging_client.project_ids = [GCP_PROJECT_ID] + logging_client.region = GCP_EU1_LOCATION + logging_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="", + labels={}, + lifecycle_state="ACTIVE", + ) + } + + monitoring_client.alert_policies = [] + + check = ( + logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled() + ) + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"There are no log metric filters or alerts associated in project {GCP_PROJECT_ID}." + ) + assert result[0].resource_id == GCP_PROJECT_ID + assert result[0].resource_name == GCP_PROJECT_ID + assert result[0].project_id == GCP_PROJECT_ID + assert result[0].location == GCP_EU1_LOCATION + def test_log_metric_filters_no_alerts(self): logging_client = MagicMock() monitoring_client = MagicMock() diff --git a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled_test.py b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled_test.py index d08cabcfb3..fe4ca5e344 100644 --- a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled_test.py +++ b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled_test.py @@ -93,6 +93,58 @@ class Test_logging_log_metric_filter_and_alert_for_custom_role_changes_enabled: assert result[0].project_id == GCP_PROJECT_ID assert result[0].location == GCP_EU1_LOCATION + def test_no_log_metric_filters_no_alerts_one_project_empty_name(self): + logging_client = MagicMock() + monitoring_client = MagicMock() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_custom_role_changes_enabled.logging_log_metric_filter_and_alert_for_custom_role_changes_enabled.logging_client", + new=logging_client, + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_custom_role_changes_enabled.logging_log_metric_filter_and_alert_for_custom_role_changes_enabled.monitoring_client", + new=monitoring_client, + ), + ): + from prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_custom_role_changes_enabled.logging_log_metric_filter_and_alert_for_custom_role_changes_enabled import ( + logging_log_metric_filter_and_alert_for_custom_role_changes_enabled, + ) + + logging_client.metrics = [] + logging_client.project_ids = [GCP_PROJECT_ID] + logging_client.region = GCP_EU1_LOCATION + logging_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="", + labels={}, + lifecycle_state="ACTIVE", + ) + } + + monitoring_client.alert_policies = [] + + check = ( + logging_log_metric_filter_and_alert_for_custom_role_changes_enabled() + ) + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"There are no log metric filters or alerts associated in project {GCP_PROJECT_ID}." + ) + assert result[0].resource_id == GCP_PROJECT_ID + assert result[0].resource_name == GCP_PROJECT_ID + assert result[0].project_id == GCP_PROJECT_ID + assert result[0].location == GCP_EU1_LOCATION + def test_log_metric_filters_no_alerts(self): logging_client = MagicMock() monitoring_client = MagicMock() diff --git a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled_test.py b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled_test.py index 6687c5d3bd..a3279bca0d 100644 --- a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled_test.py +++ b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled_test.py @@ -93,6 +93,58 @@ class Test_logging_log_metric_filter_and_alert_for_project_ownership_changes_ena assert result[0].project_id == GCP_PROJECT_ID assert result[0].location == GCP_EU1_LOCATION + def test_no_log_metric_filters_no_alerts_one_project_empty_name(self): + logging_client = MagicMock() + monitoring_client = MagicMock() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.logging_client", + new=logging_client, + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.monitoring_client", + new=monitoring_client, + ), + ): + from prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled import ( + logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled, + ) + + logging_client.metrics = [] + logging_client.project_ids = [GCP_PROJECT_ID] + logging_client.region = GCP_EU1_LOCATION + logging_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="", + labels={}, + lifecycle_state="ACTIVE", + ) + } + + monitoring_client.alert_policies = [] + + check = ( + logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled() + ) + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"There are no log metric filters or alerts associated in project {GCP_PROJECT_ID}." + ) + assert result[0].resource_id == GCP_PROJECT_ID + assert result[0].resource_name == GCP_PROJECT_ID + assert result[0].project_id == GCP_PROJECT_ID + assert result[0].location == GCP_EU1_LOCATION + def test_log_metric_filters_no_alerts(self): logging_client = MagicMock() monitoring_client = MagicMock() diff --git a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled_test.py b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled_test.py index d4bdfaa579..18562913de 100644 --- a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled_test.py +++ b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled_test.py @@ -93,6 +93,58 @@ class Test_logging_log_metric_filter_and_alert_for_sql_instance_configuration_ch assert result[0].project_id == GCP_PROJECT_ID assert result[0].location == GCP_EU1_LOCATION + def test_no_log_metric_filters_no_alerts_one_project_empty_name(self): + logging_client = MagicMock() + monitoring_client = MagicMock() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled.logging_client", + new=logging_client, + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled.monitoring_client", + new=monitoring_client, + ), + ): + from prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled import ( + logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled, + ) + + logging_client.metrics = [] + logging_client.project_ids = [GCP_PROJECT_ID] + logging_client.region = GCP_EU1_LOCATION + logging_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="", + labels={}, + lifecycle_state="ACTIVE", + ) + } + + monitoring_client.alert_policies = [] + + check = ( + logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled() + ) + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"There are no log metric filters or alerts associated in project {GCP_PROJECT_ID}." + ) + assert result[0].resource_id == GCP_PROJECT_ID + assert result[0].resource_name == GCP_PROJECT_ID + assert result[0].project_id == GCP_PROJECT_ID + assert result[0].location == GCP_EU1_LOCATION + def test_log_metric_filters_no_alerts(self): logging_client = MagicMock() monitoring_client = MagicMock() diff --git a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled_test.py b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled_test.py index cc8dfc6338..ce773d041d 100644 --- a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled_test.py +++ b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled_test.py @@ -93,6 +93,58 @@ class Test_logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_ena assert result[0].project_id == GCP_PROJECT_ID assert result[0].location == GCP_EU1_LOCATION + def test_no_log_metric_filters_no_alerts_one_project_empty_name(self): + logging_client = MagicMock() + monitoring_client = MagicMock() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled.logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled.logging_client", + new=logging_client, + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled.logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled.monitoring_client", + new=monitoring_client, + ), + ): + from prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled.logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled import ( + logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled, + ) + + logging_client.metrics = [] + logging_client.project_ids = [GCP_PROJECT_ID] + logging_client.region = GCP_EU1_LOCATION + logging_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="", + labels={}, + lifecycle_state="ACTIVE", + ) + } + + monitoring_client.alert_policies = [] + + check = ( + logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled() + ) + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"There are no log metric filters or alerts associated in project {GCP_PROJECT_ID}." + ) + assert result[0].resource_id == GCP_PROJECT_ID + assert result[0].resource_name == GCP_PROJECT_ID + assert result[0].project_id == GCP_PROJECT_ID + assert result[0].location == GCP_EU1_LOCATION + def test_log_metric_filters_no_alerts(self): logging_client = MagicMock() monitoring_client = MagicMock() diff --git a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled_test.py b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled_test.py index d9a166f33e..e97aba2ec3 100644 --- a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled_test.py +++ b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled_test.py @@ -93,6 +93,58 @@ class Test_logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled: assert result[0].project_id == GCP_PROJECT_ID assert result[0].location == GCP_EU1_LOCATION + def test_no_log_metric_filters_no_alerts_one_project_empty_name(self): + logging_client = MagicMock() + monitoring_client = MagicMock() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled.logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled.logging_client", + new=logging_client, + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled.logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled.monitoring_client", + new=monitoring_client, + ), + ): + from prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled.logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled import ( + logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled, + ) + + logging_client.metrics = [] + logging_client.project_ids = [GCP_PROJECT_ID] + logging_client.region = GCP_EU1_LOCATION + logging_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="", + labels={}, + lifecycle_state="ACTIVE", + ) + } + + monitoring_client.alert_policies = [] + + check = ( + logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled() + ) + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"There are no log metric filters or alerts associated in project {GCP_PROJECT_ID}." + ) + assert result[0].resource_id == GCP_PROJECT_ID + assert result[0].resource_name == GCP_PROJECT_ID + assert result[0].project_id == GCP_PROJECT_ID + assert result[0].location == GCP_EU1_LOCATION + def test_log_metric_filters_no_alerts(self): logging_client = MagicMock() monitoring_client = MagicMock() diff --git a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled_test.py b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled_test.py index f96835238c..1f413a03f2 100644 --- a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled_test.py +++ b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled_test.py @@ -93,6 +93,58 @@ class Test_logging_log_metric_filter_and_alert_for_vpc_network_route_changes_ena assert result[0].project_id == GCP_PROJECT_ID assert result[0].location == GCP_EU1_LOCATION + def test_no_log_metric_filters_no_alerts_one_project_empty_name(self): + logging_client = MagicMock() + monitoring_client = MagicMock() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled.logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled.logging_client", + new=logging_client, + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled.logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled.monitoring_client", + new=monitoring_client, + ), + ): + from prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled.logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled import ( + logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled, + ) + + logging_client.metrics = [] + logging_client.project_ids = [GCP_PROJECT_ID] + logging_client.region = GCP_EU1_LOCATION + logging_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="", + labels={}, + lifecycle_state="ACTIVE", + ) + } + + monitoring_client.alert_policies = [] + + check = ( + logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled() + ) + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"There are no log metric filters or alerts associated in project {GCP_PROJECT_ID}." + ) + assert result[0].resource_id == GCP_PROJECT_ID + assert result[0].resource_name == GCP_PROJECT_ID + assert result[0].project_id == GCP_PROJECT_ID + assert result[0].location == GCP_EU1_LOCATION + def test_log_metric_filters_no_alerts(self): logging_client = MagicMock() monitoring_client = MagicMock() diff --git a/tests/providers/gcp/services/logging/logging_sink_created/logging_sink_created_test.py b/tests/providers/gcp/services/logging/logging_sink_created/logging_sink_created_test.py index 9392efb625..f13b19d0cf 100644 --- a/tests/providers/gcp/services/logging/logging_sink_created/logging_sink_created_test.py +++ b/tests/providers/gcp/services/logging/logging_sink_created/logging_sink_created_test.py @@ -168,3 +168,46 @@ class Test_logging_sink_created: assert result[0].resource_name == "test" assert result[0].project_id == GCP_PROJECT_ID assert result[0].location == GCP_EU1_LOCATION + + def test_no_sinks_empty_project_name(self): + logging_client = MagicMock() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + patch( + "prowler.providers.gcp.services.logging.logging_sink_created.logging_sink_created.logging_client", + new=logging_client, + ), + ): + from prowler.providers.gcp.services.logging.logging_sink_created.logging_sink_created import ( + logging_sink_created, + ) + + logging_client.project_ids = [GCP_PROJECT_ID] + logging_client.region = GCP_EU1_LOCATION + logging_client.sinks = [] + logging_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="", + labels={}, + lifecycle_state="ACTIVE", + ) + } + + check = logging_sink_created() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].resource_id == GCP_PROJECT_ID + assert result[0].resource_name == GCP_PROJECT_ID + assert result[0].project_id == GCP_PROJECT_ID + assert result[0].location == GCP_EU1_LOCATION + assert ( + result[0].status_extended + == f"There are no logging sinks to export copies of all the log entries in project {GCP_PROJECT_ID}." + ) diff --git a/tests/providers/iac/iac_fixtures.py b/tests/providers/iac/iac_fixtures.py index 70f0698bdf..877d843939 100644 --- a/tests/providers/iac/iac_fixtures.py +++ b/tests/providers/iac/iac_fixtures.py @@ -1,67 +1,257 @@ +from checkov.common.models.enums import CheckResult +from checkov.common.output.record import Record +from checkov.common.output.report import Report + # IAC Provider Constants DEFAULT_SCAN_PATH = "." -# Sample Checkov Output -SAMPLE_CHECKOV_OUTPUT = [ - { - "check_type": "terraform", - "results": { - "failed_checks": [ - { - "check_id": "CKV_AWS_1", - "check_name": "Ensure S3 bucket has encryption enabled", - "guideline": "https://docs.bridgecrew.io/docs/s3_1-s3-bucket-has-encryption-enabled", - "severity": "low", - }, - { - "check_id": "CKV_AWS_2", - "check_name": "Ensure S3 bucket has public access blocked", - "guideline": "https://docs.bridgecrew.io/docs/s3_2-s3-bucket-has-public-access-blocked", - "severity": "low", - }, - ], - "passed_checks": [ - { - "check_id": "CKV_AWS_3", - "check_name": "Ensure S3 bucket has versioning enabled", - "guideline": "https://docs.bridgecrew.io/docs/s3_3-s3-bucket-has-versioning-enabled", - "severity": "low", - } - ], - }, - } -] - # Sample Finding Data -SAMPLE_FINDING = SAMPLE_CHECKOV_OUTPUT[0] +SAMPLE_FINDING = Report(check_type="terraform") +SAMPLE_FAILED_CHECK = Record( + check_id="CKV_AWS_1", + check_name="Ensure S3 bucket has encryption enabled", + severity="low", + file_path="test.tf", + file_line_range=[1, 2], + resource="aws_s3_bucket.test_bucket", + evaluations=[], + check_class="terraform", + check_result=CheckResult.FAILED, + code_block=[], + file_abs_path="test.tf", +) +SAMPLE_FAILED_CHECK.guideline = ( + "https://docs.bridgecrew.io/docs/s3_1-s3-bucket-has-encryption-enabled" +) -SAMPLE_FAILED_CHECK = { - "check_id": "CKV_AWS_1", - "check_name": "Ensure S3 bucket has encryption enabled", - "guideline": "https://docs.bridgecrew.io/docs/s3_1-s3-bucket-has-encryption-enabled", - "severity": "low", -} +SAMPLE_PASSED_CHECK = Record( + check_id="CKV_AWS_3", + check_name="Ensure S3 bucket has versioning enabled", + severity="low", + file_path="test.tf", + file_line_range=[1, 2], + resource="aws_s3_bucket.test_bucket", + evaluations=[], + check_class="terraform", + check_result=CheckResult.PASSED, + code_block=[], + file_abs_path="test.tf", +) +SAMPLE_PASSED_CHECK.guideline = ( + "https://docs.bridgecrew.io/docs/s3_3-s3-bucket-has-versioning-enabled" +) -SAMPLE_PASSED_CHECK = { - "check_id": "CKV_AWS_3", - "check_name": "Ensure S3 bucket has versioning enabled", - "guideline": "https://docs.bridgecrew.io/docs/s3_3-s3-bucket-has-versioning-enabled", - "severity": "low", -} +# Additional test fixtures for comprehensive testing +SAMPLE_SKIPPED_CHECK = Record( + check_id="CKV_AWS_2", + check_name="Ensure S3 bucket has public access blocked", + severity="high", + file_path="test.tf", + file_line_range=[3, 4], + resource="aws_s3_bucket.test_bucket", + evaluations=[], + check_class="terraform", + check_result=CheckResult.SKIPPED, + code_block=[], + file_abs_path="test.tf", +) +SAMPLE_SKIPPED_CHECK.guideline = ( + "https://docs.bridgecrew.io/docs/s3_2-s3-bucket-has-public-access-blocked" +) +SAMPLE_HIGH_SEVERITY_CHECK = Record( + check_id="CKV_AWS_4", + check_name="Ensure S3 bucket has logging enabled", + severity="HIGH", + file_path="test.tf", + file_line_range=[5, 6], + resource="aws_s3_bucket.test_bucket", + evaluations=[], + check_class="terraform", + check_result=CheckResult.FAILED, + code_block=[], + file_abs_path="test.tf", +) +SAMPLE_HIGH_SEVERITY_CHECK.guideline = ( + "https://docs.bridgecrew.io/docs/s3_4-s3-bucket-has-logging-enabled" +) -def get_sample_checkov_json_output(): - """Return sample Checkov JSON output as string""" - import json +SAMPLE_KUBERNETES_CHECK = Record( + check_id="CKV_K8S_1", + check_name="Ensure API server has audit logging enabled", + severity="medium", + file_path="deployment.yaml", + file_line_range=[1, 10], + resource="kubernetes_deployment.test_deployment", + evaluations=[], + check_class="kubernetes", + check_result=CheckResult.FAILED, + code_block=[], + file_abs_path="deployment.yaml", +) +SAMPLE_KUBERNETES_CHECK.guideline = ( + "https://docs.bridgecrew.io/docs/k8s_1-api-server-has-audit-logging-enabled" +) - return json.dumps(SAMPLE_CHECKOV_OUTPUT) +SAMPLE_CLOUDFORMATION_CHECK = Record( + check_id="CKV_AWS_5", + check_name="Ensure CloudFormation stacks are not publicly accessible", + severity="critical", + file_path="template.yaml", + file_line_range=[1, 20], + resource="AWS::CloudFormation::Stack", + evaluations=[], + check_class="cloudformation", + check_result=CheckResult.PASSED, + code_block=[], + file_abs_path="template.yaml", +) +SAMPLE_CLOUDFORMATION_CHECK.guideline = "https://docs.bridgecrew.io/docs/cfn_1-cloudformation-stacks-are-not-publicly-accessible" +# Sample findings for different frameworks +SAMPLE_KUBERNETES_FINDING = Report(check_type="kubernetes") +SAMPLE_CLOUDFORMATION_FINDING = Report(check_type="cloudformation") -def get_empty_checkov_output(): - """Return empty Checkov output as string""" - return "[]" +# Additional fixtures for different test scenarios +SAMPLE_CHECK_WITHOUT_GUIDELINE = Record( + check_id="CKV_AWS_6", + check_name="Test check without guideline", + severity="low", + file_path="test.tf", + file_line_range=[1, 2], + resource="aws_s3_bucket.test_bucket", + evaluations=[], + check_class="terraform", + check_result=CheckResult.FAILED, + code_block=[], + file_abs_path="test.tf", +) +# Note: No guideline attribute set +SAMPLE_MEDIUM_SEVERITY_CHECK = Record( + check_id="CKV_AWS_7", + check_name="Ensure S3 bucket has proper access controls", + severity="MEDIUM", + file_path="test.tf", + file_line_range=[7, 8], + resource="aws_s3_bucket.test_bucket", + evaluations=[], + check_class="terraform", + check_result=CheckResult.FAILED, + code_block=[], + file_abs_path="test.tf", +) +SAMPLE_MEDIUM_SEVERITY_CHECK.guideline = ( + "https://docs.bridgecrew.io/docs/s3_7-s3-bucket-has-proper-access-controls" +) -def get_invalid_checkov_output(): - """Return invalid JSON output as string""" - return "invalid json output" +SAMPLE_CRITICAL_SEVERITY_CHECK = Record( + check_id="CKV_AWS_8", + check_name="Ensure S3 bucket has encryption at rest", + severity="CRITICAL", + file_path="test.tf", + file_line_range=[9, 10], + resource="aws_s3_bucket.test_bucket", + evaluations=[], + check_class="terraform", + check_result=CheckResult.FAILED, + code_block=[], + file_abs_path="test.tf", +) +SAMPLE_CRITICAL_SEVERITY_CHECK.guideline = ( + "https://docs.bridgecrew.io/docs/s3_8-s3-bucket-has-encryption-at-rest" +) + +# Sample reports for different frameworks +SAMPLE_TERRAFORM_REPORT = Report(check_type="terraform") +SAMPLE_KUBERNETES_REPORT = Report(check_type="kubernetes") +SAMPLE_CLOUDFORMATION_REPORT = Report(check_type="cloudformation") +SAMPLE_DOCKERFILE_REPORT = Report(check_type="dockerfile") +SAMPLE_YAML_REPORT = Report(check_type="yaml") + +# Sample checks for different frameworks +SAMPLE_DOCKERFILE_CHECK = Record( + check_id="CKV_DOCKER_1", + check_name="Ensure base image is not using latest tag", + severity="high", + file_path="Dockerfile", + file_line_range=[1, 1], + resource="Dockerfile", + evaluations=[], + check_class="dockerfile", + check_result=CheckResult.FAILED, + code_block=[], + file_abs_path="Dockerfile", +) +SAMPLE_DOCKERFILE_CHECK.guideline = ( + "https://docs.bridgecrew.io/docs/docker_1-base-image-not-using-latest-tag" +) + +SAMPLE_YAML_CHECK = Record( + check_id="CKV_YAML_1", + check_name="Ensure YAML file has proper indentation", + severity="low", + file_path="config.yaml", + file_line_range=[1, 5], + resource="config.yaml", + evaluations=[], + check_class="yaml", + check_result=CheckResult.PASSED, + code_block=[], + file_abs_path="config.yaml", +) +SAMPLE_YAML_CHECK.guideline = ( + "https://docs.bridgecrew.io/docs/yaml_1-proper-indentation" +) + +# Sample checks with different statuses for comprehensive testing +SAMPLE_ANOTHER_FAILED_CHECK = Record( + check_id="CKV_AWS_9", + check_name="Ensure S3 bucket has lifecycle policy", + severity="medium", + file_path="test.tf", + file_line_range=[11, 12], + resource="aws_s3_bucket.test_bucket", + evaluations=[], + check_class="terraform", + check_result=CheckResult.FAILED, + code_block=[], + file_abs_path="test.tf", +) +SAMPLE_ANOTHER_FAILED_CHECK.guideline = ( + "https://docs.bridgecrew.io/docs/s3_9-s3-bucket-has-lifecycle-policy" +) + +SAMPLE_ANOTHER_PASSED_CHECK = Record( + check_id="CKV_AWS_10", + check_name="Ensure S3 bucket has proper tags", + severity="low", + file_path="test.tf", + file_line_range=[13, 14], + resource="aws_s3_bucket.test_bucket", + evaluations=[], + check_class="terraform", + check_result=CheckResult.PASSED, + code_block=[], + file_abs_path="test.tf", +) +SAMPLE_ANOTHER_PASSED_CHECK.guideline = ( + "https://docs.bridgecrew.io/docs/s3_10-s3-bucket-has-proper-tags" +) + +SAMPLE_ANOTHER_SKIPPED_CHECK = Record( + check_id="CKV_AWS_11", + check_name="Ensure S3 bucket has cross-region replication", + severity="high", + file_path="test.tf", + file_line_range=[15, 16], + resource="aws_s3_bucket.test_bucket", + evaluations=[], + check_class="terraform", + check_result=CheckResult.SKIPPED, + code_block=[], + file_abs_path="test.tf", +) +SAMPLE_ANOTHER_SKIPPED_CHECK.guideline = ( + "https://docs.bridgecrew.io/docs/s3_11-s3-bucket-has-cross-region-replication" +) diff --git a/tests/providers/iac/iac_provider_test.py b/tests/providers/iac/iac_provider_test.py index e2f3469569..f3da2a7c11 100644 --- a/tests/providers/iac/iac_provider_test.py +++ b/tests/providers/iac/iac_provider_test.py @@ -1,4 +1,4 @@ -from unittest.mock import MagicMock, patch +from unittest.mock import Mock, patch import pytest @@ -6,12 +6,24 @@ from prowler.lib.check.models import CheckReportIAC from prowler.providers.iac.iac_provider import IacProvider from tests.providers.iac.iac_fixtures import ( DEFAULT_SCAN_PATH, + SAMPLE_ANOTHER_FAILED_CHECK, + SAMPLE_ANOTHER_PASSED_CHECK, + SAMPLE_ANOTHER_SKIPPED_CHECK, + SAMPLE_CHECK_WITHOUT_GUIDELINE, + SAMPLE_CLOUDFORMATION_CHECK, + SAMPLE_CRITICAL_SEVERITY_CHECK, + SAMPLE_DOCKERFILE_CHECK, + SAMPLE_DOCKERFILE_REPORT, SAMPLE_FAILED_CHECK, SAMPLE_FINDING, + SAMPLE_HIGH_SEVERITY_CHECK, + SAMPLE_KUBERNETES_CHECK, + SAMPLE_KUBERNETES_FINDING, + SAMPLE_MEDIUM_SEVERITY_CHECK, SAMPLE_PASSED_CHECK, - get_empty_checkov_output, - get_invalid_checkov_output, - get_sample_checkov_json_output, + SAMPLE_SKIPPED_CHECK, + SAMPLE_YAML_CHECK, + SAMPLE_YAML_REPORT, ) @@ -46,10 +58,10 @@ class TestIacProvider: assert report.status == "FAIL" assert report.check_metadata.Provider == "iac" - assert report.check_metadata.CheckID == SAMPLE_FAILED_CHECK["check_id"] - assert report.check_metadata.CheckTitle == SAMPLE_FAILED_CHECK["check_name"] + assert report.check_metadata.CheckID == SAMPLE_FAILED_CHECK.check_id + assert report.check_metadata.CheckTitle == SAMPLE_FAILED_CHECK.check_name assert report.check_metadata.Severity == "low" - assert report.check_metadata.RelatedUrl == SAMPLE_FAILED_CHECK["guideline"] + assert report.check_metadata.RelatedUrl == SAMPLE_FAILED_CHECK.guideline def test_iac_provider_process_check_passed(self): """Test processing a passed check""" @@ -61,72 +73,473 @@ class TestIacProvider: assert report.status == "PASS" assert report.check_metadata.Provider == "iac" - assert report.check_metadata.CheckID == SAMPLE_PASSED_CHECK["check_id"] - assert report.check_metadata.CheckTitle == SAMPLE_PASSED_CHECK["check_name"] + assert report.check_metadata.CheckID == SAMPLE_PASSED_CHECK.check_id + assert report.check_metadata.CheckTitle == SAMPLE_PASSED_CHECK.check_name assert report.check_metadata.Severity == "low" + assert report.check_metadata.RelatedUrl == SAMPLE_PASSED_CHECK.guideline - @patch("subprocess.run") - def test_iac_provider_run_scan_success(self, mock_subprocess): - """Test successful IAC scan with Checkov""" + def test_iac_provider_process_check_skipped(self): + """Test processing a skipped check""" provider = IacProvider() - mock_subprocess.return_value = MagicMock( - stdout=get_sample_checkov_json_output(), stderr="" - ) + report = provider._process_check(SAMPLE_FINDING, SAMPLE_SKIPPED_CHECK, "MUTED") - reports = provider.run_scan("/test/directory") + assert isinstance(report, CheckReportIAC) + assert report.status == "MUTED" + assert report.muted is True - # Should have 2 failed checks + 1 passed check = 3 total reports - assert len(reports) == 3 + assert report.check_metadata.Provider == "iac" + assert report.check_metadata.CheckID == SAMPLE_SKIPPED_CHECK.check_id + assert report.check_metadata.CheckTitle == SAMPLE_SKIPPED_CHECK.check_name + assert report.check_metadata.Severity == "high" + assert report.check_metadata.RelatedUrl == SAMPLE_SKIPPED_CHECK.guideline - # Check that we have both failed and passed reports - failed_reports = [r for r in reports if r.status == "FAIL"] - passed_reports = [r for r in reports if r.status == "PASS"] - - assert len(failed_reports) == 2 - assert len(passed_reports) == 1 - - # Verify subprocess was called correctly - mock_subprocess.assert_called_once_with( - ["checkov", "-d", "/test/directory", "-o", "json"], - capture_output=True, - text=True, - ) - - @patch("subprocess.run") - def test_iac_provider_run_scan_empty_output(self, mock_subprocess): - """Test IAC scan with empty Checkov output""" + def test_iac_provider_process_check_high_severity(self): + """Test processing a high severity check""" provider = IacProvider() - mock_subprocess.return_value = MagicMock( - stdout=get_empty_checkov_output(), stderr="" + report = provider._process_check( + SAMPLE_FINDING, SAMPLE_HIGH_SEVERITY_CHECK, "FAIL" ) - reports = provider.run_scan("/test/directory") + assert isinstance(report, CheckReportIAC) + assert report.status == "FAIL" + assert report.check_metadata.Severity == "high" - assert len(reports) == 0 - - @patch("subprocess.run") - def test_iac_provider_run_scan_invalid_json(self, mock_subprocess): - """Test IAC scan with invalid JSON output""" + def test_iac_provider_process_check_different_framework(self): + """Test processing a check from a different framework (Kubernetes)""" provider = IacProvider() - mock_subprocess.return_value = MagicMock( - stdout=get_invalid_checkov_output(), stderr="" + report = provider._process_check( + SAMPLE_KUBERNETES_FINDING, SAMPLE_KUBERNETES_CHECK, "FAIL" ) - with pytest.raises(SystemExit) as excinfo: - provider.run_scan("/test/directory") + assert isinstance(report, CheckReportIAC) + assert report.status == "FAIL" + assert report.check_metadata.ServiceName == "kubernetes" + assert report.check_metadata.CheckID == SAMPLE_KUBERNETES_CHECK.check_id - assert excinfo.value.code == 1 - - @patch("subprocess.run") - def test_iac_provider_run_scan_null_output(self, mock_subprocess): - """Test IAC scan with null Checkov output""" + def test_iac_provider_process_check_no_guideline(self): + """Test processing a check without guideline URL""" provider = IacProvider() - mock_subprocess.return_value = MagicMock(stdout="null", stderr="") + report = provider._process_check( + SAMPLE_FINDING, SAMPLE_CHECK_WITHOUT_GUIDELINE, "FAIL" + ) - reports = provider.run_scan("/test/directory") + assert isinstance(report, CheckReportIAC) + assert report.status == "FAIL" + assert report.check_metadata.RelatedUrl == "" - assert len(reports) == 0 + def test_iac_provider_process_check_medium_severity(self): + """Test processing a medium severity check""" + provider = IacProvider() + + report = provider._process_check( + SAMPLE_FINDING, SAMPLE_MEDIUM_SEVERITY_CHECK, "FAIL" + ) + + assert isinstance(report, CheckReportIAC) + assert report.status == "FAIL" + assert report.check_metadata.Severity == "medium" + + def test_iac_provider_process_check_critical_severity(self): + """Test processing a critical severity check""" + provider = IacProvider() + + report = provider._process_check( + SAMPLE_FINDING, SAMPLE_CRITICAL_SEVERITY_CHECK, "FAIL" + ) + + assert isinstance(report, CheckReportIAC) + assert report.status == "FAIL" + assert report.check_metadata.Severity == "critical" + + def test_iac_provider_process_check_dockerfile(self): + """Test processing a Dockerfile check""" + provider = IacProvider() + + report = provider._process_check( + SAMPLE_DOCKERFILE_REPORT, SAMPLE_DOCKERFILE_CHECK, "FAIL" + ) + + assert isinstance(report, CheckReportIAC) + assert report.status == "FAIL" + assert report.check_metadata.ServiceName == "dockerfile" + assert report.check_metadata.CheckID == SAMPLE_DOCKERFILE_CHECK.check_id + + def test_iac_provider_process_check_yaml(self): + """Test processing a YAML check""" + provider = IacProvider() + + report = provider._process_check(SAMPLE_YAML_REPORT, SAMPLE_YAML_CHECK, "PASS") + + assert isinstance(report, CheckReportIAC) + assert report.status == "PASS" + assert report.check_metadata.ServiceName == "yaml" + assert report.check_metadata.CheckID == SAMPLE_YAML_CHECK.check_id + + @patch("prowler.providers.iac.iac_provider.RunnerRegistry") + @patch("prowler.providers.iac.iac_provider.RunnerFilter") + @patch("prowler.providers.iac.iac_provider.logger") + def test_run_scan_success_with_failed_and_passed_checks( + self, mock_logger, mock_runner_filter, mock_runner_registry + ): + """Test successful run_scan with both failed and passed checks""" + # Setup mocks + mock_registry_instance = Mock() + mock_runner_registry.return_value = mock_registry_instance + + # Create mock reports with failed and passed checks + mock_report = Mock() + mock_report.check_type = "terraform" # Set the check_type attribute + mock_report.failed_checks = [SAMPLE_FAILED_CHECK] + mock_report.passed_checks = [SAMPLE_PASSED_CHECK] + mock_report.skipped_checks = [] + + mock_registry_instance.run.return_value = [mock_report] + + provider = IacProvider() + result = provider.run_scan("/test/directory", ["terraform"], []) + + # Verify logger was called + mock_logger.info.assert_called_with("Running IaC scan on /test/directory...") + + # Verify RunnerFilter was created with correct parameters + mock_runner_filter.assert_called_with( + framework=["terraform"], excluded_paths=[] + ) + + # Verify RunnerRegistry was created and run was called + mock_runner_registry.assert_called_once() + mock_registry_instance.run.assert_called_with(root_folder="/test/directory") + + # Verify results + assert len(result) == 2 + assert all(isinstance(report, CheckReportIAC) for report in result) + + # Check that we have one FAIL and one PASS report + statuses = [report.status for report in result] + assert "FAIL" in statuses + assert "PASS" in statuses + + @patch("prowler.providers.iac.iac_provider.RunnerRegistry") + @patch("prowler.providers.iac.iac_provider.RunnerFilter") + @patch("prowler.providers.iac.iac_provider.logger") + def test_run_scan_with_skipped_checks( + self, mock_logger, mock_runner_filter, mock_runner_registry + ): + """Test run_scan with skipped checks (muted)""" + # Setup mocks + mock_registry_instance = Mock() + mock_runner_registry.return_value = mock_registry_instance + + # Create mock report with skipped checks + mock_report = Mock() + mock_report.check_type = "terraform" # Set the check_type attribute + mock_report.failed_checks = [] + mock_report.passed_checks = [] + mock_report.skipped_checks = [SAMPLE_SKIPPED_CHECK] + + mock_registry_instance.run.return_value = [mock_report] + + provider = IacProvider() + result = provider.run_scan("/test/directory", ["all"], ["exclude/path"]) + + # Verify RunnerFilter was created with correct parameters + mock_runner_filter.assert_called_with( + framework=["all"], excluded_paths=["exclude/path"] + ) + + # Verify results + assert len(result) == 1 + assert isinstance(result[0], CheckReportIAC) + assert result[0].status == "MUTED" + assert result[0].muted is True + + @patch("prowler.providers.iac.iac_provider.RunnerRegistry") + @patch("prowler.providers.iac.iac_provider.RunnerFilter") + @patch("prowler.providers.iac.iac_provider.logger") + def test_run_scan_empty_results( + self, mock_logger, mock_runner_filter, mock_runner_registry + ): + """Test run_scan with no findings""" + # Setup mocks + mock_registry_instance = Mock() + mock_runner_registry.return_value = mock_registry_instance + + # Create mock report with no checks + mock_report = Mock() + mock_report.check_type = "terraform" # Set the check_type attribute + mock_report.failed_checks = [] + mock_report.passed_checks = [] + mock_report.skipped_checks = [] + + mock_registry_instance.run.return_value = [mock_report] + + provider = IacProvider() + result = provider.run_scan("/test/directory", ["kubernetes"], []) + + # Verify results + assert len(result) == 0 + + @patch("prowler.providers.iac.iac_provider.RunnerRegistry") + @patch("prowler.providers.iac.iac_provider.RunnerFilter") + @patch("prowler.providers.iac.iac_provider.logger") + def test_run_scan_multiple_reports( + self, mock_logger, mock_runner_filter, mock_runner_registry + ): + """Test run_scan with multiple reports from different frameworks""" + # Setup mocks + mock_registry_instance = Mock() + mock_runner_registry.return_value = mock_registry_instance + + # Create multiple mock reports + mock_report1 = Mock() + mock_report1.check_type = "terraform" # Set the check_type attribute + mock_report1.failed_checks = [SAMPLE_FAILED_CHECK] + mock_report1.passed_checks = [] + mock_report1.skipped_checks = [] + + mock_report2 = Mock() + mock_report2.check_type = "kubernetes" # Set the check_type attribute + mock_report2.failed_checks = [] + mock_report2.passed_checks = [SAMPLE_PASSED_CHECK] + mock_report2.skipped_checks = [] + + mock_registry_instance.run.return_value = [mock_report1, mock_report2] + + provider = IacProvider() + result = provider.run_scan("/test/directory", ["terraform", "kubernetes"], []) + + # Verify results + assert len(result) == 2 + assert all(isinstance(report, CheckReportIAC) for report in result) + + # Check that we have one FAIL and one PASS report + statuses = [report.status for report in result] + assert "FAIL" in statuses + assert "PASS" in statuses + + @patch("prowler.providers.iac.iac_provider.RunnerRegistry") + @patch("prowler.providers.iac.iac_provider.RunnerFilter") + @patch("prowler.providers.iac.iac_provider.logger") + @patch("prowler.providers.iac.iac_provider.sys") + def test_run_scan_exception_handling( + self, mock_sys, mock_logger, mock_runner_filter, mock_runner_registry + ): + """Test run_scan exception handling""" + # Setup mocks to raise an exception + mock_registry_instance = Mock() + mock_runner_registry.return_value = mock_registry_instance + mock_registry_instance.run.side_effect = Exception("Test exception") + + # Configure sys.exit to raise SystemExit + mock_sys.exit.side_effect = SystemExit(1) + + provider = IacProvider() + + # The function should call sys.exit(1) when an exception occurs + with pytest.raises(SystemExit) as exc_info: + provider.run_scan("/test/directory", ["terraform"], []) + + assert exc_info.value.code == 1 + + # Verify logger was called with error information + mock_logger.critical.assert_called_once() + critical_call_args = mock_logger.critical.call_args[0][0] + assert "Exception" in critical_call_args + assert "Test exception" in critical_call_args + + @patch("prowler.providers.iac.iac_provider.RunnerRegistry") + @patch("prowler.providers.iac.iac_provider.RunnerFilter") + @patch("prowler.providers.iac.iac_provider.logger") + def test_run_scan_with_different_frameworks( + self, mock_logger, mock_runner_filter, mock_runner_registry + ): + """Test run_scan with different framework configurations""" + # Setup mocks + mock_registry_instance = Mock() + mock_runner_registry.return_value = mock_registry_instance + + mock_report = Mock() + mock_report.check_type = "terraform" # Set the check_type attribute + mock_report.failed_checks = [] + mock_report.passed_checks = [SAMPLE_PASSED_CHECK] + mock_report.skipped_checks = [] + + mock_registry_instance.run.return_value = [mock_report] + + provider = IacProvider() + + # Test with specific frameworks + frameworks = ["terraform", "kubernetes", "cloudformation"] + result = provider.run_scan("/test/directory", frameworks, []) + + # Verify RunnerFilter was created with correct frameworks + mock_runner_filter.assert_called_with(framework=frameworks, excluded_paths=[]) + + # Verify results + assert len(result) == 1 + assert result[0].status == "PASS" + + @patch("prowler.providers.iac.iac_provider.RunnerRegistry") + @patch("prowler.providers.iac.iac_provider.RunnerFilter") + @patch("prowler.providers.iac.iac_provider.logger") + def test_run_scan_with_exclude_paths( + self, mock_logger, mock_runner_filter, mock_runner_registry + ): + """Test run_scan with exclude paths""" + # Setup mocks + mock_registry_instance = Mock() + mock_runner_registry.return_value = mock_registry_instance + + mock_report = Mock() + mock_report.check_type = "terraform" # Set the check_type attribute + mock_report.failed_checks = [] + mock_report.passed_checks = [SAMPLE_PASSED_CHECK] + mock_report.skipped_checks = [] + + mock_registry_instance.run.return_value = [mock_report] + + provider = IacProvider() + + # Test with exclude paths + exclude_paths = ["node_modules", ".git", "vendor"] + result = provider.run_scan("/test/directory", ["all"], exclude_paths) + + # Verify RunnerFilter was created with correct exclude paths + mock_runner_filter.assert_called_with( + framework=["all"], excluded_paths=exclude_paths + ) + + # Verify results + assert len(result) == 1 + assert result[0].status == "PASS" + + @patch("prowler.providers.iac.iac_provider.RunnerRegistry") + @patch("prowler.providers.iac.iac_provider.RunnerFilter") + @patch("prowler.providers.iac.iac_provider.logger") + def test_run_scan_all_check_types( + self, mock_logger, mock_runner_filter, mock_runner_registry + ): + """Test run_scan with all types of checks (failed, passed, skipped)""" + # Setup mocks + mock_registry_instance = Mock() + mock_runner_registry.return_value = mock_registry_instance + + mock_report = Mock() + mock_report.check_type = "terraform" # Set the check_type attribute + mock_report.failed_checks = [SAMPLE_FAILED_CHECK, SAMPLE_HIGH_SEVERITY_CHECK] + mock_report.passed_checks = [SAMPLE_PASSED_CHECK, SAMPLE_CLOUDFORMATION_CHECK] + mock_report.skipped_checks = [SAMPLE_SKIPPED_CHECK] + + mock_registry_instance.run.return_value = [mock_report] + + provider = IacProvider() + result = provider.run_scan("/test/directory", ["all"], []) + + # Verify results + assert len(result) == 5 # 2 failed + 2 passed + 1 skipped + + # Check status distribution + statuses = [report.status for report in result] + assert statuses.count("FAIL") == 2 + assert statuses.count("PASS") == 2 + assert statuses.count("MUTED") == 1 + + # Check that muted reports have muted=True + muted_reports = [report for report in result if report.status == "MUTED"] + assert all(report.muted for report in muted_reports) + + @patch("prowler.providers.iac.iac_provider.RunnerRegistry") + @patch("prowler.providers.iac.iac_provider.RunnerFilter") + @patch("prowler.providers.iac.iac_provider.logger") + def test_run_scan_no_reports_returned( + self, mock_logger, mock_runner_filter, mock_runner_registry + ): + """Test run_scan when no reports are returned from registry""" + # Setup mocks + mock_registry_instance = Mock() + mock_runner_registry.return_value = mock_registry_instance + + # Return empty list of reports + mock_registry_instance.run.return_value = [] + + provider = IacProvider() + result = provider.run_scan("/test/directory", ["terraform"], []) + + # Verify results + assert len(result) == 0 + + @patch("prowler.providers.iac.iac_provider.RunnerRegistry") + @patch("prowler.providers.iac.iac_provider.RunnerFilter") + @patch("prowler.providers.iac.iac_provider.logger") + def test_run_scan_multiple_frameworks_with_different_checks( + self, mock_logger, mock_runner_filter, mock_runner_registry + ): + """Test run_scan with multiple frameworks and different types of checks""" + # Setup mocks + mock_registry_instance = Mock() + mock_runner_registry.return_value = mock_registry_instance + + # Create reports for different frameworks + terraform_report = Mock() + terraform_report.check_type = "terraform" + terraform_report.failed_checks = [ + SAMPLE_FAILED_CHECK, + SAMPLE_ANOTHER_FAILED_CHECK, + ] + terraform_report.passed_checks = [SAMPLE_PASSED_CHECK] + terraform_report.skipped_checks = [] + + kubernetes_report = Mock() + kubernetes_report.check_type = "kubernetes" + kubernetes_report.failed_checks = [SAMPLE_KUBERNETES_CHECK] + kubernetes_report.passed_checks = [] + kubernetes_report.skipped_checks = [SAMPLE_ANOTHER_SKIPPED_CHECK] + + cloudformation_report = Mock() + cloudformation_report.check_type = "cloudformation" + cloudformation_report.failed_checks = [] + cloudformation_report.passed_checks = [ + SAMPLE_CLOUDFORMATION_CHECK, + SAMPLE_ANOTHER_PASSED_CHECK, + ] + cloudformation_report.skipped_checks = [] + + mock_registry_instance.run.return_value = [ + terraform_report, + kubernetes_report, + cloudformation_report, + ] + + provider = IacProvider() + result = provider.run_scan( + "/test/directory", ["terraform", "kubernetes", "cloudformation"], [] + ) + + # Verify results + assert ( + len(result) == 7 + ) # 2 failed + 1 passed (terraform) + 1 failed + 1 skipped (kubernetes) + 2 passed (cloudformation) + + # Check status distribution + statuses = [report.status for report in result] + assert statuses.count("FAIL") == 3 + assert statuses.count("PASS") == 3 + assert statuses.count("MUTED") == 1 + + def test_run_method_calls_run_scan(self): + """Test that the run method calls run_scan with correct parameters""" + provider = IacProvider( + scan_path="/custom/path", frameworks=["terraform"], exclude_path=["exclude"] + ) + + with patch.object(provider, "run_scan") as mock_run_scan: + mock_run_scan.return_value = [] + provider.run() + + mock_run_scan.assert_called_once_with( + "/custom/path", ["terraform"], ["exclude"] + ) diff --git a/tests/providers/m365/lib/powershell/m365_powershell_test.py b/tests/providers/m365/lib/powershell/m365_powershell_test.py index 1fa4753dad..f02f283d1f 100644 --- a/tests/providers/m365/lib/powershell/m365_powershell_test.py +++ b/tests/providers/m365/lib/powershell/m365_powershell_test.py @@ -4,6 +4,9 @@ import pytest from prowler.lib.powershell.powershell import PowerShellSession from prowler.providers.m365.exceptions.exceptions import ( + M365ExchangeConnectionError, + M365GraphConnectionError, + M365TeamsConnectionError, M365UserNotBelongingToTenantError, ) from prowler.providers.m365.lib.powershell.m365_powershell import M365PowerShell @@ -97,7 +100,7 @@ class Testm365PowerShell: session.init_credential(credentials) # Verify encrypt_password was called - session.encrypt_password.assert_called_once_with(credentials.passwd) + session.encrypt_password.assert_any_call(credentials.passwd) # Verify execute was called with the correct commands session.execute.assert_any_call(f'$user = "{credentials.user}"') @@ -167,6 +170,37 @@ class Testm365PowerShell: ) session.close() + @patch("subprocess.Popen") + def test_test_credentials_application_auth(self, mock_popen): + mock_process = MagicMock() + mock_popen.return_value = mock_process + with patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365Credentials", + autospec=True, + ) as mock_creds: + credentials = mock_creds.return_value + credentials.user = "" + credentials.passwd = "" + credentials.encrypted_passwd = "" + credentials.client_id = "test_client_id" + credentials.client_secret = "test_client_secret" + credentials.tenant_id = "test_tenant_id" + identity = M365IdentityInfo( + identity_id="test_id", + identity_type="Application", + tenant_id="test_tenant", + tenant_domain="contoso.onmicrosoft.com", + tenant_domains=["contoso.onmicrosoft.com"], + location="test_location", + ) + session = M365PowerShell(credentials, identity) + session.execute = MagicMock(return_value="sometoken") + + result = session.test_credentials(credentials) + assert result is True + session.execute.assert_any_call("Write-Output $graphToken") + session.close() + @patch("subprocess.Popen") @patch("msal.ConfidentialClientApplication") def test_test_credentials_user_not_belonging_to_tenant(self, mock_msal, mock_popen): @@ -514,12 +548,13 @@ class Testm365PowerShell: # Verify successful initialization assert result is True # Verify that execute was called for each module - assert mock_execute_obj.call_count == 6 # 2 modules * 3 commands each + assert mock_execute_obj.call_count == 9 # 3 modules * 3 commands each # Verify success messages were logged mock_info.assert_any_call( "Successfully installed module ExchangeOnlineManagement" ) mock_info.assert_any_call("Successfully installed module MicrosoftTeams") + mock_info.assert_any_call("Successfully installed module MSAL.PS") @patch("subprocess.Popen") def test_initialize_m365_powershell_modules_failure(self, mock_popen): @@ -582,11 +617,12 @@ class Testm365PowerShell: main() # Verify all info messages were logged in the correct order - assert mock_info.call_count == 3 + assert mock_info.call_count == 4 mock_info.assert_has_calls( [ call("Successfully installed module ExchangeOnlineManagement"), call("Successfully installed module MicrosoftTeams"), + call("Successfully installed module MSAL.PS"), call("M365 PowerShell modules initialized successfully"), ] ) @@ -629,6 +665,260 @@ class Testm365PowerShell: # Verify no info messages were logged mock_info.assert_not_called() + @patch("subprocess.Popen") + def test_test_graph_connection_success(self, mock_popen): + """Test test_graph_connection when token is valid""" + mock_process = MagicMock() + mock_popen.return_value = mock_process + credentials = M365Credentials(user="test@example.com", passwd="test_password") + identity = M365IdentityInfo( + identity_id="test_id", + identity_type="Application", + tenant_id="test_tenant", + tenant_domain="example.com", + tenant_domains=["example.com"], + location="test_location", + ) + session = M365PowerShell(credentials, identity) + + # Mock execute to return a valid token + session.execute = MagicMock(return_value="valid_token") + + result = session.test_graph_connection() + + assert result is True + session.execute.assert_called_once_with("Write-Output $graphToken") + session.close() + + @patch("subprocess.Popen") + def test_test_graph_connection_empty_token(self, mock_popen): + """Test test_graph_connection when token is empty""" + mock_process = MagicMock() + mock_popen.return_value = mock_process + credentials = M365Credentials(user="test@example.com", passwd="test_password") + identity = M365IdentityInfo( + identity_id="test_id", + identity_type="Application", + tenant_id="test_tenant", + tenant_domain="example.com", + tenant_domains=["example.com"], + location="test_location", + ) + session = M365PowerShell(credentials, identity) + + # Mock execute to return empty token + session.execute = MagicMock(return_value="") + + with pytest.raises(M365GraphConnectionError) as exc_info: + session.test_graph_connection() + + assert "Microsoft Graph token is empty or invalid" in str(exc_info.value) + session.execute.assert_called_once_with("Write-Output $graphToken") + session.close() + + @patch("subprocess.Popen") + def test_test_graph_connection_exception(self, mock_popen): + """Test test_graph_connection when an exception occurs""" + mock_process = MagicMock() + mock_popen.return_value = mock_process + credentials = M365Credentials(user="test@example.com", passwd="test_password") + identity = M365IdentityInfo( + identity_id="test_id", + identity_type="Application", + tenant_id="test_tenant", + tenant_domain="example.com", + tenant_domains=["example.com"], + location="test_location", + ) + session = M365PowerShell(credentials, identity) + + # Mock execute to raise an exception + session.execute = MagicMock(side_effect=Exception("PowerShell error")) + + with pytest.raises(M365GraphConnectionError) as exc_info: + session.test_graph_connection() + + assert "Failed to connect to Microsoft Graph API: PowerShell error" in str( + exc_info.value + ) + session.close() + + @patch("subprocess.Popen") + def test_test_teams_connection_success(self, mock_popen): + """Test test_teams_connection when token is valid""" + mock_process = MagicMock() + mock_popen.return_value = mock_process + credentials = M365Credentials(user="test@example.com", passwd="test_password") + identity = M365IdentityInfo( + identity_id="test_id", + identity_type="Application", + tenant_id="test_tenant", + tenant_domain="example.com", + tenant_domains=["example.com"], + location="test_location", + ) + session = M365PowerShell(credentials, identity) + + # Mock execute to return valid responses + def mock_execute(command, *args, **kwargs): + if "Write-Output $teamsToken" in command: + return "valid_teams_token" + return None + + session.execute = MagicMock(side_effect=mock_execute) + + result = session.test_teams_connection() + + assert result is True + # Verify all expected PowerShell commands were called + assert session.execute.call_count == 3 + session.close() + + @patch("subprocess.Popen") + def test_test_teams_connection_empty_token(self, mock_popen): + """Test test_teams_connection when token is empty""" + mock_process = MagicMock() + mock_popen.return_value = mock_process + credentials = M365Credentials(user="test@example.com", passwd="test_password") + identity = M365IdentityInfo( + identity_id="test_id", + identity_type="Application", + tenant_id="test_tenant", + tenant_domain="example.com", + tenant_domains=["example.com"], + location="test_location", + ) + session = M365PowerShell(credentials, identity) + + # Mock execute to return empty token when checking + def mock_execute(command, *args, **kwargs): + if "Write-Output $teamsToken" in command: + return "" + return None + + session.execute = MagicMock(side_effect=mock_execute) + + with pytest.raises(M365TeamsConnectionError) as exc_info: + session.test_teams_connection() + + assert "Microsoft Teams token is empty or invalid" in str(exc_info.value) + session.close() + + @patch("subprocess.Popen") + def test_test_teams_connection_exception(self, mock_popen): + """Test test_teams_connection when an exception occurs""" + mock_process = MagicMock() + mock_popen.return_value = mock_process + credentials = M365Credentials(user="test@example.com", passwd="test_password") + identity = M365IdentityInfo( + identity_id="test_id", + identity_type="Application", + tenant_id="test_tenant", + tenant_domain="example.com", + tenant_domains=["example.com"], + location="test_location", + ) + session = M365PowerShell(credentials, identity) + + # Mock execute to raise an exception + session.execute = MagicMock(side_effect=Exception("Teams API error")) + + with pytest.raises(M365TeamsConnectionError) as exc_info: + session.test_teams_connection() + + assert "Failed to connect to Microsoft Teams API: Teams API error" in str( + exc_info.value + ) + session.close() + + @patch("subprocess.Popen") + def test_test_exchange_connection_success(self, mock_popen): + """Test test_exchange_connection when token is valid""" + mock_process = MagicMock() + mock_popen.return_value = mock_process + credentials = M365Credentials(user="test@example.com", passwd="test_password") + identity = M365IdentityInfo( + identity_id="test_id", + identity_type="Application", + tenant_id="test_tenant", + tenant_domain="example.com", + tenant_domains=["example.com"], + location="test_location", + ) + session = M365PowerShell(credentials, identity) + + # Mock execute to return valid responses + def mock_execute(command, *args, **kwargs): + if "Write-Output $exchangeToken" in command: + return "valid_exchange_token" + return None + + session.execute = MagicMock(side_effect=mock_execute) + + result = session.test_exchange_connection() + + assert result is True + # Verify all expected PowerShell commands were called + assert session.execute.call_count == 3 + session.close() + + @patch("subprocess.Popen") + def test_test_exchange_connection_empty_token(self, mock_popen): + """Test test_exchange_connection when token is empty""" + mock_process = MagicMock() + mock_popen.return_value = mock_process + credentials = M365Credentials(user="test@example.com", passwd="test_password") + identity = M365IdentityInfo( + identity_id="test_id", + identity_type="Application", + tenant_id="test_tenant", + tenant_domain="example.com", + tenant_domains=["example.com"], + location="test_location", + ) + session = M365PowerShell(credentials, identity) + + # Mock execute to return empty token when checking + def mock_execute(command, *args, **kwargs): + if "Write-Output $exchangeToken" in command: + return "" + return None + + session.execute = MagicMock(side_effect=mock_execute) + + with pytest.raises(M365ExchangeConnectionError) as exc_info: + session.test_exchange_connection() + + assert "Exchange Online token is empty or invalid" in str(exc_info.value) + session.close() + + @patch("subprocess.Popen") + def test_test_exchange_connection_exception(self, mock_popen): + """Test test_exchange_connection when an exception occurs""" + mock_process = MagicMock() + mock_popen.return_value = mock_process + credentials = M365Credentials(user="test@example.com", passwd="test_password") + identity = M365IdentityInfo( + identity_id="test_id", + identity_type="Application", + tenant_id="test_tenant", + tenant_domain="example.com", + tenant_domains=["example.com"], + location="test_location", + ) + session = M365PowerShell(credentials, identity) + + # Mock execute to raise an exception + session.execute = MagicMock(side_effect=Exception("Exchange API error")) + + with pytest.raises(M365ExchangeConnectionError) as exc_info: + session.test_exchange_connection() + + assert "Failed to connect to Exchange Online API: Exchange API error" in str( + exc_info.value + ) + session.close() + @patch("subprocess.Popen") def test_encrypt_password(self, mock_popen): credentials = M365Credentials(user="test@example.com", passwd="test_password") diff --git a/tests/providers/m365/m365_provider_test.py b/tests/providers/m365/m365_provider_test.py index 457173dc44..0f629d21a9 100644 --- a/tests/providers/m365/m365_provider_test.py +++ b/tests/providers/m365/m365_provider_test.py @@ -76,6 +76,16 @@ class TestM365Provider: location=LOCATION, ), ), + patch( + "prowler.providers.m365.m365_provider.M365Provider.setup_powershell", + return_value=M365Credentials( + client_id=CLIENT_ID, + tenant_id=TENANT_ID, + client_secret=CLIENT_SECRET, + user="", + passwd="", + ), + ), ): m365_provider = M365Provider( sp_env_auth=True, @@ -490,27 +500,6 @@ class TestM365Provider: assert result.user == credentials_dict["user"] assert result.passwd == credentials_dict["password"] - def test_setup_powershell_invalid_env_credentials(self): - credentials = None - - with patch( - "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell" - ) as mock_powershell: - mock_session = MagicMock() - mock_session.test_credentials.return_value = False - mock_powershell.return_value = mock_session - - with pytest.raises(M365MissingEnvironmentCredentialsError) as exc_info: - M365Provider.setup_powershell( - env_auth=True, m365_credentials=credentials - ) - - assert ( - "Missing M365_USER or M365_PASSWORD environment variables required for credentials authentication" - in str(exc_info.value) - ) - mock_session.test_credentials.assert_not_called() - def test_test_connection_user_not_belonging_to_tenant( self, ): @@ -572,28 +561,6 @@ class TestM365Provider: ) assert "The provided Client Secret is not valid." in str(exception.value) - def test_validate_static_credentials_missing_user(self): - with pytest.raises(M365NotValidUserError) as exception: - M365Provider.validate_static_credentials( - tenant_id="12345678-1234-5678-1234-567812345678", - client_id="12345678-1234-5678-1234-567812345678", - client_secret="test_secret", - user="", - password="test_password", - ) - assert "The provided User is not valid." in str(exception.value) - - def test_validate_static_credentials_missing_password(self): - with pytest.raises(M365NotValidPasswordError) as exception: - M365Provider.validate_static_credentials( - tenant_id="12345678-1234-5678-1234-567812345678", - client_id="12345678-1234-5678-1234-567812345678", - client_secret="test_secret", - user="test@example.com", - password="", - ) - assert "The provided Password is not valid." in str(exception.value) - def test_validate_arguments_missing_env_credentials(self): with pytest.raises(M365MissingEnvironmentCredentialsError) as exception: M365Provider.validate_arguments( diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index 73fa758d8c..e98629166a 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -2,7 +2,40 @@ All notable changes to the **Prowler UI** are documented in this file. -## [v1.8.0] (Prowler v5.8.0) – Not released +## [v1.9.0] (Prowler v5.9.0) – UNRELEASED + +### 🚀 Added + +- Mutelist configuration form [(#8190)](https://github.com/prowler-cloud/prowler/pull/8190) +- SAML login integration [(#8203)](https://github.com/prowler-cloud/prowler/pull/8203) + +### Security + +- Enhanced password validation to enforce 12+ character passwords with special characters, uppercase, lowercase, and numbers [(#8225)](https://github.com/prowler-cloud/prowler/pull/8225) + +### 🔄 Changed + +- Upgrade to Next.js 14.2.30 and lock TypeScript to 5.5.4 for ESLint compatibility [(#8189)](https://github.com/prowler-cloud/prowler/pull/8189) + +### 🐞 Fixed + +### Removed + +--- + +## [v1.8.1] (Prowler 5.8.1) + +### 🔄 Changed + +- Latest new failed findings now use `GET /findings/latest` [(#8219)](https://github.com/prowler-cloud/prowler/pull/8219) + +### Removed + +- Validation of the provider's secret type during updates [(#8197)](https://github.com/prowler-cloud/prowler/pull/8197) + +--- + +## [v1.8.0] (Prowler v5.8.0) ### 🚀 Added @@ -15,10 +48,10 @@ All notable changes to the **Prowler UI** are documented in this file. - Compliance detail view: CIS [(#7913)](https://github.com/prowler-cloud/prowler/pull/7913) - Compliance detail view: AWS Well-Architected Framework [(#7925)](https://github.com/prowler-cloud/prowler/pull/7925) - Compliance detail view: KISA [(#7965)](https://github.com/prowler-cloud/prowler/pull/7965) -- Compliance detail view: ProwlerThreatScore [(#7966)](https://github.com/prowler-cloud/prowler/pull/7966) +- Compliance detail view: ProwlerThreatScore [(#7979)](https://github.com/prowler-cloud/prowler/pull/7979) - Compliance detail view: Generic (rest of the compliances) [(#7990)](https://github.com/prowler-cloud/prowler/pull/7990) - Compliance detail view: MITRE ATTACK [(#8002)](https://github.com/prowler-cloud/prowler/pull/8002) -- Improve `Scan ID` filter by adding more context and enhancing the UI/UX [(#7979)](https://github.com/prowler-cloud/prowler/pull/7979) +- Improve `Scan ID` filter by adding more context and enhancing the UI/UX [(#8046)](https://github.com/prowler-cloud/prowler/pull/8046) - Lighthouse chat interface [(#7878)](https://github.com/prowler-cloud/prowler/pull/7878) - Google Tag Manager integration [(#8058)](https://github.com/prowler-cloud/prowler/pull/8058) @@ -28,13 +61,18 @@ All notable changes to the **Prowler UI** are documented in this file. - Aligned Next.js version to `v14.2.29` across Prowler and Cloud environments for consistency and improved maintainability [(#7962)](https://github.com/prowler-cloud/prowler/pull/7962) - Refactor credentials forms with reusable components and error handling [(#7988)](https://github.com/prowler-cloud/prowler/pull/7988) - Updated the provider details section in Scan and Findings detail pages [(#7968)](https://github.com/prowler-cloud/prowler/pull/7968) +- Make user and password fields optional but mutually required for M365 cloud provider [(#8044)](https://github.com/prowler-cloud/prowler/pull/8044) - Improve filter behaviour and relationships between filters in findings page [(#8046)](https://github.com/prowler-cloud/prowler/pull/8046) +- Set filters panel to be always open by default [(#8085)](https://github.com/prowler-cloud/prowler/pull/8085) +- Updated "Sign in"/"Sign up" capitalization for consistency [(#8136)](https://github.com/prowler-cloud/prowler/pull/8136) +- Duplicate API base URL as an env var to make it accessible in client components [(#8131)](https://github.com/prowler-cloud/prowler/pull/8131) ### 🐞 Fixed - Sync between filter buttons and URL when filters change [(#7928)](https://github.com/prowler-cloud/prowler/pull/7928) - Improve heatmap perfomance [(#7934)](https://github.com/prowler-cloud/prowler/pull/7934) - SelectScanProvider warning fixed with empty alias [(#7998)](https://github.com/prowler-cloud/prowler/pull/7998) +- Prevent console warnings for accessibility and SVG[(#8019)](https://github.com/prowler-cloud/prowler/pull/8019) --- diff --git a/ui/Dockerfile b/ui/Dockerfile index a4c4d793f5..da920e52fe 100644 --- a/ui/Dockerfile +++ b/ui/Dockerfile @@ -30,6 +30,8 @@ ARG NEXT_PUBLIC_PROWLER_RELEASE_VERSION ENV NEXT_PUBLIC_PROWLER_RELEASE_VERSION=${NEXT_PUBLIC_PROWLER_RELEASE_VERSION} ARG NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID ENV NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID=${NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID} +ARG NEXT_PUBLIC_API_BASE_URL +ENV NEXT_PUBLIC_API_BASE_URL=${NEXT_PUBLIC_API_BASE_URL} RUN \ if [ -f package-lock.json ]; then npm run build; \ diff --git a/ui/actions/integrations/index.ts b/ui/actions/integrations/index.ts new file mode 100644 index 0000000000..466fc73cb2 --- /dev/null +++ b/ui/actions/integrations/index.ts @@ -0,0 +1 @@ +export * from "./saml"; diff --git a/ui/actions/integrations/saml.ts b/ui/actions/integrations/saml.ts new file mode 100644 index 0000000000..c5bc6d18bb --- /dev/null +++ b/ui/actions/integrations/saml.ts @@ -0,0 +1,231 @@ +"use server"; + +import { revalidatePath } from "next/cache"; + +import { apiBaseUrl, getAuthHeaders, parseStringify } from "@/lib/helper"; +import { samlConfigFormSchema } from "@/types/formSchemas"; + +export const createSamlConfig = async (_prevState: any, formData: FormData) => { + const headers = await getAuthHeaders({ contentType: true }); + const formDataObject = Object.fromEntries(formData); + const validatedData = samlConfigFormSchema.safeParse(formDataObject); + + if (!validatedData.success) { + const formFieldErrors = validatedData.error.flatten().fieldErrors; + + return { + errors: { + email_domain: formFieldErrors?.email_domain?.[0], + metadata_xml: formFieldErrors?.metadata_xml?.[0], + }, + }; + } + + const { email_domain, metadata_xml } = validatedData.data; + + try { + const url = new URL(`${apiBaseUrl}/saml-config`); + const response = await fetch(url.toString(), { + method: "POST", + headers, + body: JSON.stringify({ + data: { + type: "saml-configurations", + attributes: { + email_domain: email_domain.trim(), + metadata_xml: metadata_xml.trim(), + }, + }, + }), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error( + errorData.errors?.[0]?.detail || + `Failed to create SAML config: ${response.statusText}`, + ); + } + + await response.json(); + revalidatePath("/integrations"); + return { success: "SAML configuration created successfully!" }; + } catch (error) { + console.error("Error creating SAML config:", error); + return { + errors: { + general: + error instanceof Error + ? error.message + : "Error creating SAML configuration. Please try again.", + }, + }; + } +}; + +export const updateSamlConfig = async (_prevState: any, formData: FormData) => { + const headers = await getAuthHeaders({ contentType: true }); + const formDataObject = Object.fromEntries(formData); + const validatedData = samlConfigFormSchema.safeParse(formDataObject); + + if (!validatedData.success) { + const formFieldErrors = validatedData.error.flatten().fieldErrors; + + return { + errors: { + email_domain: formFieldErrors?.email_domain?.[0], + metadata_xml: formFieldErrors?.metadata_xml?.[0], + }, + }; + } + + const { email_domain, metadata_xml } = validatedData.data; + + try { + const url = new URL(`${apiBaseUrl}/saml-config/${formDataObject.id}`); + const response = await fetch(url.toString(), { + method: "PATCH", + headers, + body: JSON.stringify({ + data: { + type: "saml-configurations", + id: formDataObject.id, + attributes: { + email_domain: email_domain.trim(), + metadata_xml: metadata_xml.trim(), + }, + }, + }), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error( + errorData.errors?.[0]?.detail || + `Failed to update SAML config: ${response.statusText}`, + ); + } + + await response.json(); + revalidatePath("/integrations"); + return { success: "SAML configuration updated successfully!" }; + } catch (error) { + console.error("Error updating SAML config:", error); + return { + errors: { + general: + error instanceof Error + ? error.message + : "Error creating SAML configuration. Please try again.", + }, + }; + } +}; + +export const getSamlConfig = async () => { + const headers = await getAuthHeaders({ contentType: false }); + const url = new URL(`${apiBaseUrl}/saml-config`); + + try { + const response = await fetch(url.toString(), { + method: "GET", + headers, + }); + + if (!response.ok) { + throw new Error(`Failed to fetch SAML config: ${response.statusText}`); + } + + const data = await response.json(); + const parsedData = parseStringify(data); + return parsedData; + } catch (error) { + console.error("Error fetching SAML config:", error); + return undefined; + } +}; + +export const deleteSamlConfig = async (id: string) => { + const headers = await getAuthHeaders({ contentType: true }); + + try { + const url = new URL(`${apiBaseUrl}/saml-config/${id}`); + const response = await fetch(url.toString(), { + method: "DELETE", + headers, + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error( + errorData.errors?.[0]?.detail || + `Failed to delete SAML config: ${response.statusText}`, + ); + } + + revalidatePath("/integrations"); + return { success: "SAML configuration deleted successfully!" }; + } catch (error) { + console.error("Error deleting SAML config:", error); + return { + errors: { + general: + error instanceof Error + ? error.message + : "Error deleting SAML configuration. Please try again.", + }, + }; + } +}; + +export const initiateSamlAuth = async (email: string) => { + try { + const response = await fetch(`${apiBaseUrl}/auth/saml/initiate/`, { + method: "POST", + headers: { + "Content-Type": "application/vnd.api+json", + }, + body: JSON.stringify({ + data: { + type: "saml-initiate", + attributes: { + email_domain: email, + }, + }, + }), + redirect: "manual", + }); + + if (response.status === 302) { + const location = response.headers.get("Location"); + + if (location) { + return { + success: true, + redirectUrl: location, + }; + } + } + + if (response.status === 403) { + return { + success: false, + error: "Domain is not authorized for SAML authentication.", + }; + } + + // Add error other error case: + const errorData = await response.json().catch(() => ({})); + return { + success: false, + error: + errorData.errors?.[0]?.detail || + "An error occurred during SAML authentication.", + }; + } catch (error) { + return { + success: false, + error: "Failed to connect to authentication service.", + }; + } +}; diff --git a/ui/actions/overview/overview.ts b/ui/actions/overview/overview.ts index 3a95a2a3f3..363a2b9df3 100644 --- a/ui/actions/overview/overview.ts +++ b/ui/actions/overview/overview.ts @@ -59,9 +59,9 @@ export const getFindingsByStatus = async ({ if (query) url.searchParams.append("filter[search]", query); if (sort) url.searchParams.append("sort", sort); - // Handle multiple filters + // Handle multiple filters, but exclude muted filter as overviews endpoint doesn't support it Object.entries(filters).forEach(([key, value]) => { - if (key !== "filter[search]") { + if (key !== "filter[search]" && key !== "filter[muted]") { url.searchParams.append(key, String(value)); } }); @@ -102,9 +102,9 @@ export const getFindingsBySeverity = async ({ if (query) url.searchParams.append("filter[search]", query); if (sort) url.searchParams.append("sort", sort); - // Handle multiple filters + // Handle multiple filters, but exclude muted filter as overviews endpoint doesn't support it Object.entries(filters).forEach(([key, value]) => { - if (key !== "filter[search]") { + if (key !== "filter[search]" && key !== "filter[muted]") { url.searchParams.append(key, String(value)); } }); diff --git a/ui/actions/processors/index.ts b/ui/actions/processors/index.ts new file mode 100644 index 0000000000..3bd4e78191 --- /dev/null +++ b/ui/actions/processors/index.ts @@ -0,0 +1 @@ +export * from "./processors"; diff --git a/ui/actions/processors/processors.ts b/ui/actions/processors/processors.ts new file mode 100644 index 0000000000..f177dc9ac4 --- /dev/null +++ b/ui/actions/processors/processors.ts @@ -0,0 +1,225 @@ +"use server"; + +import { apiBaseUrl, getAuthHeaders } from "@/lib/helper"; +import { mutedFindingsConfigFormSchema } from "@/types/formSchemas"; +import { + DeleteMutedFindingsConfigActionState, + MutedFindingsConfigActionState, + ProcessorData, +} from "@/types/processors"; + +export const createMutedFindingsConfig = async ( + _prevState: MutedFindingsConfigActionState, + formData: FormData, +): Promise => { + const headers = await getAuthHeaders({ contentType: true }); + const formDataObject = Object.fromEntries(formData); + const validatedData = mutedFindingsConfigFormSchema.safeParse(formDataObject); + + if (!validatedData.success) { + const formFieldErrors = validatedData.error.flatten().fieldErrors; + return { + errors: { + configuration: formFieldErrors?.configuration?.[0], + }, + }; + } + + const { configuration } = validatedData.data; + + try { + const url = new URL(`${apiBaseUrl}/processors`); + + const bodyData = { + data: { + type: "processors", + attributes: { + processor_type: "mutelist", + configuration: configuration, + }, + }, + }; + + const response = await fetch(url.toString(), { + method: "POST", + headers, + body: JSON.stringify(bodyData), + }); + + if (!response.ok) { + try { + const errorData = await response.json(); + throw new Error( + errorData?.errors?.[0]?.detail || + errorData?.message || + `Failed to create Mutelist configuration: ${response.statusText}`, + ); + } catch { + throw new Error( + `Failed to create Mutelist configuration: ${response.statusText}`, + ); + } + } + + await response.json(); + return { success: "Mutelist configuration created successfully!" }; + } catch (error) { + console.error("Error creating Mutelist config:", error); + return { + errors: { + configuration: + error instanceof Error + ? error.message + : "Error creating Mutelist configuration. Please try again.", + }, + }; + } +}; + +export const updateMutedFindingsConfig = async ( + _prevState: MutedFindingsConfigActionState, + formData: FormData, +): Promise => { + const headers = await getAuthHeaders({ contentType: true }); + const formDataObject = Object.fromEntries(formData); + const validatedData = mutedFindingsConfigFormSchema.safeParse(formDataObject); + + if (!validatedData.success) { + const formFieldErrors = validatedData.error.flatten().fieldErrors; + return { + errors: { + configuration: formFieldErrors?.configuration?.[0], + }, + }; + } + + const { configuration, id } = validatedData.data; + + if (!id) { + return { + errors: { + general: "Configuration ID is required for update", + }, + }; + } + + try { + const url = new URL(`${apiBaseUrl}/processors/${id}`); + + const bodyData = { + data: { + type: "processors", + id, + attributes: { + configuration: configuration, + }, + }, + }; + + const response = await fetch(url.toString(), { + method: "PATCH", + headers, + body: JSON.stringify(bodyData), + }); + + if (!response.ok) { + try { + const errorData = await response.json(); + throw new Error( + errorData?.errors?.[0]?.detail || + errorData?.message || + `Failed to update Mutelist configuration: ${response.statusText}`, + ); + } catch { + throw new Error( + `Failed to update Mutelist configuration: ${response.statusText}`, + ); + } + } + + await response.json(); + return { success: "Mutelist configuration updated successfully!" }; + } catch (error) { + console.error("Error updating Mutelist config:", error); + return { + errors: { + configuration: + error instanceof Error + ? error.message + : "Error updating Mutelist configuration. Please try again.", + }, + }; + } +}; + +export const getMutedFindingsConfig = async (): Promise< + ProcessorData | undefined +> => { + const headers = await getAuthHeaders({ contentType: false }); + const url = new URL(`${apiBaseUrl}/processors`); + url.searchParams.append("filter[processor_type]", "mutelist"); + + try { + const response = await fetch(url.toString(), { + method: "GET", + headers, + }); + + if (!response.ok) { + throw new Error( + `Failed to fetch Mutelist config: ${response.statusText}`, + ); + } + + const data = await response.json(); + return data.data[0]; + } catch (error) { + console.error("Error fetching Mutelist config:", error); + return undefined; + } +}; + +export const deleteMutedFindingsConfig = async ( + _prevState: DeleteMutedFindingsConfigActionState, + formData: FormData, +): Promise => { + const headers = await getAuthHeaders({ contentType: true }); + const formDataObject = Object.fromEntries(formData); + const processorId = formDataObject.id as string; + + if (!processorId) { + return { + errors: { + general: "Configuration ID is required for deletion", + }, + }; + } + + try { + const url = new URL(`${apiBaseUrl}/processors/${processorId}`); + const response = await fetch(url.toString(), { + method: "DELETE", + headers, + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error( + errorData.errors?.[0]?.detail || + `Failed to delete Mutelist configuration: ${response.statusText}`, + ); + } + + return { success: "Mutelist configuration deleted successfully!" }; + } catch (error) { + console.error("Error deleting Mutelist config:", error); + return { + errors: { + general: + error instanceof Error + ? error.message + : "Error deleting Mutelist configuration. Please try again.", + }, + }; + } +}; diff --git a/ui/actions/providers/providers.ts b/ui/actions/providers/providers.ts index 922041d487..7a38b8875f 100644 --- a/ui/actions/providers/providers.ts +++ b/ui/actions/providers/providers.ts @@ -13,7 +13,6 @@ import { } from "@/lib"; import { buildSecretConfig, - buildUpdateSecretConfig, handleApiError, handleApiResponse, } from "@/lib/provider-credentials/build-crendentials"; @@ -194,8 +193,7 @@ export const updateCredentialsProvider = async ( ) as ProviderType; try { - const secret = buildUpdateSecretConfig(formData, providerType); - + const { secretType, secret } = buildSecretConfig(formData, providerType); const response = await fetch(url.toString(), { method: "PATCH", headers, @@ -203,7 +201,7 @@ export const updateCredentialsProvider = async ( data: { type: "provider-secrets", id: credentialsId, - attributes: { secret }, + attributes: { secret_type: secretType, secret }, }, }), }); diff --git a/ui/actions/scans/scans.ts b/ui/actions/scans/scans.ts index b3115ea0e9..6d37fed0d7 100644 --- a/ui/actions/scans/scans.ts +++ b/ui/actions/scans/scans.ts @@ -16,6 +16,8 @@ export const getScans = async ({ sort = "", filters = {}, pageSize = 10, + fields = {}, + include = "", }) => { const headers = await getAuthHeaders({ contentType: false }); @@ -27,6 +29,12 @@ export const getScans = async ({ if (pageSize) url.searchParams.append("page[size]", pageSize.toString()); if (query) url.searchParams.append("filter[search]", query); if (sort) url.searchParams.append("sort", sort); + if (include) url.searchParams.append("include", include); + + // Handle fields parameters + Object.entries(fields).forEach(([key, value]) => { + url.searchParams.append(`fields[${key}]`, String(value)); + }); // Handle multiple filters Object.entries(filters).forEach(([key, value]) => { diff --git a/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx b/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx index 17636db9d4..93e1c48842 100644 --- a/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx +++ b/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx @@ -115,7 +115,9 @@ export default async function ComplianceDetail({ > {selectedScanId && selectedScan && (
- +
+ +
)} diff --git a/ui/app/(prowler)/compliance/page.tsx b/ui/app/(prowler)/compliance/page.tsx index 6a8326f334..5a1e7fcca9 100644 --- a/ui/app/(prowler)/compliance/page.tsx +++ b/ui/app/(prowler)/compliance/page.tsx @@ -3,7 +3,6 @@ import { Suspense } from "react"; import { getCompliancesOverview } from "@/actions/compliances"; import { getComplianceOverviewMetadataInfo } from "@/actions/compliances"; -import { getProvider } from "@/actions/providers"; import { getScans } from "@/actions/scans"; import { ComplianceCard, @@ -36,34 +35,41 @@ export default async function Compliance({ "filter[state]": "completed", }, pageSize: 50, + fields: { + scans: "name,completed_at,provider", + }, + include: "provider", }); if (!scansData?.data) { return ; } - // Expand scans with provider information - only include scans with valid provider - const expandedScansData: ExpandedScanData[] = await Promise.all( - scansData.data - .filter((scan: ScanProps) => scan.relationships?.provider?.data?.id) - .map(async (scan: ScanProps) => { - const providerId = scan.relationships!.provider!.data!.id; + // Process scans with provider information from included data + const expandedScansData: ExpandedScanData[] = scansData.data + .filter((scan: ScanProps) => scan.relationships?.provider?.data?.id) + .map((scan: ScanProps) => { + const providerId = scan.relationships!.provider!.data!.id; - const formData = new FormData(); - formData.append("id", providerId); + // Find the provider data in the included array + const providerData = scansData.included?.find( + (item: any) => item.type === "providers" && item.id === providerId, + ); - const providerData = await getProvider(formData); + if (!providerData) { + return null; + } - return { - ...scan, - providerInfo: { - provider: providerData.data.attributes.provider, - uid: providerData.data.attributes.uid, - alias: providerData.data.attributes.alias, - }, - }; - }), - ); + return { + ...scan, + providerInfo: { + provider: providerData.attributes.provider, + uid: providerData.attributes.uid, + alias: providerData.attributes.alias, + }, + }; + }) + .filter(Boolean) as ExpandedScanData[]; const selectedScanId = searchParams.scanId || expandedScansData[0]?.id || null; @@ -140,7 +146,7 @@ const SSRComplianceGrid = async ({ query, }); - const type = compliancesData?.data?.[0]?.type; + const type = compliancesData?.data?.type; // Check if the response contains no data if ( diff --git a/ui/app/(prowler)/lighthouse/page.tsx b/ui/app/(prowler)/lighthouse/page.tsx index 089504a78d..f283237f2f 100644 --- a/ui/app/(prowler)/lighthouse/page.tsx +++ b/ui/app/(prowler)/lighthouse/page.tsx @@ -9,7 +9,7 @@ export default async function AIChatbot() { const isActive = config?.attributes?.is_active ?? false; return ( - + ); diff --git a/ui/app/(prowler)/page.tsx b/ui/app/(prowler)/page.tsx index 7c6f2ac906..a061e04f96 100644 --- a/ui/app/(prowler)/page.tsx +++ b/ui/app/(prowler)/page.tsx @@ -1,8 +1,7 @@ import { Spacer } from "@nextui-org/react"; -import { format, subDays } from "date-fns"; import { Suspense } from "react"; -import { getFindings } from "@/actions/findings/findings"; +import { getLatestFindings } from "@/actions/findings/findings"; import { getFindingsBySeverity, getFindingsByStatus, @@ -33,7 +32,7 @@ export default function Home({ const searchParamsKey = JSON.stringify(searchParams || {}); return ( - +
@@ -60,7 +59,7 @@ export default function Home({ key={searchParamsKey} fallback={} > - +
@@ -125,23 +124,34 @@ const SSRFindingsBySeverity = async ({ ); }; -const SSRDataNewFindingsTable = async () => { +const SSRDataNewFindingsTable = async ({ + searchParams, +}: { + searchParams: SearchParamsProps | undefined | null; +}) => { const page = 1; const sort = "severity,-inserted_at"; - const twoDaysAgo = format(subDays(new Date(), 2), "yyyy-MM-dd"); - const defaultFilters = { - "filter[status__in]": "FAIL", - "filter[delta__in]": "new", - "filter[inserted_at__gte]": twoDaysAgo, + "filter[status]": "FAIL", + "filter[delta]": "new", }; - const findingsData = await getFindings({ + const filters = searchParams + ? Object.fromEntries( + Object.entries(searchParams).filter(([key]) => + key.startsWith("filter["), + ), + ) + : {}; + + const combinedFilters = { ...defaultFilters, ...filters }; + + const findingsData = await getLatestFindings({ query: undefined, page, sort, - filters: defaultFilters, + filters: combinedFilters, }); // Create dictionaries for resources, scans, and providers @@ -178,8 +188,7 @@ const SSRDataNewFindingsTable = async () => { Latest new failing findings

- Showing the latest 10 new failing findings by severity from the last - 2 days. + Showing the latest 10 new failing findings by severity.

diff --git a/ui/app/(prowler)/profile/page.tsx b/ui/app/(prowler)/profile/page.tsx index ff26a9db15..74108355f9 100644 --- a/ui/app/(prowler)/profile/page.tsx +++ b/ui/app/(prowler)/profile/page.tsx @@ -1,8 +1,10 @@ import React, { Suspense } from "react"; +import { getSamlConfig } from "@/actions/integrations/saml"; import { getAllTenants } from "@/actions/users/tenants"; import { getUserInfo } from "@/actions/users/users"; import { getUserMemberships } from "@/actions/users/users"; +import { SamlIntegrationCard } from "@/components/integrations/saml-integration-card"; import { ContentLayout } from "@/components/ui"; import { UserBasicInfoCard } from "@/components/users/profile"; import { MembershipsCard } from "@/components/users/profile/memberships-card"; @@ -22,6 +24,7 @@ export default async function Profile() { } const SSRDataUser = async () => { + const samlConfig = await getSamlConfig(); const userProfile = await getUserInfo(); if (!userProfile?.data) { return null; @@ -69,11 +72,11 @@ const SSRDataUser = async () => { return (
-
-
+
+
-
+
{ />
+
+ +
); }; diff --git a/ui/app/(prowler)/providers/page.tsx b/ui/app/(prowler)/providers/page.tsx index ff78664ca9..435b8d73b3 100644 --- a/ui/app/(prowler)/providers/page.tsx +++ b/ui/app/(prowler)/providers/page.tsx @@ -4,7 +4,10 @@ import { Suspense } from "react"; import { getProviders } from "@/actions/providers"; import { FilterControls, filterProviders } from "@/components/filters"; import { ManageGroupsButton } from "@/components/manage-groups"; -import { AddProviderButton } from "@/components/providers"; +import { + AddProviderButton, + MutedFindingsConfigButton, +} from "@/components/providers"; import { ColumnProviders, SkeletonTableProviders, @@ -24,24 +27,31 @@ export default async function Providers({ -
- - -
- - -
-
- }> - - -
-
+ +
+ + + +
+ +
+
+ +
+
+ + } + > + +
); } -const SSRDataTable = async ({ +const ProvidersContent = async ({ searchParams, }: { searchParams: SearchParamsProps; @@ -66,6 +76,8 @@ const SSRDataTable = async ({ pageSize, }); + const hasProviders = providersData?.data && providersData.data.length > 0; + const providerGroupDict = providersData?.included ?.filter((item: any) => item.type === "provider-groups") @@ -85,10 +97,23 @@ const SSRDataTable = async ({ }) || []; return ( - + <> +
+ + + +
+ + +
+
+ +
+
+ ); }; diff --git a/ui/app/(prowler)/scans/page.tsx b/ui/app/(prowler)/scans/page.tsx index 9a6214bf59..dda91b1747 100644 --- a/ui/app/(prowler)/scans/page.tsx +++ b/ui/app/(prowler)/scans/page.tsx @@ -1,8 +1,9 @@ import { Spacer } from "@nextui-org/react"; import { Suspense } from "react"; -import { getProvider, getProviders } from "@/actions/providers"; +import { getProviders } from "@/actions/providers"; import { getScans, getScansByState } from "@/actions/scans"; +import { MutedFindingsConfigButton } from "@/components/providers"; import { AutoRefresh, NoProvidersAdded, @@ -45,13 +46,9 @@ export default async function Scans({ connected: provider.attributes.connection.connected, })) || []; - const providersCountConnected = await getProviders({ - filters: { "filter[connected]": true }, - pageSize: 50, - }); - const thereIsNoProviders = !providersCountConnected?.data; + const thereIsNoProviders = !providersData?.data; - const thereIsNoProvidersConnected = providersCountConnected?.data?.every( + const thereIsNoProvidersConnected = providersData?.data?.every( (provider: ProviderProps) => !provider.attributes.connection.connected, ); @@ -96,6 +93,10 @@ export default async function Scans({ providerDetails={providerDetails} /> +
+ +
+ }> @@ -123,34 +124,43 @@ const SSRDataTableScans = async ({ // Extract query from filters const query = (filters["filter[search]"] as string) || ""; - // Fetch scans data - const scansData = await getScans({ query, page, sort, filters, pageSize }); + // Fetch scans data with provider information included + const scansData = await getScans({ + query, + page, + sort, + filters, + pageSize, + include: "provider", + }); - // Handle expanded scans data - const expandedScansData = await Promise.all( - scansData?.data?.map(async (scan: any) => { + // Process scans with provider information from included data + const expandedScansData = + scansData?.data?.map((scan: any) => { const providerId = scan.relationships?.provider?.data?.id; if (!providerId) { return { ...scan, providerInfo: null }; } - const formData = new FormData(); - formData.append("id", providerId); + // Find the provider data in the included array + const providerData = scansData.included?.find( + (item: any) => item.type === "providers" && item.id === providerId, + ); - const providerData = await getProvider(formData); - - if (providerData?.data) { - const { provider, uid, alias } = providerData.data.attributes; - return { - ...scan, - providerInfo: { provider, uid, alias }, - }; + if (!providerData) { + return { ...scan, providerInfo: null }; } - return { ...scan, providerInfo: null }; - }) || [], - ); + return { + ...scan, + providerInfo: { + provider: providerData.attributes.provider, + uid: providerData.attributes.uid, + alias: providerData.attributes.alias, + }, + }; + }) || []; return ( ) => { if (type === "sign-in") { + if (data.isSamlMode) { + const email = data.email.toLowerCase(); + if (isSamlMode) { + form.setValue("password", ""); + } + + const result = await initiateSamlAuth(email); + + if (result.success && result.redirectUrl) { + window.location.href = result.redirectUrl; + } else { + toast({ + variant: "destructive", + title: "SAML Authentication Error", + description: + result.error || "An error occurred during SAML authentication.", + }); + } + return; + } + const result = await authenticate(null, { email: data.email.toLowerCase(), password: data.password, @@ -150,7 +175,11 @@ export const AuthForm = ({

- {type === "sign-in" ? "Sign In" : "Sign Up"} + {type === "sign-in" + ? isSamlMode + ? "Sign in with SAML SSO" + : "Sign in" + : "Sign up"}

@@ -181,7 +210,6 @@ export const AuthForm = ({ /> )} - - - - + {!isSamlMode && ( + <> + + {type === "sign-up" && ( + + )} + + )} {/* {type === "sign-in" && (
@@ -265,17 +300,15 @@ export const AuthForm = ({ )} )} - {type === "sign-in" && form.formState.errors?.email && (

Invalid email or password

)} - Loading ) : ( - {type === "sign-in" ? "Log In" : "Sign Up"} + {type === "sign-in" ? "Log in" : "Sign up"} )} - {!invitationToken && ( + {!invitationToken && type === "sign-in" && ( <>
@@ -302,88 +335,110 @@ export const AuthForm = ({
- - Social Login with Google is not enabled.{" "} - - Read the docs - -
- } - placement="right-start" - shadow="sm" - isDisabled={isGoogleOAuthEnabled} - className="w-96" - > - -
} - variant="bordered" - className="w-full" - as="a" - href={googleAuthUrl} - isDisabled={!isGoogleOAuthEnabled} + placement="right-start" + shadow="sm" + isDisabled={isGoogleOAuthEnabled} + className="w-96" > - Continue with Google - - - - - Social Login with Github is not enabled.{" "} - - Read the docs - -
- } - placement="right-start" - shadow="sm" - isDisabled={isGithubOAuthEnabled} - className="w-96" - > - - + + + + Social Login with Github is not enabled.{" "} + + Read the docs + +
} - variant="bordered" - className="w-full" - as="a" - href={githubAuthUrl} - isDisabled={!isGithubOAuthEnabled} + placement="right-start" + shadow="sm" + isDisabled={isGithubOAuthEnabled} + className="w-96" > - Continue with Github - - - + + + + + + )} +
)} {type === "sign-in" ? (

Need to create an account?  - Sign Up + Sign up

) : (

Already have an account?  - Log In + Log in

)} diff --git a/ui/components/auth/oss/password-validator.tsx b/ui/components/auth/oss/password-validator.tsx new file mode 100644 index 0000000000..e3ef2a2b1e --- /dev/null +++ b/ui/components/auth/oss/password-validator.tsx @@ -0,0 +1,121 @@ +"use client"; + +import { AlertCircle, CheckCircle } from "lucide-react"; + +import { + PASSWORD_REQUIREMENTS, + passwordRequirementCheckers, +} from "@/types/authFormSchema"; + +interface PasswordRequirementsMessageProps { + password: string; + className?: string; +} + +const REQUIREMENTS = [ + { + key: "minLength", + checker: passwordRequirementCheckers.minLength, + label: `At least ${PASSWORD_REQUIREMENTS.minLength} characters`, + }, + { + key: "specialChars", + checker: passwordRequirementCheckers.specialChars, + label: "Special characters", + }, + { + key: "uppercase", + checker: passwordRequirementCheckers.uppercase, + label: "Uppercase letters", + }, + { + key: "lowercase", + checker: passwordRequirementCheckers.lowercase, + label: "Lowercase letters", + }, + { + key: "numbers", + checker: passwordRequirementCheckers.numbers, + label: "Numbers", + }, +]; + +export const PasswordRequirementsMessage = ({ + password, + className = "", +}: PasswordRequirementsMessageProps) => { + const hasPasswordInput = password.length > 0; + if (!hasPasswordInput) { + return null; + } + const results = REQUIREMENTS.map((req) => ({ + ...req, + isMet: req.checker(password), + })); + const metCount = results.filter((r) => r.isMet).length; + const allRequirementsMet = metCount === REQUIREMENTS.length; + + return ( +
+
+ {allRequirementsMet ? ( +
+
+ ) : ( +
+
+
+
    + {results.map((req) => ( +
  • +
    + + + {req.isMet ? "Requirement met" : "Requirement not met"} + +
  • + ))} +
+
+ )} +
+
+ ); +}; diff --git a/ui/components/compliance/compliance-charts/bar-chart.tsx b/ui/components/compliance/compliance-charts/bar-chart.tsx index 2b0add1160..a556799091 100644 --- a/ui/components/compliance/compliance-charts/bar-chart.tsx +++ b/ui/components/compliance/compliance-charts/bar-chart.tsx @@ -44,7 +44,7 @@ interface FailedSectionsListProps { const title = (

- Failed Sections (Top 5) + Top Failed Sections

); diff --git a/ui/components/compliance/compliance-header/compliance-header.tsx b/ui/components/compliance/compliance-header/compliance-header.tsx index 428d9909a4..12dea88d92 100644 --- a/ui/components/compliance/compliance-header/compliance-header.tsx +++ b/ui/components/compliance/compliance-header/compliance-header.tsx @@ -64,9 +64,7 @@ export const ComplianceHeader = ({ )} - {allFilters.length > 0 && ( - - )} + {allFilters.length > 0 && } ); diff --git a/ui/components/filters/custom-checkbox-muted-findings.tsx b/ui/components/filters/custom-checkbox-muted-findings.tsx index 5edcaa1cad..05cae96522 100644 --- a/ui/components/filters/custom-checkbox-muted-findings.tsx +++ b/ui/components/filters/custom-checkbox-muted-findings.tsx @@ -1,18 +1,38 @@ +"use client"; + import { Checkbox } from "@nextui-org/react"; -import React from "react"; +import { useSearchParams } from "next/navigation"; +import { useState } from "react"; + +import { useUrlFilters } from "@/hooks/use-url-filters"; export const CustomCheckboxMutedFindings = () => { + const { updateFilter } = useUrlFilters(); + const searchParams = useSearchParams(); + const [excludeMuted, setExcludeMuted] = useState( + searchParams.get("filter[muted]") === "false", + ); + + const handleMutedChange = (value: boolean) => { + setExcludeMuted(value); + updateFilter("muted", value ? "false" : "true"); + }; + return ( - - Include Muted Findings - +
+ + Exclude muted findings + +
); }; diff --git a/ui/components/filters/filter-controls.tsx b/ui/components/filters/filter-controls.tsx index b460999807..36e55f003b 100644 --- a/ui/components/filters/filter-controls.tsx +++ b/ui/components/filters/filter-controls.tsx @@ -4,7 +4,7 @@ import { Spacer } from "@nextui-org/react"; import { useSearchParams } from "next/navigation"; import React, { useEffect, useState } from "react"; -import { FilterControlsProps } from "@/types"; +import { FilterOption } from "@/types"; import { DataTableFilterCustom } from "../ui/table"; import { ClearFiltersButton } from "./clear-filters-button"; @@ -15,6 +15,17 @@ import { CustomRegionSelection } from "./custom-region-selection"; import { CustomSearchInput } from "./custom-search-input"; import { CustomSelectProvider } from "./custom-select-provider"; +export interface FilterControlsProps { + search?: boolean; + providers?: boolean; + date?: boolean; + regions?: boolean; + accounts?: boolean; + mutedFindings?: boolean; + customFilters?: FilterOption[]; + showClearButton?: boolean; +} + export const FilterControls: React.FC = ({ search = false, providers = false, @@ -22,16 +33,17 @@ export const FilterControls: React.FC = ({ regions = false, accounts = false, mutedFindings = false, + showClearButton = true, customFilters, }) => { const searchParams = useSearchParams(); - const [showClearButton, setShowClearButton] = useState(false); + const [hasFilters, setHasFilters] = useState(false); useEffect(() => { const hasFilters = Array.from(searchParams.keys()).some( (key) => key.startsWith("filter[") || key === "sort", ); - setShowClearButton(hasFilters); + setHasFilters(hasFilters); }, [searchParams]); return ( @@ -43,7 +55,9 @@ export const FilterControls: React.FC = ({ {regions && } {accounts && } {mutedFindings && } - {!customFilters && showClearButton && } + {!customFilters && hasFilters && showClearButton && ( + + )} {customFilters && ( diff --git a/ui/components/findings/findings-filters.tsx b/ui/components/findings/findings-filters.tsx index 0dc025084b..d94d0c668d 100644 --- a/ui/components/findings/findings-filters.tsx +++ b/ui/components/findings/findings-filters.tsx @@ -38,6 +38,7 @@ export const FindingsFilters = ({ { +export const Muted = ({ + isMuted, + mutedReason = "This finding is muted", +}: MutedProps) => { if (isMuted === false) return null; return ( - +
diff --git a/ui/components/findings/table/column-findings.tsx b/ui/components/findings/table/column-findings.tsx index a8636ab750..51e3d4625d 100644 --- a/ui/components/findings/table/column-findings.tsx +++ b/ui/components/findings/table/column-findings.tsx @@ -103,7 +103,7 @@ export const ColumnFindings: ColumnDef[] = [ cell: ({ row }) => { const { checktitle } = getFindingsMetadata(row); const { - attributes: { muted }, + attributes: { muted, muted_reason }, } = getFindingsData(row); const { delta } = row.original.attributes; @@ -120,7 +120,7 @@ export const ColumnFindings: ColumnDef[] = [

- + ); diff --git a/ui/components/findings/table/finding-detail.tsx b/ui/components/findings/table/finding-detail.tsx index 92c7bcb9e1..a32eeaa207 100644 --- a/ui/components/findings/table/finding-detail.tsx +++ b/ui/components/findings/table/finding-detail.tsx @@ -67,7 +67,10 @@ export const FindingDetail = ({
- +
= ({ {...props} > ); diff --git a/ui/components/integrations/forms/index.ts b/ui/components/integrations/forms/index.ts new file mode 100644 index 0000000000..fc10a1a721 --- /dev/null +++ b/ui/components/integrations/forms/index.ts @@ -0,0 +1 @@ +export * from "./saml-config-form"; diff --git a/ui/components/integrations/forms/saml-config-form.tsx b/ui/components/integrations/forms/saml-config-form.tsx new file mode 100644 index 0000000000..ecea01343f --- /dev/null +++ b/ui/components/integrations/forms/saml-config-form.tsx @@ -0,0 +1,319 @@ +"use client"; + +import Link from "next/link"; +import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react"; +import { useFormState } from "react-dom"; +import { z } from "zod"; + +import { createSamlConfig, updateSamlConfig } from "@/actions/integrations"; +import { AddIcon } from "@/components/icons"; +import { useToast } from "@/components/ui"; +import { CustomButton, CustomServerInput } from "@/components/ui/custom"; +import { SnippetChip } from "@/components/ui/entities"; +import { FormButtons } from "@/components/ui/form"; +import { apiBaseUrl } from "@/lib"; + +export const SamlConfigForm = ({ + setIsOpen, + samlConfig, +}: { + setIsOpen: Dispatch>; + samlConfig?: any; +}) => { + const [state, formAction, isPending] = useFormState( + samlConfig?.id ? updateSamlConfig : createSamlConfig, + null, + ); + const [emailDomain, setEmailDomain] = useState( + samlConfig?.attributes?.email_domain || "", + ); + const [uploadedFile, setUploadedFile] = useState(null); + const [clientErrors, setClientErrors] = useState<{ + email_domain?: string | null; + metadata_xml?: string | null; + }>({}); + const formRef = useRef(null); + const { toast } = useToast(); + + // Client-side validation function + const validateFields = (email: string, hasFile: boolean) => { + // Validar cada campo por separado para poder limpiarlos individualmente + const emailValidation = z + .string() + .trim() + .min(1, { message: "Email domain is required" }) + .safeParse(email); + const metadataValidation = z + .string() + .trim() + .min(1, { message: "Metadata XML is required" }) + .safeParse(hasFile ? "dummy_xml_content" : ""); + + const newErrors = { + email_domain: emailValidation.success + ? null + : emailValidation.error.issues[0]?.message, + metadata_xml: metadataValidation.success + ? null + : metadataValidation.error.issues[0]?.message, + }; + + setClientErrors(newErrors); + }; + + useEffect(() => { + if (state?.success) { + toast({ + title: "Configuration saved successfully", + description: state.success, + }); + setIsOpen(false); + } else if (state?.errors?.general) { + toast({ + variant: "destructive", + title: "Oops! Something went wrong", + description: state.errors.general, + }); + } + }, [state, toast, setIsOpen]); + + const handleFileUpload = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) { + setUploadedFile(null); + validateFields(emailDomain, false); + return; + } + + // Check file extension + const isXmlFile = + file.name.toLowerCase().endsWith(".xml") || + file.type === "text/xml" || + file.type === "application/xml"; + + if (!isXmlFile) { + toast({ + variant: "destructive", + title: "Invalid file type", + description: "Please select a valid XML file (.xml extension).", + }); + // Clear the file input + event.target.value = ""; + setUploadedFile(null); + validateFields(emailDomain, false); + + return; + } + + const reader = new FileReader(); + reader.onload = (e) => { + const content = e.target?.result as string; + + // Basic XML validation + if (!content.trim().startsWith("<") || !content.includes(" { + toast({ + variant: "destructive", + title: "File read error", + description: "Failed to read the selected file.", + }); + // Clear the file input + event.target.value = ""; + setUploadedFile(null); + validateFields(emailDomain, false); + }; + + reader.readAsText(file); + }; + + const acsUrl = emailDomain + ? `${apiBaseUrl}/accounts/saml/${emailDomain}/acs/` + : `${apiBaseUrl}/accounts/saml/your-domain.com/acs/`; + + return ( +
+ + ) => { + const newValue = e.target.value; + setEmailDomain(newValue); + validateFields(newValue, !!uploadedFile); + }} + /> + +
+

+ Identity Provider Configuration +

+ +
+
+ + ACS URL: + + +
+ +
+ + Audience: + + +
+ +
+ + Name ID Format: + + + urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress + +
+ +
+ + Supported Assertion Attributes: + +
    +
  • • firstName
  • +
  • • lastName
  • +
  • • userType
  • +
  • • organization
  • +
+

+ Note: The userType attribute will be used to + assign the user's role. If the role does not exist, one will + be created with minimal permissions. You can assign permissions to + roles on the{" "} + + Roles + {" "} + page. +

+
+
+
+
+ + Metadata XML File * + + { + const fileInput = document.getElementById( + "metadata_xml_file", + ) as HTMLInputElement; + if (fileInput) { + fileInput.click(); + } + }} + startContent={} + className={`h-10 justify-start rounded-medium border-2 text-default-500 ${ + ( + clientErrors.metadata_xml === null + ? undefined + : clientErrors.metadata_xml || state?.errors?.metadata_xml + ) + ? "border-red-500" + : uploadedFile + ? "border-green-500 bg-green-50 dark:bg-green-900/20" + : "border-default-200" + }`} + > + + {uploadedFile ? ( + + {uploadedFile.name} + + ) : ( + "Choose File" + )} + + + + + +

+ Upload your Identity Provider's SAML metadata XML file +

+ + {(() => { + const finalError = + clientErrors.metadata_xml === null + ? undefined + : clientErrors.metadata_xml || state?.errors?.metadata_xml; + return finalError; + })()} + +
+ + + ); +}; diff --git a/ui/components/integrations/index.ts b/ui/components/integrations/index.ts new file mode 100644 index 0000000000..fc77086ec6 --- /dev/null +++ b/ui/components/integrations/index.ts @@ -0,0 +1,2 @@ +export * from "./forms"; +export * from "./saml-integration-card"; diff --git a/ui/components/integrations/saml-integration-card.tsx b/ui/components/integrations/saml-integration-card.tsx new file mode 100644 index 0000000000..ae3eb6f60d --- /dev/null +++ b/ui/components/integrations/saml-integration-card.tsx @@ -0,0 +1,125 @@ +"use client"; + +import { Card, CardBody, CardHeader } from "@nextui-org/react"; +import { Link } from "@nextui-org/react"; +import { CheckIcon, Trash2Icon } from "lucide-react"; +import { useState } from "react"; + +import { deleteSamlConfig } from "@/actions/integrations"; +import { useToast } from "@/components/ui"; +import { CustomAlertModal, CustomButton } from "@/components/ui/custom"; + +import { SamlConfigForm } from "./forms"; + +export const SamlIntegrationCard = ({ samlConfig }: { samlConfig?: any }) => { + const [isSamlModalOpen, setIsSamlModalOpen] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + const { toast } = useToast(); + const id = samlConfig?.id; + + const handleRemoveSaml = async () => { + if (!id) return; + + setIsDeleting(true); + try { + const result = await deleteSamlConfig(id); + + if (result.success) { + toast({ + title: "SAML configuration removed", + description: result.success, + }); + } else if (result.errors?.general) { + toast({ + variant: "destructive", + title: "Error removing SAML configuration", + description: result.errors.general, + }); + } + } catch (error) { + toast({ + variant: "destructive", + title: "Error", + description: "Failed to remove SAML configuration. Please try again.", + }); + } finally { + setIsDeleting(false); + } + }; + + return ( + <> + + + + + + +
+
+

SAML SSO Integration

+ {id && } +
+

+ {id ? ( + "SAML Single Sign-On is enabled for this organization" + ) : ( + <> + Configure SAML Single Sign-On for secure authentication.{" "} + + Read the docs + + + )} +

+
+
+ +
+
+ Status: + + {id ? "Enabled" : "Disabled"} + +
+
+ setIsSamlModalOpen(true)} + > + {id ? "Update" : "Enable"} + + {id && ( + : null} + onPress={handleRemoveSaml} + > + Remove + + )} +
+
+
+
+ + ); +}; diff --git a/ui/components/lighthouse/chat.tsx b/ui/components/lighthouse/chat.tsx index 1018607f17..cfc350f4da 100644 --- a/ui/components/lighthouse/chat.tsx +++ b/ui/components/lighthouse/chat.tsx @@ -135,8 +135,8 @@ export const Chat = ({ hasConfig, isActive }: ChatProps) => {

{!hasConfig - ? "Please configure your OpenAI API key to use the Lighthouse Cloud Security Analyst." - : "OpenAI API key is invalid. Please update your key to use Lighthouse Cloud Security Analyst."} + ? "Please configure your OpenAI API key to use Lighthouse." + : "OpenAI API key is invalid. Please update your key to use Lighthouse."}

= ({ findingsByStatus, }) => { + const searchParams = useSearchParams(); + const shouldShowMuted = searchParams.get("filter[muted]") !== "false"; + const { fail = 0, pass = 0, + muted = 0, pass_new = 0, fail_new = 0, + muted_new = 0, } = findingsByStatus?.data?.attributes || {}; - const chartData = useMemo( - () => [ + + const chartData = useMemo(() => { + const data = [ { findings: "Success", number: pass, @@ -75,9 +88,18 @@ export const FindingsByStatusChart: React.FC = ({ number: fail, fill: "var(--color-fail)", }, - ], - [pass, fail], - ); + ]; + + if (shouldShowMuted) { + data.push({ + findings: "Muted", + number: muted, + fill: "var(--color-muted)", + }); + } + + return data; + }, [pass, fail, muted, shouldShowMuted]); const updatedChartData = calculatePercent(chartData); @@ -86,6 +108,8 @@ export const FindingsByStatusChart: React.FC = ({ [chartData], ); + const hasDataToShow = totalFindings > 0; + const emptyChartData = [ { findings: "Empty", @@ -105,7 +129,7 @@ export const FindingsByStatusChart: React.FC = ({ } /> 0 ? chartData : emptyChartData} + data={hasDataToShow ? chartData : emptyChartData} dataKey="number" nameKey="findings" innerRadius={65} @@ -126,7 +150,7 @@ export const FindingsByStatusChart: React.FC = ({ y={viewBox.cy} className="fill-foreground text-xl font-bold" > - {totalFindings > 0 + {hasDataToShow ? totalFindings.toLocaleString() : "0"} @@ -135,7 +159,7 @@ export const FindingsByStatusChart: React.FC = ({ y={(viewBox.cy || 0) + 20} className="fill-foreground text-xs" > - Findings + {"Findings"} ); @@ -204,6 +228,46 @@ export const FindingsByStatusChart: React.FC = ({
+ + {shouldShowMuted && ( +
+
+ + } + color="warning" + radius="lg" + size="md" + > + {chartData.find((item) => item.findings === "Muted") + ?.number || 0} + + + {updatedChartData.find( + (item) => item.findings === "Muted", + )?.percent || "0%"} + + +
+
+ {muted_new > 0 ? ( + <> + +{muted_new} muted findings from last day{" "} + + + ) : muted_new < 0 ? ( + <>{muted_new} muted findings from last day + ) : ( + "No change from last day" + )} +
+
+ )} diff --git a/ui/components/overview/new-findings-table/table/column-new-findings-to-date.tsx b/ui/components/overview/new-findings-table/table/column-new-findings-to-date.tsx index 488f8b190b..348c1be8df 100644 --- a/ui/components/overview/new-findings-table/table/column-new-findings-to-date.tsx +++ b/ui/components/overview/new-findings-table/table/column-new-findings-to-date.tsx @@ -79,7 +79,7 @@ export const ColumnNewFindingsToDate: ColumnDef[] = [ cell: ({ row }) => { const { checktitle } = getFindingsMetadata(row); const { - attributes: { muted }, + attributes: { muted, muted_reason }, } = getFindingsData(row); const { delta } = row.original.attributes; return ( @@ -95,7 +95,7 @@ export const ColumnNewFindingsToDate: ColumnDef[] = [

- + ); diff --git a/ui/components/providers/credentials-update-info.tsx b/ui/components/providers/credentials-update-info.tsx index 1b32e2391f..2715a1ea6b 100644 --- a/ui/components/providers/credentials-update-info.tsx +++ b/ui/components/providers/credentials-update-info.tsx @@ -1,6 +1,5 @@ "use client"; -import { InfoIcon } from "@/components/icons"; import { SelectViaAWS } from "@/components/providers/workflow/forms/select-credentials-type/aws"; import { SelectViaGCP } from "@/components/providers/workflow/forms/select-credentials-type/gcp"; import { ProviderType } from "@/types/providers"; @@ -24,28 +23,5 @@ export const CredentialsUpdateInfo = ({ return null; }; - return ( -
-

- To update provider credentials,{" "} - - the same type that was originally configured must be used. - -

-
- -

- If the provider was configured with static credentials, updates must - also use static credentials. If it was configured with a role in AWS - (or service account in GCP),{" "} - updates must use the same type. -

-
-

- To switch from one type to another, the provider must be deleted and set - up again. -

- {renderSelectComponent()} -
- ); + return
{renderSelectComponent()}
; }; diff --git a/ui/components/providers/forms/index.ts b/ui/components/providers/forms/index.ts index a081952ade..e6001f358d 100644 --- a/ui/components/providers/forms/index.ts +++ b/ui/components/providers/forms/index.ts @@ -1,2 +1,3 @@ export * from "./delete-form"; export * from "./edit-form"; +export { MutedFindingsConfigForm } from "./muted-findings-config-form"; diff --git a/ui/components/providers/forms/muted-findings-config-form.tsx b/ui/components/providers/forms/muted-findings-config-form.tsx new file mode 100644 index 0000000000..52c5e2c7df --- /dev/null +++ b/ui/components/providers/forms/muted-findings-config-form.tsx @@ -0,0 +1,262 @@ +"use client"; + +import { Textarea } from "@nextui-org/react"; +import Link from "next/link"; +import { Dispatch, SetStateAction, useEffect, useState } from "react"; +import { useFormState } from "react-dom"; + +import { + createMutedFindingsConfig, + deleteMutedFindingsConfig, + getMutedFindingsConfig, + updateMutedFindingsConfig, +} from "@/actions/processors"; +import { DeleteIcon } from "@/components/icons"; +import { useToast } from "@/components/ui"; +import { CustomButton } from "@/components/ui/custom"; +import { FormButtons } from "@/components/ui/form"; +import { fontMono } from "@/config/fonts"; +import { convertToYaml, parseYamlValidation } from "@/lib/yaml"; +import { + MutedFindingsConfigActionState, + ProcessorData, +} from "@/types/processors"; + +interface MutedFindingsConfigFormProps { + setIsOpen: Dispatch>; +} + +export const MutedFindingsConfigForm = ({ + setIsOpen, +}: MutedFindingsConfigFormProps) => { + const [config, setConfig] = useState(null); + const [configText, setConfigText] = useState(""); + const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + const [yamlValidation, setYamlValidation] = useState<{ + isValid: boolean; + error?: string; + }>({ isValid: true }); + const [hasUserStartedTyping, setHasUserStartedTyping] = useState(false); + + const [state, formAction, isPending] = useFormState< + MutedFindingsConfigActionState, + FormData + >(config ? updateMutedFindingsConfig : createMutedFindingsConfig, null); + + const { toast } = useToast(); + + useEffect(() => { + getMutedFindingsConfig().then((result) => { + setConfig(result || null); + const yamlConfig = convertToYaml(result?.attributes.configuration || ""); + setConfigText(yamlConfig); + setHasUserStartedTyping(false); // Reset when loading initial config + if (yamlConfig) { + setYamlValidation(parseYamlValidation(yamlConfig)); + } + }); + }, []); + + useEffect(() => { + if (state?.success) { + toast({ + title: "Configuration saved successfully", + description: state.success, + }); + setIsOpen(false); + } else if (state?.errors?.general) { + toast({ + variant: "destructive", + title: "Oops! Something went wrong", + description: state.errors.general, + }); + } else if (state?.errors?.configuration) { + // Reset typing state when there are new server errors + setHasUserStartedTyping(false); + } + }, [state, toast, setIsOpen]); + + const handleConfigChange = (value: string) => { + setConfigText(value); + // Clear server errors when user starts typing + setHasUserStartedTyping(true); + // Validate YAML in real-time + const validation = parseYamlValidation(value); + setYamlValidation(validation); + }; + + const handleDelete = async () => { + if (!config) return; + + setIsDeleting(true); + const formData = new FormData(); + formData.append("id", config.id); + + try { + const result = await deleteMutedFindingsConfig(null, formData); + if (result?.success) { + toast({ + title: "Configuration deleted successfully", + description: result.success, + }); + setIsOpen(false); + } else if (result?.errors?.general) { + toast({ + variant: "destructive", + title: "Oops! Something went wrong", + description: result.errors.general, + }); + } + } catch (error) { + toast({ + variant: "destructive", + title: "Oops! Something went wrong", + description: "Error deleting configuration. Please try again.", + }); + } finally { + setIsDeleting(false); + setShowDeleteConfirmation(false); + } + }; + + if (showDeleteConfirmation) { + return ( +
+

+ Delete Mutelist Configuration +

+

+ Are you sure you want to delete this configuration? This action cannot + be undone. +

+
+ setShowDeleteConfirmation(false)} + isDisabled={isDeleting} + > + Cancel + + } + onPress={handleDelete} + > + {isDeleting ? "Deleting" : "Delete"} + +
+
+ ); + } + + return ( +
+ {config && } + +
+
+
    +
  • + + This Mutelist configuration will take effect on the next scan. + +
  • +
  • + Mutelist configuration can be modified at anytime on the Providers + and Scans pages. +
  • +
  • + Learn more about configuring the Mutelist{" "} + + here + + . +
  • +
  • + A default Mutelist is used, to exclude certain predefined + resources, if no Mutelist is provided. +
  • +
+
+ +
+ +
+