diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index f98e90fd03..94122d8349 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -8,6 +8,10 @@ If fixes an issue please add it with `Fix #XXXX` Please include a summary of the change and which issue is fixed. List any dependencies that are required for this change. +### Steps to review + +Please add a detailed description of how to review this PR. + ### Checklist - Are there new checks included in this PR? Yes / No diff --git a/.github/workflows/build-documentation-on-pr.yml b/.github/workflows/build-documentation-on-pr.yml index 25fc2910f9..20553b87af 100644 --- a/.github/workflows/build-documentation-on-pr.yml +++ b/.github/workflows/build-documentation-on-pr.yml @@ -7,6 +7,7 @@ on: - 'v3' paths: - 'docs/**' + - '.github/workflows/build-documentation-on-pr.yml' env: PR_NUMBER: ${{ github.event.pull_request.number }} @@ -16,9 +17,20 @@ jobs: name: Documentation Link runs-on: ubuntu-latest steps: - - name: Leave PR comment with the Prowler Documentation URI - uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0 + - name: Find existing documentation comment + uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0 + id: find-comment with: + issue-number: ${{ env.PR_NUMBER }} + comment-author: 'github-actions[bot]' + body-includes: '' + + - name: Create or update PR comment with the Prowler Documentation URI + uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0 + with: + comment-id: ${{ steps.find-comment.outputs.comment-id }} issue-number: ${{ env.PR_NUMBER }} body: | + You can check the documentation for this PR here -> [Prowler Documentation](https://prowler-prowler-docs--${{ env.PR_NUMBER }}.com.readthedocs.build/projects/prowler-open-source/en/${{ env.PR_NUMBER }}/) + edit-mode: replace diff --git a/.github/workflows/create-backport-label.yml b/.github/workflows/create-backport-label.yml index 0bc93bd45d..3b485ec513 100644 --- a/.github/workflows/create-backport-label.yml +++ b/.github/workflows/create-backport-label.yml @@ -1,4 +1,4 @@ -name: Create Backport Label +name: Prowler - Create Backport Label on: release: diff --git a/.github/workflows/pr-conflict-checker.yml b/.github/workflows/pr-conflict-checker.yml new file mode 100644 index 0000000000..2071dbaa78 --- /dev/null +++ b/.github/workflows/pr-conflict-checker.yml @@ -0,0 +1,166 @@ +name: Prowler - PR Conflict Checker + +on: + pull_request: + types: + - opened + - synchronize + - reopened + branches: + - "master" + - "v5.*" + pull_request_target: + types: + - opened + - synchronize + - reopened + branches: + - "master" + - "v5.*" + +jobs: + conflict-checker: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5 + with: + files: | + ** + + - name: Check for conflict markers + id: conflict-check + run: | + echo "Checking for conflict markers in changed files..." + + CONFLICT_FILES="" + HAS_CONFLICTS=false + + # Check each changed file for conflict markers + for file in ${{ steps.changed-files.outputs.all_changed_files }}; do + if [ -f "$file" ]; then + echo "Checking file: $file" + + # Look for conflict markers + if grep -l "^<<<<<<<\|^=======\|^>>>>>>>" "$file" 2>/dev/null; then + echo "Conflict markers found in: $file" + CONFLICT_FILES="$CONFLICT_FILES$file " + HAS_CONFLICTS=true + fi + fi + done + + if [ "$HAS_CONFLICTS" = true ]; then + echo "has_conflicts=true" >> $GITHUB_OUTPUT + echo "conflict_files=$CONFLICT_FILES" >> $GITHUB_OUTPUT + echo "Conflict markers detected in files: $CONFLICT_FILES" + else + echo "has_conflicts=false" >> $GITHUB_OUTPUT + echo "No conflict markers found in changed files" + fi + + - name: Add conflict label + if: steps.conflict-check.outputs.has_conflicts == 'true' + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + github-token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} + script: | + const { data: labels } = await github.rest.issues.listLabelsOnIssue({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const hasConflictLabel = labels.some(label => label.name === 'has-conflicts'); + + if (!hasConflictLabel) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + labels: ['has-conflicts'] + }); + console.log('Added has-conflicts label'); + } else { + console.log('has-conflicts label already exists'); + } + + - name: Remove conflict label + if: steps.conflict-check.outputs.has_conflicts == 'false' + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + try { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + name: 'has-conflicts' + }); + console.log('Removed has-conflicts label'); + } catch (error) { + if (error.status === 404) { + console.log('has-conflicts label was not present'); + } else { + throw error; + } + } + + - name: Find existing conflict comment + if: steps.conflict-check.outputs.has_conflicts == 'true' + uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0 + id: find-comment + with: + issue-number: ${{ github.event.pull_request.number }} + comment-author: 'github-actions[bot]' + body-regex: '(⚠️ \*\*Conflict Markers Detected\*\*|✅ \*\*Conflict Markers Resolved\*\*)' + + - name: Create or update conflict comment + if: steps.conflict-check.outputs.has_conflicts == 'true' + uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0 + with: + comment-id: ${{ steps.find-comment.outputs.comment-id }} + issue-number: ${{ github.event.pull_request.number }} + edit-mode: replace + body: | + ⚠️ **Conflict Markers Detected** + + This pull request contains unresolved conflict markers in the following files: + ``` + ${{ steps.conflict-check.outputs.conflict_files }} + ``` + + Please resolve these conflicts by: + 1. Locating the conflict markers: `<<<<<<<`, `=======`, and `>>>>>>>` + 2. Manually editing the files to resolve the conflicts + 3. Removing all conflict markers + 4. Committing and pushing the changes + + - name: Find existing conflict comment when resolved + if: steps.conflict-check.outputs.has_conflicts == 'false' + uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0 + id: find-resolved-comment + with: + issue-number: ${{ github.event.pull_request.number }} + comment-author: 'github-actions[bot]' + body-regex: '(⚠️ \*\*Conflict Markers Detected\*\*|✅ \*\*Conflict Markers Resolved\*\*)' + + - name: Update comment when conflicts resolved + if: steps.conflict-check.outputs.has_conflicts == 'false' && steps.find-resolved-comment.outputs.comment-id != '' + uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0 + with: + comment-id: ${{ steps.find-resolved-comment.outputs.comment-id }} + issue-number: ${{ github.event.pull_request.number }} + edit-mode: replace + body: | + ✅ **Conflict Markers Resolved** + + All conflict markers have been successfully resolved in this pull request. diff --git a/.github/workflows/prowler-release-preparation.yml b/.github/workflows/prowler-release-preparation.yml index 509b1352d5..6008a25795 100644 --- a/.github/workflows/prowler-release-preparation.yml +++ b/.github/workflows/prowler-release-preparation.yml @@ -1,4 +1,4 @@ -name: Prowler Release Preparation +name: Prowler - Release Preparation run-name: Prowler Release Preparation for ${{ inputs.prowler_version }} diff --git a/.github/workflows/pull-request-check-changelog.yml b/.github/workflows/pull-request-check-changelog.yml index 5055edf110..c4e0fa31c0 100644 --- a/.github/workflows/pull-request-check-changelog.yml +++ b/.github/workflows/pull-request-check-changelog.yml @@ -1,4 +1,4 @@ -name: Check Changelog +name: Prowler - Check Changelog on: pull_request: diff --git a/.gitignore b/.gitignore index 1b5075705c..6e7e284566 100644 --- a/.gitignore +++ b/.gitignore @@ -75,3 +75,6 @@ node_modules # Persistent data _data/ + +# Claude +CLAUDE.md diff --git a/SECURITY.md b/SECURITY.md index e74c3960b3..365699f6a1 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,23 +1,65 @@ -# Security Policy +# Security -## Software Security -As an **AWS Partner** and we have passed the [AWS Foundation Technical Review (FTR)](https://aws.amazon.com/partners/foundational-technical-review/) and we use the following tools and automation to make sure our code is secure and dependencies up-to-dated: +## Reporting Vulnerabilities -- `bandit` for code security review. -- `safety` and `dependabot` for dependencies. -- `hadolint` and `dockle` for our containers security. -- `snyk` in Docker Hub. -- `clair` in Amazon ECR. -- `vulture`, `flake8`, `black` and `pylint` for formatting and best practices. +At Prowler, we consider the security of our open source software and systems a top priority. But no matter how much effort we put into system security, there can still be vulnerabilities present. -## Reporting a Vulnerability +If you discover a vulnerability, we would like to know about it so we can take steps to address it as quickly as possible. We would like to ask you to help us better protect our users, our clients and our systems. -If you would like to report a vulnerability or have a security concern regarding Prowler Open Source or ProwlerPro service, please submit the information by contacting to https://support.prowler.com. +When reporting vulnerabilities, please consider (1) attack scenario / exploitability, and (2) the security impact of the bug. The following issues are considered out of scope: -The information you share with ProwlerPro as part of this process is kept confidential within ProwlerPro. We will only share this information with a third party if the vulnerability you report is found to affect a third-party product, in which case we will share this information with the third-party product's author or manufacturer. Otherwise, we will only share this information as permitted by you. +- Social engineering support or attacks requiring social engineering. +- Clickjacking on pages with no sensitive actions. +- Cross-Site Request Forgery (CSRF) on unauthenticated forms or forms with no sensitive actions. +- Attacks requiring Man-In-The-Middle (MITM) or physical access to a user's device. +- Previously known vulnerable libraries without a working Proof of Concept (PoC). +- Comma Separated Values (CSV) injection without demonstrating a vulnerability. +- Missing best practices in SSL/TLS configuration. +- Any activity that could lead to the disruption of service (DoS). +- Rate limiting or brute force issues on non-authentication endpoints. +- Missing best practices in Content Security Policy (CSP). +- Missing HttpOnly or Secure flags on cookies. +- Configuration of or missing security headers. +- Missing email best practices, such as invalid, incomplete, or missing SPF/DKIM/DMARC records. +- Vulnerabilities only affecting users of outdated or unpatched browsers (less than two stable versions behind). +- Software version disclosure, banner identification issues, or descriptive error messages. +- Tabnabbing. +- Issues that require unlikely user interaction. +- Improper logout functionality and improper session timeout. +- CORS misconfiguration without an exploitation scenario. +- Broken link hijacking. +- Automated scanning results (e.g., sqlmap, Burp active scanner) that have not been manually verified. +- Content spoofing and text injection issues without a clear attack vector. +- Email spoofing without exploiting security flaws. +- Dead links or broken links. +- User enumeration. -We will review the submitted report, and assign it a tracking number. We will then respond to you, acknowledging receipt of the report, and outline the next steps in the process. +Testing guidelines: +- Do not run automated scanners on other customer projects. Running automated scanners can run up costs for our users. Aggressively configured scanners might inadvertently disrupt services, exploit vulnerabilities, lead to system instability or breaches and violate Terms of Service from our upstream providers. Our own security systems won't be able to distinguish hostile reconnaissance from whitehat research. If you wish to run an automated scanner, notify us at support@prowler.com and only run it on your own Prowler app project. Do NOT attack Prowler in usage of other customers. +- Do not take advantage of the vulnerability or problem you have discovered, for example by downloading more data than necessary to demonstrate the vulnerability or deleting or modifying other people's data. -You will receive a non-automated response to your initial contact within 24 hours, confirming receipt of your reported vulnerability. +Reporting guidelines: +- File a report through our Support Desk at https://support.prowler.com +- If it is about a lack of a security functionality, please file a feature request instead at https://github.com/prowler-cloud/prowler/issues +- Do provide sufficient information to reproduce the problem, so we will be able to resolve it as quickly as possible. +- If you have further questions and want direct interaction with the Prowler team, please contact us at via our Community Slack at goto.prowler.com/slack. -We will coordinate public notification of any validated vulnerability with you. Where possible, we prefer that our respective public disclosures be posted simultaneously. +Disclosure guidelines: +- In order to protect our users and customers, do not reveal the problem to others until we have researched, addressed and informed our affected customers. +- If you want to publicly share your research about Prowler at a conference, in a blog or any other public forum, you should share a draft with us for review and approval at least 30 days prior to the publication date. Please note that the following should not be included: + - Data regarding any Prowler user or customer projects. + - Prowler customers' data. + - Information about Prowler employees, contractors or partners. + +What we promise: +- We will respond to your report within 5 business days with our evaluation of the report and an expected resolution date. +- If you have followed the instructions above, we will not take any legal action against you in regard to the report. +- We will handle your report with strict confidentiality, and not pass on your personal details to third parties without your permission. +- We will keep you informed of the progress towards resolving the problem. +- In the public information concerning the problem reported, we will give your name as the discoverer of the problem (unless you desire otherwise). + +We strive to resolve all problems as quickly as possible, and we would like to play an active role in the ultimate publication on the problem after it is resolved. + +--- + +For more information about our security policies, please refer to our [Security](https://docs.prowler.com/projects/prowler-open-source/en/latest/security/) section in our documentation. \ No newline at end of file diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index 14cb1d027c..adc80ed8cf 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to the **Prowler API** are documented in this file. ### Added - Lighthouse support for OpenAI GPT-5 [(#8527)](https://github.com/prowler-cloud/prowler/pull/8527) +- Integration with Amazon Security Hub, enabling sending findings to Security Hub [(#8365)](https://github.com/prowler-cloud/prowler/pull/8365) +- Generate ASFF output for AWS providers with SecurityHub integration enabled [(#8569)](https://github.com/prowler-cloud/prowler/pull/8569) ## [1.11.0] (Prowler 5.10.0) diff --git a/api/poetry.lock b/api/poetry.lock index 771a036594..5171644ab1 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" @@ -26,98 +26,98 @@ files = [ [[package]] name = "aiohttp" -version = "3.12.14" +version = "3.12.15" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "aiohttp-3.12.14-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:906d5075b5ba0dd1c66fcaaf60eb09926a9fef3ca92d912d2a0bbdbecf8b1248"}, - {file = "aiohttp-3.12.14-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c875bf6fc2fd1a572aba0e02ef4e7a63694778c5646cdbda346ee24e630d30fb"}, - {file = "aiohttp-3.12.14-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fbb284d15c6a45fab030740049d03c0ecd60edad9cd23b211d7e11d3be8d56fd"}, - {file = "aiohttp-3.12.14-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38e360381e02e1a05d36b223ecab7bc4a6e7b5ab15760022dc92589ee1d4238c"}, - {file = "aiohttp-3.12.14-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aaf90137b5e5d84a53632ad95ebee5c9e3e7468f0aab92ba3f608adcb914fa95"}, - {file = "aiohttp-3.12.14-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e532a25e4a0a2685fa295a31acf65e027fbe2bea7a4b02cdfbbba8a064577663"}, - {file = "aiohttp-3.12.14-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eab9762c4d1b08ae04a6c77474e6136da722e34fdc0e6d6eab5ee93ac29f35d1"}, - {file = "aiohttp-3.12.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abe53c3812b2899889a7fca763cdfaeee725f5be68ea89905e4275476ffd7e61"}, - {file = "aiohttp-3.12.14-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5760909b7080aa2ec1d320baee90d03b21745573780a072b66ce633eb77a8656"}, - {file = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:02fcd3f69051467bbaa7f84d7ec3267478c7df18d68b2e28279116e29d18d4f3"}, - {file = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4dcd1172cd6794884c33e504d3da3c35648b8be9bfa946942d353b939d5f1288"}, - {file = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:224d0da41355b942b43ad08101b1b41ce633a654128ee07e36d75133443adcda"}, - {file = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e387668724f4d734e865c1776d841ed75b300ee61059aca0b05bce67061dcacc"}, - {file = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:dec9cde5b5a24171e0b0a4ca064b1414950904053fb77c707efd876a2da525d8"}, - {file = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bbad68a2af4877cc103cd94af9160e45676fc6f0c14abb88e6e092b945c2c8e3"}, - {file = "aiohttp-3.12.14-cp310-cp310-win32.whl", hash = "sha256:ee580cb7c00bd857b3039ebca03c4448e84700dc1322f860cf7a500a6f62630c"}, - {file = "aiohttp-3.12.14-cp310-cp310-win_amd64.whl", hash = "sha256:cf4f05b8cea571e2ccc3ca744e35ead24992d90a72ca2cf7ab7a2efbac6716db"}, - {file = "aiohttp-3.12.14-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f4552ff7b18bcec18b60a90c6982049cdb9dac1dba48cf00b97934a06ce2e597"}, - {file = "aiohttp-3.12.14-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8283f42181ff6ccbcf25acaae4e8ab2ff7e92b3ca4a4ced73b2c12d8cd971393"}, - {file = "aiohttp-3.12.14-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:040afa180ea514495aaff7ad34ec3d27826eaa5d19812730fe9e529b04bb2179"}, - {file = "aiohttp-3.12.14-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b413c12f14c1149f0ffd890f4141a7471ba4b41234fe4fd4a0ff82b1dc299dbb"}, - {file = "aiohttp-3.12.14-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d6f607ce2e1a93315414e3d448b831238f1874b9968e1195b06efaa5c87e245"}, - {file = "aiohttp-3.12.14-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:565e70d03e924333004ed101599902bba09ebb14843c8ea39d657f037115201b"}, - {file = "aiohttp-3.12.14-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4699979560728b168d5ab63c668a093c9570af2c7a78ea24ca5212c6cdc2b641"}, - {file = "aiohttp-3.12.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad5fdf6af93ec6c99bf800eba3af9a43d8bfd66dce920ac905c817ef4a712afe"}, - {file = "aiohttp-3.12.14-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4ac76627c0b7ee0e80e871bde0d376a057916cb008a8f3ffc889570a838f5cc7"}, - {file = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:798204af1180885651b77bf03adc903743a86a39c7392c472891649610844635"}, - {file = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:4f1205f97de92c37dd71cf2d5bcfb65fdaed3c255d246172cce729a8d849b4da"}, - {file = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:76ae6f1dd041f85065d9df77c6bc9c9703da9b5c018479d20262acc3df97d419"}, - {file = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a194ace7bc43ce765338ca2dfb5661489317db216ea7ea700b0332878b392cab"}, - {file = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:16260e8e03744a6fe3fcb05259eeab8e08342c4c33decf96a9dad9f1187275d0"}, - {file = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c779e5ebbf0e2e15334ea404fcce54009dc069210164a244d2eac8352a44b28"}, - {file = "aiohttp-3.12.14-cp311-cp311-win32.whl", hash = "sha256:a289f50bf1bd5be227376c067927f78079a7bdeccf8daa6a9e65c38bae14324b"}, - {file = "aiohttp-3.12.14-cp311-cp311-win_amd64.whl", hash = "sha256:0b8a69acaf06b17e9c54151a6c956339cf46db4ff72b3ac28516d0f7068f4ced"}, - {file = "aiohttp-3.12.14-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a0ecbb32fc3e69bc25efcda7d28d38e987d007096cbbeed04f14a6662d0eee22"}, - {file = "aiohttp-3.12.14-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0400f0ca9bb3e0b02f6466421f253797f6384e9845820c8b05e976398ac1d81a"}, - {file = "aiohttp-3.12.14-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a56809fed4c8a830b5cae18454b7464e1529dbf66f71c4772e3cfa9cbec0a1ff"}, - {file = "aiohttp-3.12.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27f2e373276e4755691a963e5d11756d093e346119f0627c2d6518208483fb6d"}, - {file = "aiohttp-3.12.14-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ca39e433630e9a16281125ef57ece6817afd1d54c9f1bf32e901f38f16035869"}, - {file = "aiohttp-3.12.14-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c748b3f8b14c77720132b2510a7d9907a03c20ba80f469e58d5dfd90c079a1c"}, - {file = "aiohttp-3.12.14-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0a568abe1b15ce69d4cc37e23020720423f0728e3cb1f9bcd3f53420ec3bfe7"}, - {file = "aiohttp-3.12.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9888e60c2c54eaf56704b17feb558c7ed6b7439bca1e07d4818ab878f2083660"}, - {file = "aiohttp-3.12.14-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3006a1dc579b9156de01e7916d38c63dc1ea0679b14627a37edf6151bc530088"}, - {file = "aiohttp-3.12.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aa8ec5c15ab80e5501a26719eb48a55f3c567da45c6ea5bb78c52c036b2655c7"}, - {file = "aiohttp-3.12.14-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:39b94e50959aa07844c7fe2206b9f75d63cc3ad1c648aaa755aa257f6f2498a9"}, - {file = "aiohttp-3.12.14-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:04c11907492f416dad9885d503fbfc5dcb6768d90cad8639a771922d584609d3"}, - {file = "aiohttp-3.12.14-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:88167bd9ab69bb46cee91bd9761db6dfd45b6e76a0438c7e884c3f8160ff21eb"}, - {file = "aiohttp-3.12.14-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:791504763f25e8f9f251e4688195e8b455f8820274320204f7eafc467e609425"}, - {file = "aiohttp-3.12.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2785b112346e435dd3a1a67f67713a3fe692d288542f1347ad255683f066d8e0"}, - {file = "aiohttp-3.12.14-cp312-cp312-win32.whl", hash = "sha256:15f5f4792c9c999a31d8decf444e79fcfd98497bf98e94284bf390a7bb8c1729"}, - {file = "aiohttp-3.12.14-cp312-cp312-win_amd64.whl", hash = "sha256:3b66e1a182879f579b105a80d5c4bd448b91a57e8933564bf41665064796a338"}, - {file = "aiohttp-3.12.14-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3143a7893d94dc82bc409f7308bc10d60285a3cd831a68faf1aa0836c5c3c767"}, - {file = "aiohttp-3.12.14-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3d62ac3d506cef54b355bd34c2a7c230eb693880001dfcda0bf88b38f5d7af7e"}, - {file = "aiohttp-3.12.14-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:48e43e075c6a438937c4de48ec30fa8ad8e6dfef122a038847456bfe7b947b63"}, - {file = "aiohttp-3.12.14-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:077b4488411a9724cecc436cbc8c133e0d61e694995b8de51aaf351c7578949d"}, - {file = "aiohttp-3.12.14-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d8c35632575653f297dcbc9546305b2c1133391089ab925a6a3706dfa775ccab"}, - {file = "aiohttp-3.12.14-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b8ce87963f0035c6834b28f061df90cf525ff7c9b6283a8ac23acee6502afd4"}, - {file = "aiohttp-3.12.14-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0a2cf66e32a2563bb0766eb24eae7e9a269ac0dc48db0aae90b575dc9583026"}, - {file = "aiohttp-3.12.14-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdea089caf6d5cde975084a884c72d901e36ef9c2fd972c9f51efbbc64e96fbd"}, - {file = "aiohttp-3.12.14-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a7865f27db67d49e81d463da64a59365ebd6b826e0e4847aa111056dcb9dc88"}, - {file = "aiohttp-3.12.14-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0ab5b38a6a39781d77713ad930cb5e7feea6f253de656a5f9f281a8f5931b086"}, - {file = "aiohttp-3.12.14-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9b3b15acee5c17e8848d90a4ebc27853f37077ba6aec4d8cb4dbbea56d156933"}, - {file = "aiohttp-3.12.14-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e4c972b0bdaac167c1e53e16a16101b17c6d0ed7eac178e653a07b9f7fad7151"}, - {file = "aiohttp-3.12.14-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7442488b0039257a3bdbc55f7209587911f143fca11df9869578db6c26feeeb8"}, - {file = "aiohttp-3.12.14-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f68d3067eecb64c5e9bab4a26aa11bd676f4c70eea9ef6536b0a4e490639add3"}, - {file = "aiohttp-3.12.14-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f88d3704c8b3d598a08ad17d06006cb1ca52a1182291f04979e305c8be6c9758"}, - {file = "aiohttp-3.12.14-cp313-cp313-win32.whl", hash = "sha256:a3c99ab19c7bf375c4ae3debd91ca5d394b98b6089a03231d4c580ef3c2ae4c5"}, - {file = "aiohttp-3.12.14-cp313-cp313-win_amd64.whl", hash = "sha256:3f8aad695e12edc9d571f878c62bedc91adf30c760c8632f09663e5f564f4baa"}, - {file = "aiohttp-3.12.14-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b8cc6b05e94d837bcd71c6531e2344e1ff0fb87abe4ad78a9261d67ef5d83eae"}, - {file = "aiohttp-3.12.14-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1dcb015ac6a3b8facd3677597edd5ff39d11d937456702f0bb2b762e390a21b"}, - {file = "aiohttp-3.12.14-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3779ed96105cd70ee5e85ca4f457adbce3d9ff33ec3d0ebcdf6c5727f26b21b3"}, - {file = "aiohttp-3.12.14-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:717a0680729b4ebd7569c1dcd718c46b09b360745fd8eb12317abc74b14d14d0"}, - {file = "aiohttp-3.12.14-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b5dd3a2ef7c7e968dbbac8f5574ebeac4d2b813b247e8cec28174a2ba3627170"}, - {file = "aiohttp-3.12.14-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4710f77598c0092239bc12c1fcc278a444e16c7032d91babf5abbf7166463f7b"}, - {file = "aiohttp-3.12.14-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f3e9f75ae842a6c22a195d4a127263dbf87cbab729829e0bd7857fb1672400b2"}, - {file = "aiohttp-3.12.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f9c8d55d6802086edd188e3a7d85a77787e50d56ce3eb4757a3205fa4657922"}, - {file = "aiohttp-3.12.14-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79b29053ff3ad307880d94562cca80693c62062a098a5776ea8ef5ef4b28d140"}, - {file = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:23e1332fff36bebd3183db0c7a547a1da9d3b4091509f6d818e098855f2f27d3"}, - {file = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:a564188ce831fd110ea76bcc97085dd6c625b427db3f1dbb14ca4baa1447dcbc"}, - {file = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a7a1b4302f70bb3ec40ca86de82def532c97a80db49cac6a6700af0de41af5ee"}, - {file = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:1b07ccef62950a2519f9bfc1e5b294de5dd84329f444ca0b329605ea787a3de5"}, - {file = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:938bd3ca6259e7e48b38d84f753d548bd863e0c222ed6ee6ace3fd6752768a84"}, - {file = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8bc784302b6b9f163b54c4e93d7a6f09563bd01ff2b841b29ed3ac126e5040bf"}, - {file = "aiohttp-3.12.14-cp39-cp39-win32.whl", hash = "sha256:a3416f95961dd7d5393ecff99e3f41dc990fb72eda86c11f2a60308ac6dcd7a0"}, - {file = "aiohttp-3.12.14-cp39-cp39-win_amd64.whl", hash = "sha256:196858b8820d7f60578f8b47e5669b3195c21d8ab261e39b1d705346458f445f"}, - {file = "aiohttp-3.12.14.tar.gz", hash = "sha256:6e06e120e34d93100de448fd941522e11dafa78ef1a893c179901b7d66aa29f2"}, + {file = "aiohttp-3.12.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b6fc902bff74d9b1879ad55f5404153e2b33a82e72a95c89cec5eb6cc9e92fbc"}, + {file = "aiohttp-3.12.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:098e92835b8119b54c693f2f88a1dec690e20798ca5f5fe5f0520245253ee0af"}, + {file = "aiohttp-3.12.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:40b3fee496a47c3b4a39a731954c06f0bd9bd3e8258c059a4beb76ac23f8e421"}, + {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ce13fcfb0bb2f259fb42106cdc63fa5515fb85b7e87177267d89a771a660b79"}, + {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3beb14f053222b391bf9cf92ae82e0171067cc9c8f52453a0f1ec7c37df12a77"}, + {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c39e87afe48aa3e814cac5f535bc6199180a53e38d3f51c5e2530f5aa4ec58c"}, + {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f1b4ce5bc528a6ee38dbf5f39bbf11dd127048726323b72b8e85769319ffc4"}, + {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1004e67962efabbaf3f03b11b4c43b834081c9e3f9b32b16a7d97d4708a9abe6"}, + {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8faa08fcc2e411f7ab91d1541d9d597d3a90e9004180edb2072238c085eac8c2"}, + {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fe086edf38b2222328cdf89af0dde2439ee173b8ad7cb659b4e4c6f385b2be3d"}, + {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:79b26fe467219add81d5e47b4a4ba0f2394e8b7c7c3198ed36609f9ba161aecb"}, + {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b761bac1192ef24e16706d761aefcb581438b34b13a2f069a6d343ec8fb693a5"}, + {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e153e8adacfe2af562861b72f8bc47f8a5c08e010ac94eebbe33dc21d677cd5b"}, + {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:fc49c4de44977aa8601a00edbf157e9a421f227aa7eb477d9e3df48343311065"}, + {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2776c7ec89c54a47029940177e75c8c07c29c66f73464784971d6a81904ce9d1"}, + {file = "aiohttp-3.12.15-cp310-cp310-win32.whl", hash = "sha256:2c7d81a277fa78b2203ab626ced1487420e8c11a8e373707ab72d189fcdad20a"}, + {file = "aiohttp-3.12.15-cp310-cp310-win_amd64.whl", hash = "sha256:83603f881e11f0f710f8e2327817c82e79431ec976448839f3cd05d7afe8f830"}, + {file = "aiohttp-3.12.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d3ce17ce0220383a0f9ea07175eeaa6aa13ae5a41f30bc61d84df17f0e9b1117"}, + {file = "aiohttp-3.12.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:010cc9bbd06db80fe234d9003f67e97a10fe003bfbedb40da7d71c1008eda0fe"}, + {file = "aiohttp-3.12.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3f9d7c55b41ed687b9d7165b17672340187f87a773c98236c987f08c858145a9"}, + {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc4fbc61bb3548d3b482f9ac7ddd0f18c67e4225aaa4e8552b9f1ac7e6bda9e5"}, + {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7fbc8a7c410bb3ad5d595bb7118147dfbb6449d862cc1125cf8867cb337e8728"}, + {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74dad41b3458dbb0511e760fb355bb0b6689e0630de8a22b1b62a98777136e16"}, + {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b6f0af863cf17e6222b1735a756d664159e58855da99cfe965134a3ff63b0b0"}, + {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5b7fe4972d48a4da367043b8e023fb70a04d1490aa7d68800e465d1b97e493b"}, + {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6443cca89553b7a5485331bc9bedb2342b08d073fa10b8c7d1c60579c4a7b9bd"}, + {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c5f40ec615e5264f44b4282ee27628cea221fcad52f27405b80abb346d9f3f8"}, + {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2abbb216a1d3a2fe86dbd2edce20cdc5e9ad0be6378455b05ec7f77361b3ab50"}, + {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:db71ce547012a5420a39c1b744d485cfb823564d01d5d20805977f5ea1345676"}, + {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ced339d7c9b5030abad5854aa5413a77565e5b6e6248ff927d3e174baf3badf7"}, + {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:7c7dd29c7b5bda137464dc9bfc738d7ceea46ff70309859ffde8c022e9b08ba7"}, + {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:421da6fd326460517873274875c6c5a18ff225b40da2616083c5a34a7570b685"}, + {file = "aiohttp-3.12.15-cp311-cp311-win32.whl", hash = "sha256:4420cf9d179ec8dfe4be10e7d0fe47d6d606485512ea2265b0d8c5113372771b"}, + {file = "aiohttp-3.12.15-cp311-cp311-win_amd64.whl", hash = "sha256:edd533a07da85baa4b423ee8839e3e91681c7bfa19b04260a469ee94b778bf6d"}, + {file = "aiohttp-3.12.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:802d3868f5776e28f7bf69d349c26fc0efadb81676d0afa88ed00d98a26340b7"}, + {file = "aiohttp-3.12.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2800614cd560287be05e33a679638e586a2d7401f4ddf99e304d98878c29444"}, + {file = "aiohttp-3.12.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8466151554b593909d30a0a125d638b4e5f3836e5aecde85b66b80ded1cb5b0d"}, + {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e5a495cb1be69dae4b08f35a6c4579c539e9b5706f606632102c0f855bcba7c"}, + {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6404dfc8cdde35c69aaa489bb3542fb86ef215fc70277c892be8af540e5e21c0"}, + {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ead1c00f8521a5c9070fcb88f02967b1d8a0544e6d85c253f6968b785e1a2ab"}, + {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6990ef617f14450bc6b34941dba4f12d5613cbf4e33805932f853fbd1cf18bfb"}, + {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd736ed420f4db2b8148b52b46b88ed038d0354255f9a73196b7bbce3ea97545"}, + {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c5092ce14361a73086b90c6efb3948ffa5be2f5b6fbcf52e8d8c8b8848bb97c"}, + {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aaa2234bb60c4dbf82893e934d8ee8dea30446f0647e024074237a56a08c01bd"}, + {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6d86a2fbdd14192e2f234a92d3b494dd4457e683ba07e5905a0b3ee25389ac9f"}, + {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a041e7e2612041a6ddf1c6a33b883be6a421247c7afd47e885969ee4cc58bd8d"}, + {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5015082477abeafad7203757ae44299a610e89ee82a1503e3d4184e6bafdd519"}, + {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:56822ff5ddfd1b745534e658faba944012346184fbfe732e0d6134b744516eea"}, + {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b2acbbfff69019d9014508c4ba0401822e8bae5a5fdc3b6814285b71231b60f3"}, + {file = "aiohttp-3.12.15-cp312-cp312-win32.whl", hash = "sha256:d849b0901b50f2185874b9a232f38e26b9b3d4810095a7572eacea939132d4e1"}, + {file = "aiohttp-3.12.15-cp312-cp312-win_amd64.whl", hash = "sha256:b390ef5f62bb508a9d67cb3bba9b8356e23b3996da7062f1a57ce1a79d2b3d34"}, + {file = "aiohttp-3.12.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9f922ffd05034d439dde1c77a20461cf4a1b0831e6caa26151fe7aa8aaebc315"}, + {file = "aiohttp-3.12.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ee8a8ac39ce45f3e55663891d4b1d15598c157b4d494a4613e704c8b43112cd"}, + {file = "aiohttp-3.12.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3eae49032c29d356b94eee45a3f39fdf4b0814b397638c2f718e96cfadf4c4e4"}, + {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97752ff12cc12f46a9b20327104448042fce5c33a624f88c18f66f9368091c7"}, + {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:894261472691d6fe76ebb7fcf2e5870a2ac284c7406ddc95823c8598a1390f0d"}, + {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5fa5d9eb82ce98959fc1031c28198b431b4d9396894f385cb63f1e2f3f20ca6b"}, + {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0fa751efb11a541f57db59c1dd821bec09031e01452b2b6217319b3a1f34f3d"}, + {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5346b93e62ab51ee2a9d68e8f73c7cf96ffb73568a23e683f931e52450e4148d"}, + {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:049ec0360f939cd164ecbfd2873eaa432613d5e77d6b04535e3d1fbae5a9e645"}, + {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b52dcf013b57464b6d1e51b627adfd69a8053e84b7103a7cd49c030f9ca44461"}, + {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9b2af240143dd2765e0fb661fd0361a1b469cab235039ea57663cda087250ea9"}, + {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ac77f709a2cde2cc71257ab2d8c74dd157c67a0558a0d2799d5d571b4c63d44d"}, + {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:47f6b962246f0a774fbd3b6b7be25d59b06fdb2f164cf2513097998fc6a29693"}, + {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:760fb7db442f284996e39cf9915a94492e1896baac44f06ae551974907922b64"}, + {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad702e57dc385cae679c39d318def49aef754455f237499d5b99bea4ef582e51"}, + {file = "aiohttp-3.12.15-cp313-cp313-win32.whl", hash = "sha256:f813c3e9032331024de2eb2e32a88d86afb69291fbc37a3a3ae81cc9917fb3d0"}, + {file = "aiohttp-3.12.15-cp313-cp313-win_amd64.whl", hash = "sha256:1a649001580bdb37c6fdb1bebbd7e3bc688e8ec2b5c6f52edbb664662b17dc84"}, + {file = "aiohttp-3.12.15-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:691d203c2bdf4f4637792efbbcdcd157ae11e55eaeb5e9c360c1206fb03d4d98"}, + {file = "aiohttp-3.12.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e995e1abc4ed2a454c731385bf4082be06f875822adc4c6d9eaadf96e20d406"}, + {file = "aiohttp-3.12.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bd44d5936ab3193c617bfd6c9a7d8d1085a8dc8c3f44d5f1dcf554d17d04cf7d"}, + {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46749be6e89cd78d6068cdf7da51dbcfa4321147ab8e4116ee6678d9a056a0cf"}, + {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0c643f4d75adea39e92c0f01b3fb83d57abdec8c9279b3078b68a3a52b3933b6"}, + {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0a23918fedc05806966a2438489dcffccbdf83e921a1170773b6178d04ade142"}, + {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:74bdd8c864b36c3673741023343565d95bfbd778ffe1eb4d412c135a28a8dc89"}, + {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a146708808c9b7a988a4af3821379e379e0f0e5e466ca31a73dbdd0325b0263"}, + {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7011a70b56facde58d6d26da4fec3280cc8e2a78c714c96b7a01a87930a9530"}, + {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3bdd6e17e16e1dbd3db74d7f989e8af29c4d2e025f9828e6ef45fbdee158ec75"}, + {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:57d16590a351dfc914670bd72530fd78344b885a00b250e992faea565b7fdc05"}, + {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:bc9a0f6569ff990e0bbd75506c8d8fe7214c8f6579cca32f0546e54372a3bb54"}, + {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:536ad7234747a37e50e7b6794ea868833d5220b49c92806ae2d7e8a9d6b5de02"}, + {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f0adb4177fa748072546fb650d9bd7398caaf0e15b370ed3317280b13f4083b0"}, + {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:14954a2988feae3987f1eb49c706bff39947605f4b6fa4027c1d75743723eb09"}, + {file = "aiohttp-3.12.15-cp39-cp39-win32.whl", hash = "sha256:b784d6ed757f27574dca1c336f968f4e81130b27595e458e69457e6878251f5d"}, + {file = "aiohttp-3.12.15-cp39-cp39-win_amd64.whl", hash = "sha256:86ceded4e78a992f835209e236617bffae649371c4a50d5e5a3987f237db84b8"}, + {file = "aiohttp-3.12.15.tar.gz", hash = "sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2"}, ] [package.dependencies] @@ -150,19 +150,19 @@ typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} [[package]] name = "alive-progress" -version = "3.2.0" +version = "3.3.0" description = "A new kind of Progress Bar, with real-time throughput, ETA, and very cool animations!" optional = false python-versions = "<4,>=3.9" groups = ["main"] files = [ - {file = "alive-progress-3.2.0.tar.gz", hash = "sha256:ede29d046ff454fe56b941f686f89dd9389430c4a5b7658e445cb0b80e0e4deb"}, - {file = "alive_progress-3.2.0-py3-none-any.whl", hash = "sha256:0677929f8d3202572e9d142f08170b34dbbe256cc6d2afbf75ef187c7da964a8"}, + {file = "alive-progress-3.3.0.tar.gz", hash = "sha256:457dd2428b48dacd49854022a46448d236a48f1b7277874071c39395307e830c"}, + {file = "alive_progress-3.3.0-py3-none-any.whl", hash = "sha256:63dd33bb94cde15ad9e5b666dbba8fedf71b72a4935d6fb9a92931e69402c9ff"}, ] [package.dependencies] about-time = "4.2.1" -grapheme = "0.6.0" +graphemeu = "0.7.2" [[package]] name = "amqp" @@ -179,16 +179,28 @@ files = [ [package.dependencies] vine = ">=5.0.0,<6.0.0" +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + [[package]] name = "anyio" -version = "4.9.0" -description = "High level compatibility layer for multiple asynchronous event loop implementations" +version = "4.10.0" +description = "High-level concurrency and networking framework on top of asyncio or Trio" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"}, - {file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"}, + {file = "anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1"}, + {file = "anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6"}, ] [package.dependencies] @@ -197,24 +209,22 @@ sniffio = ">=1.1" typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] -doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] -test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""] trio = ["trio (>=0.26.1)"] [[package]] name = "asgiref" -version = "3.8.1" +version = "3.9.1" description = "ASGI specs, helper code, and adapters" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, - {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, + {file = "asgiref-3.9.1-py3-none-any.whl", hash = "sha256:f3bba7092a48005b5f5bacd747d36ee4a5a61f4a269a6df590b43144355ebd2c"}, + {file = "asgiref-3.9.1.tar.gz", hash = "sha256:a5ab6582236218e5ef1648f242fd9f10626cfd4de8dc377db215d5d5098e3142"}, ] [package.extras] -tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] +tests = ["mypy (>=1.14.0)", "pytest", "pytest-asyncio"] [[package]] name = "astroid" @@ -263,14 +273,14 @@ tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" a [[package]] name = "authlib" -version = "1.5.2" +version = "1.6.1" 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.1-py2.py3-none-any.whl", hash = "sha256:e9d2031c34c6309373ab845afc24168fe9e93dc52d252631f52642f21f5ed06e"}, + {file = "authlib-1.6.1.tar.gz", hash = "sha256:4dffdbb1460ba6ec8c17981a4c67af7d8af131231b5a36a88a1e8c80c111cdfd"}, ] [package.dependencies] @@ -317,14 +327,14 @@ files = [ [[package]] name = "azure-core" -version = "1.33.0" +version = "1.35.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.35.0-py3-none-any.whl", hash = "sha256:8db78c72868a58f3de8991eb4d22c4d368fae226dac1002998d6c50437e7dad1"}, + {file = "azure_core-1.35.0.tar.gz", hash = "sha256:c0be528489485e9ede59b6971eb63c1eaacf83ef53001bfe3904e475e972be5c"}, ] [package.dependencies] @@ -464,18 +474,18 @@ typing-extensions = ">=4.6.0" [[package]] name = "azure-mgmt-core" -version = "1.5.0" +version = "1.6.0" description = "Microsoft Azure Management Core Library for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "azure_mgmt_core-1.5.0-py3-none-any.whl", hash = "sha256:18aaa5a723ee8ae05bf1bfc9f6d0ffb996631c7ea3c922cc86f522973ce07b5f"}, - {file = "azure_mgmt_core-1.5.0.tar.gz", hash = "sha256:380ae3dfa3639f4a5c246a7db7ed2d08374e88230fd0da3eb899f7c11e5c441a"}, + {file = "azure_mgmt_core-1.6.0-py3-none-any.whl", hash = "sha256:0460d11e85c408b71c727ee1981f74432bc641bb25dfcf1bb4e90a49e776dbc4"}, + {file = "azure_mgmt_core-1.6.0.tar.gz", hash = "sha256:b26232af857b021e61d813d9f4ae530465255cb10b3dde945ad3743f7a58e79c"}, ] [package.dependencies] -azure-core = ">=1.31.0" +azure-core = ">=1.32.0" [[package]] name = "azure-mgmt-cosmosdb" @@ -495,6 +505,23 @@ azure-mgmt-core = ">=1.3.2" isodate = ">=0.6.1" typing-extensions = ">=4.6.0" +[[package]] +name = "azure-mgmt-databricks" +version = "2.0.0" +description = "Microsoft Azure Data Bricks Management Client Library for Python" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "azure-mgmt-databricks-2.0.0.zip", hash = "sha256:70d11362dc2d17f5fb1db0cfe65c1af55b8f136f1a0db9a5b51e7acf760cf5b9"}, + {file = "azure_mgmt_databricks-2.0.0-py3-none-any.whl", hash = "sha256:0c29434a7339e74231bd171a6c08dcdf8153abaebd332658d7f66b8ea143fa17"}, +] + +[package.dependencies] +azure-common = ">=1.1,<2.0" +azure-mgmt-core = ">=1.3.2,<2.0.0" +isodate = ">=0.6.1,<1.0.0" + [[package]] name = "azure-mgmt-keyvault" version = "10.3.1" @@ -565,6 +592,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" @@ -759,34 +822,34 @@ files = [ [[package]] name = "boto3" -version = "1.35.99" +version = "1.39.15" description = "The AWS SDK for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "boto3-1.35.99-py3-none-any.whl", hash = "sha256:83e560faaec38a956dfb3d62e05e1703ee50432b45b788c09e25107c5058bd71"}, - {file = "boto3-1.35.99.tar.gz", hash = "sha256:e0abd794a7a591d90558e92e29a9f8837d25ece8e3c120e530526fe27eba5fca"}, + {file = "boto3-1.39.15-py3-none-any.whl", hash = "sha256:38fc54576b925af0075636752de9974e172c8a2cf7133400e3e09b150d20fb6a"}, + {file = "boto3-1.39.15.tar.gz", hash = "sha256:b4483625f0d8c35045254dee46cd3c851bbc0450814f20b9b25bee1b5c0d8409"}, ] [package.dependencies] -botocore = ">=1.35.99,<1.36.0" +botocore = ">=1.39.15,<1.40.0" jmespath = ">=0.7.1,<2.0.0" -s3transfer = ">=0.10.0,<0.11.0" +s3transfer = ">=0.13.0,<0.14.0" [package.extras] crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.35.99" +version = "1.39.15" description = "Low-level, data-driven core of boto 3." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "botocore-1.35.99-py3-none-any.whl", hash = "sha256:b22d27b6b617fc2d7342090d6129000af2efd20174215948c0d7ae2da0fab445"}, - {file = "botocore-1.35.99.tar.gz", hash = "sha256:1eab44e969c39c5f3d9a3104a0836c24715579a455f12b3979a31d7cde51b3c3"}, + {file = "botocore-1.39.15-py3-none-any.whl", hash = "sha256:eb9cfe918ebfbfb8654e1b153b29f0c129d586d2c0d7fb4032731d49baf04cff"}, + {file = "botocore-1.39.15.tar.gz", hash = "sha256:2aa29a717f14f8c7ca058c2e297aaed0aa10ecea24b91514eee802814d1b7600"}, ] [package.dependencies] @@ -795,7 +858,7 @@ python-dateutil = ">=2.1,<3.0.0" urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""} [package.extras] -crt = ["awscrt (==0.22.0)"] +crt = ["awscrt (==0.23.8)"] [[package]] name = "cachetools" @@ -869,14 +932,14 @@ zstd = ["zstandard (==0.22.0)"] [[package]] name = "certifi" -version = "2025.1.31" +version = "2025.8.3" description = "Python package for providing Mozilla's CA Bundle." optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" groups = ["main", "dev"] 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.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"}, + {file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"}, ] [[package]] @@ -962,116 +1025,103 @@ pycparser = "*" [[package]] name = "charset-normalizer" -version = "3.4.1" +version = "3.4.3" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" groups = ["main", "dev"] files = [ - {file = "charset_normalizer-3.4.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.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-win32.whl", hash = "sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-win32.whl", hash = "sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca"}, + {file = "charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a"}, + {file = "charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14"}, ] [[package]] name = "click" -version = "8.1.8" +version = "8.2.1" description = "Composable command line interface toolkit" optional = false -python-versions = ">=3.7" +python-versions = ">=3.10" groups = ["main", "dev"] files = [ - {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, - {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, + {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, + {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, ] [package.dependencies] @@ -1094,14 +1144,14 @@ click = ">=7" [[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] @@ -1142,6 +1192,18 @@ files = [ ] markers = {dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} +[[package]] +name = "contextlib2" +version = "21.6.0" +description = "Backports and enhancements for the contextlib module" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "contextlib2-21.6.0-py2.py3-none-any.whl", hash = "sha256:3fbdb64466afd23abaf6c977627b75b6139a5a3e8ce38405c5b413aed7a0471f"}, + {file = "contextlib2-21.6.0.tar.gz", hash = "sha256:ab1e2bfe1d01d968e1b7e8d9023bc51ef3509bba217bb730cee3827e1ee82869"}, +] + [[package]] name = "coverage" version = "7.5.4" @@ -1278,21 +1340,18 @@ test-randomorder = ["pytest-randomly"] [[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" @@ -1300,11 +1359,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)"] @@ -1312,92 +1372,56 @@ 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 = "debugpy" -version = "1.8.14" +version = "1.8.16" description = "An implementation of the Debug Adapter Protocol for Python" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "debugpy-1.8.14-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:93fee753097e85623cab1c0e6a68c76308cd9f13ffdf44127e6fab4fbf024339"}, - {file = "debugpy-1.8.14-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d937d93ae4fa51cdc94d3e865f535f185d5f9748efb41d0d49e33bf3365bd79"}, - {file = "debugpy-1.8.14-cp310-cp310-win32.whl", hash = "sha256:c442f20577b38cc7a9aafecffe1094f78f07fb8423c3dddb384e6b8f49fd2987"}, - {file = "debugpy-1.8.14-cp310-cp310-win_amd64.whl", hash = "sha256:f117dedda6d969c5c9483e23f573b38f4e39412845c7bc487b6f2648df30fe84"}, - {file = "debugpy-1.8.14-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:1b2ac8c13b2645e0b1eaf30e816404990fbdb168e193322be8f545e8c01644a9"}, - {file = "debugpy-1.8.14-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf431c343a99384ac7eab2f763980724834f933a271e90496944195318c619e2"}, - {file = "debugpy-1.8.14-cp311-cp311-win32.whl", hash = "sha256:c99295c76161ad8d507b413cd33422d7c542889fbb73035889420ac1fad354f2"}, - {file = "debugpy-1.8.14-cp311-cp311-win_amd64.whl", hash = "sha256:7816acea4a46d7e4e50ad8d09d963a680ecc814ae31cdef3622eb05ccacf7b01"}, - {file = "debugpy-1.8.14-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:8899c17920d089cfa23e6005ad9f22582fd86f144b23acb9feeda59e84405b84"}, - {file = "debugpy-1.8.14-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6bb5c0dcf80ad5dbc7b7d6eac484e2af34bdacdf81df09b6a3e62792b722826"}, - {file = "debugpy-1.8.14-cp312-cp312-win32.whl", hash = "sha256:281d44d248a0e1791ad0eafdbbd2912ff0de9eec48022a5bfbc332957487ed3f"}, - {file = "debugpy-1.8.14-cp312-cp312-win_amd64.whl", hash = "sha256:5aa56ef8538893e4502a7d79047fe39b1dae08d9ae257074c6464a7b290b806f"}, - {file = "debugpy-1.8.14-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:329a15d0660ee09fec6786acdb6e0443d595f64f5d096fc3e3ccf09a4259033f"}, - {file = "debugpy-1.8.14-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f920c7f9af409d90f5fd26e313e119d908b0dd2952c2393cd3247a462331f15"}, - {file = "debugpy-1.8.14-cp313-cp313-win32.whl", hash = "sha256:3784ec6e8600c66cbdd4ca2726c72d8ca781e94bce2f396cc606d458146f8f4e"}, - {file = "debugpy-1.8.14-cp313-cp313-win_amd64.whl", hash = "sha256:684eaf43c95a3ec39a96f1f5195a7ff3d4144e4a18d69bb66beeb1a6de605d6e"}, - {file = "debugpy-1.8.14-cp38-cp38-macosx_14_0_x86_64.whl", hash = "sha256:d5582bcbe42917bc6bbe5c12db1bffdf21f6bfc28d4554b738bf08d50dc0c8c3"}, - {file = "debugpy-1.8.14-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5349b7c3735b766a281873fbe32ca9cca343d4cc11ba4a743f84cb854339ff35"}, - {file = "debugpy-1.8.14-cp38-cp38-win32.whl", hash = "sha256:7118d462fe9724c887d355eef395fae68bc764fd862cdca94e70dcb9ade8a23d"}, - {file = "debugpy-1.8.14-cp38-cp38-win_amd64.whl", hash = "sha256:d235e4fa78af2de4e5609073972700523e372cf5601742449970110d565ca28c"}, - {file = "debugpy-1.8.14-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:413512d35ff52c2fb0fd2d65e69f373ffd24f0ecb1fac514c04a668599c5ce7f"}, - {file = "debugpy-1.8.14-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c9156f7524a0d70b7a7e22b2e311d8ba76a15496fb00730e46dcdeedb9e1eea"}, - {file = "debugpy-1.8.14-cp39-cp39-win32.whl", hash = "sha256:b44985f97cc3dd9d52c42eb59ee9d7ee0c4e7ecd62bca704891f997de4cef23d"}, - {file = "debugpy-1.8.14-cp39-cp39-win_amd64.whl", hash = "sha256:b1528cfee6c1b1c698eb10b6b096c598738a8238822d218173d21c3086de8123"}, - {file = "debugpy-1.8.14-py2.py3-none-any.whl", hash = "sha256:5cd9a579d553b6cb9759a7908a41988ee6280b961f24f63336835d9418216a20"}, - {file = "debugpy-1.8.14.tar.gz", hash = "sha256:7cd287184318416850aa8b60ac90105837bb1e59531898c07569d197d2ed5322"}, + {file = "debugpy-1.8.16-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:2a3958fb9c2f40ed8ea48a0d34895b461de57a1f9862e7478716c35d76f56c65"}, + {file = "debugpy-1.8.16-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5ca7314042e8a614cc2574cd71f6ccd7e13a9708ce3c6d8436959eae56f2378"}, + {file = "debugpy-1.8.16-cp310-cp310-win32.whl", hash = "sha256:8624a6111dc312ed8c363347a0b59c5acc6210d897e41a7c069de3c53235c9a6"}, + {file = "debugpy-1.8.16-cp310-cp310-win_amd64.whl", hash = "sha256:fee6db83ea5c978baf042440cfe29695e1a5d48a30147abf4c3be87513609817"}, + {file = "debugpy-1.8.16-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:67371b28b79a6a12bcc027d94a06158f2fde223e35b5c4e0783b6f9d3b39274a"}, + {file = "debugpy-1.8.16-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2abae6dd02523bec2dee16bd6b0781cccb53fd4995e5c71cc659b5f45581898"}, + {file = "debugpy-1.8.16-cp311-cp311-win32.whl", hash = "sha256:f8340a3ac2ed4f5da59e064aa92e39edd52729a88fbde7bbaa54e08249a04493"}, + {file = "debugpy-1.8.16-cp311-cp311-win_amd64.whl", hash = "sha256:70f5fcd6d4d0c150a878d2aa37391c52de788c3dc680b97bdb5e529cb80df87a"}, + {file = "debugpy-1.8.16-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:b202e2843e32e80b3b584bcebfe0e65e0392920dc70df11b2bfe1afcb7a085e4"}, + {file = "debugpy-1.8.16-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64473c4a306ba11a99fe0bb14622ba4fbd943eb004847d9b69b107bde45aa9ea"}, + {file = "debugpy-1.8.16-cp312-cp312-win32.whl", hash = "sha256:833a61ed446426e38b0dd8be3e9d45ae285d424f5bf6cd5b2b559c8f12305508"}, + {file = "debugpy-1.8.16-cp312-cp312-win_amd64.whl", hash = "sha256:75f204684581e9ef3dc2f67687c3c8c183fde2d6675ab131d94084baf8084121"}, + {file = "debugpy-1.8.16-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:85df3adb1de5258dca910ae0bb185e48c98801ec15018a263a92bb06be1c8787"}, + {file = "debugpy-1.8.16-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bee89e948bc236a5c43c4214ac62d28b29388453f5fd328d739035e205365f0b"}, + {file = "debugpy-1.8.16-cp313-cp313-win32.whl", hash = "sha256:cf358066650439847ec5ff3dae1da98b5461ea5da0173d93d5e10f477c94609a"}, + {file = "debugpy-1.8.16-cp313-cp313-win_amd64.whl", hash = "sha256:b5aea1083f6f50023e8509399d7dc6535a351cc9f2e8827d1e093175e4d9fa4c"}, + {file = "debugpy-1.8.16-cp38-cp38-macosx_14_0_x86_64.whl", hash = "sha256:2801329c38f77c47976d341d18040a9ac09d0c71bf2c8b484ad27c74f83dc36f"}, + {file = "debugpy-1.8.16-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:687c7ab47948697c03b8f81424aa6dc3f923e6ebab1294732df1ca9773cc67bc"}, + {file = "debugpy-1.8.16-cp38-cp38-win32.whl", hash = "sha256:a2ba6fc5d7c4bc84bcae6c5f8edf5988146e55ae654b1bb36fecee9e5e77e9e2"}, + {file = "debugpy-1.8.16-cp38-cp38-win_amd64.whl", hash = "sha256:d58c48d8dbbbf48a3a3a638714a2d16de537b0dace1e3432b8e92c57d43707f8"}, + {file = "debugpy-1.8.16-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:135ccd2b1161bade72a7a099c9208811c137a150839e970aeaf121c2467debe8"}, + {file = "debugpy-1.8.16-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:211238306331a9089e253fd997213bc4a4c65f949271057d6695953254095376"}, + {file = "debugpy-1.8.16-cp39-cp39-win32.whl", hash = "sha256:88eb9ffdfb59bf63835d146c183d6dba1f722b3ae2a5f4b9fc03e925b3358922"}, + {file = "debugpy-1.8.16-cp39-cp39-win_amd64.whl", hash = "sha256:c2c47c2e52b40449552843b913786499efcc3dbc21d6c49287d939cd0dbc49fd"}, + {file = "debugpy-1.8.16-py2.py3-none-any.whl", hash = "sha256:19c9521962475b87da6f673514f7fd610328757ec993bf7ec0d8c96f9a325f9e"}, + {file = "debugpy-1.8.16.tar.gz", hash = "sha256:31e69a1feb1cf6b51efbed3f6c9b0ef03bc46ff050679c4be7ea6d2e23540870"}, ] [[package]] @@ -1508,40 +1532,42 @@ bcrypt = ["bcrypt"] [[package]] name = "django-allauth" -version = "65.8.0" +version = "65.11.0" description = "Integrated set of Django applications addressing authentication, registration, account management as well as 3rd party (social) account authentication." optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "django_allauth-65.8.0.tar.gz", hash = "sha256:9da589d99d412740629333a01865a90c95c97e0fae0cde789aa45a8fda90e83b"}, + {file = "django_allauth-65.11.0.tar.gz", hash = "sha256:d08ee0b60a1a54f84720bb749518628c517c9af40b6cfb3bc980206e182745ab"}, ] [package.dependencies] asgiref = ">=3.8.1" Django = ">=4.2.16" -pyjwt = {version = ">=1.7", extras = ["crypto"], optional = true, markers = "extra == \"socialaccount\""} +oauthlib = {version = ">=3.3.0,<4", optional = true, markers = "extra == \"socialaccount\""} +pyjwt = {version = ">=2.0,<3", extras = ["crypto"], optional = true, markers = "extra == \"socialaccount\""} python3-saml = {version = ">=1.15.0,<2.0.0", optional = true, markers = "extra == \"saml\""} -requests = {version = ">=2.0.0", optional = true, markers = "extra == \"socialaccount\""} -requests-oauthlib = {version = ">=0.3.0", optional = true, markers = "extra == \"socialaccount\""} +requests = {version = ">=2.0.0,<3", optional = true, markers = "extra == \"socialaccount\""} [package.extras] -mfa = ["fido2 (>=1.1.2)", "qrcode (>=7.0.0)"] -openid = ["python3-openid (>=3.0.8)"] +headless-spec = ["PyYAML (>=6,<7)"] +idp-oidc = ["oauthlib (>=3.3.0,<4)", "pyjwt[crypto] (>=2.0,<3)"] +mfa = ["fido2 (>=1.1.2,<3)", "qrcode (>=7.0.0,<9)"] +openid = ["python3-openid (>=3.0.8,<4)"] saml = ["python3-saml (>=1.15.0,<2.0.0)"] -socialaccount = ["pyjwt[crypto] (>=1.7)", "requests (>=2.0.0)", "requests-oauthlib (>=0.3.0)"] -steam = ["python3-openid (>=3.0.8)"] +socialaccount = ["oauthlib (>=3.3.0,<4)", "pyjwt[crypto] (>=2.0,<3)", "requests (>=2.0.0,<3)"] +steam = ["python3-openid (>=3.0.8,<4)"] [[package]] name = "django-celery-beat" -version = "2.8.0" +version = "2.8.1" description = "Database-backed Periodic Tasks." optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "django_celery_beat-2.8.0-py3-none-any.whl", hash = "sha256:f8fd2e1ffbfa8e570ab9439383b2cd15a6642b347662d0de79c62ba6f68d4b38"}, - {file = "django_celery_beat-2.8.0.tar.gz", hash = "sha256:955bfb3c4b8f1026a8d20144d0da39c941e1eb23acbaee9e12a7e7cc1f74959a"}, + {file = "django_celery_beat-2.8.1-py3-none-any.whl", hash = "sha256:da2b1c6939495c05a551717509d6e3b79444e114a027f7b77bf3727c2a39d171"}, + {file = "django_celery_beat-2.8.1.tar.gz", hash = "sha256:dfad0201c0ac50c91a34700ef8fa0a10ee098cc7f3375fe5debed79f2204f80a"}, ] [package.dependencies] @@ -1633,14 +1659,14 @@ django = {version = ">=4.0,<6.0", markers = "python_version >= \"3.10\""} [[package]] name = "django-postgres-extra" -version = "2.0.9rc11" +version = "2.0.9" description = "Bringing all of PostgreSQL's awesomeness to Django." optional = false python-versions = ">=3.6" groups = ["main"] files = [ - {file = "django_postgres_extra-2.0.9rc11-py3-none-any.whl", hash = "sha256:23fb08261963bebf6560a2bb248b2c404f9c5f650734472ff074977952efbd56"}, - {file = "django_postgres_extra-2.0.9rc11.tar.gz", hash = "sha256:a7738125c84d133d1dbbdeab66dd14e897e01284b5e1e02e588c84891c8b8ded"}, + {file = "django_postgres_extra-2.0.9-py3-none-any.whl", hash = "sha256:0a4f9fd7f843e2ef5cfe7a291fcb883bd72d2ca8e4b369d0070866205e00b404"}, + {file = "django_postgres_extra-2.0.9.tar.gz", hash = "sha256:9e47c436d033712d0e2611b8c1583566dd4f97700e5360001bf3623913a92046"}, ] [package.dependencies] @@ -1725,25 +1751,25 @@ openapi = ["pyyaml (>=5.4)", "uritemplate (>=3.0.1)"] [[package]] name = "djangorestframework-simplejwt" -version = "5.5.0" +version = "5.5.1" description = "A minimal JSON Web Token authentication plugin for Django REST Framework" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "djangorestframework_simplejwt-5.5.0-py3-none-any.whl", hash = "sha256:4ef6b38af20cdde4a4a51d1fd8e063cbbabb7b45f149cc885d38d905c5a62edb"}, - {file = "djangorestframework_simplejwt-5.5.0.tar.gz", hash = "sha256:474a1b737067e6462b3609627a392d13a4da8a08b1f0574104ac6d7b1406f90e"}, + {file = "djangorestframework_simplejwt-5.5.1-py3-none-any.whl", hash = "sha256:2c30f3707053d384e9f315d11c2daccfcb548d4faa453111ca19a542b732e469"}, + {file = "djangorestframework_simplejwt-5.5.1.tar.gz", hash = "sha256:e72c5572f51d7803021288e2057afcbd03f17fe11d484096f40a460abc76e87f"}, ] [package.dependencies] django = ">=4.2" djangorestframework = ">=3.14" -pyjwt = ">=1.7.1,<2.10.0" +pyjwt = ">=1.7.1" [package.extras] crypto = ["cryptography (>=3.3.1)"] -dev = ["Sphinx (>=1.6.5,<2)", "cryptography", "freezegun", "ipython", "pre-commit", "pytest", "pytest-cov", "pytest-django", "pytest-watch", "pytest-xdist", "python-jose (==3.3.0)", "pyupgrade", "ruff", "sphinx_rtd_theme (>=0.1.9)", "tox", "twine", "wheel", "yesqa"] -doc = ["Sphinx (>=1.6.5,<2)", "sphinx_rtd_theme (>=0.1.9)"] +dev = ["Sphinx", "cryptography", "freezegun", "ipython", "pre-commit", "pytest", "pytest-cov", "pytest-django", "pytest-watch", "pytest-xdist", "python-jose (==3.3.0)", "pyupgrade", "ruff", "sphinx_rtd_theme (>=0.1.9)", "tox", "twine", "wheel", "yesqa"] +doc = ["Sphinx", "sphinx_rtd_theme (>=0.1.9)"] lint = ["pre-commit", "pyupgrade", "ruff", "yesqa"] python-jose = ["python-jose (==3.3.0)"] test = ["cryptography", "freezegun", "pytest", "pytest-cov", "pytest-django", "pytest-xdist", "tox"] @@ -1832,19 +1858,19 @@ packaging = ">=24.1" [[package]] name = "drf-nested-routers" -version = "0.94.1" +version = "0.94.2" description = "Nested resources for the Django Rest Framework" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "drf-nested-routers-0.94.1.tar.gz", hash = "sha256:2b846385ed95c9f17bf4242db3b264ac826b5af00dda6c737d3fe7cc7bf2c7db"}, - {file = "drf_nested_routers-0.94.1-py2.py3-none-any.whl", hash = "sha256:3a8ec45a025c0f39188ec1ec415244beb875a6f4db87911a1f5a606d09b68c9f"}, + {file = "drf_nested_routers-0.94.2-py2.py3-none-any.whl", hash = "sha256:74dbdceeae2a32f8668ba0df8e3eeabeb9b1c64d2621d914901ae653e4e3bcff"}, + {file = "drf_nested_routers-0.94.2.tar.gz", hash = "sha256:aa70923b716dc47cd93b8129b06be6c15706b405cf5f718f59cb8eed01de59cc"}, ] [package.dependencies] Django = ">=4.2" -djangorestframework = ">=3.14.0" +djangorestframework = ">=3.15.0" [[package]] name = "drf-spectacular" @@ -1889,16 +1915,64 @@ djangorestframework-jsonapi = ">=6.0.0" drf-extensions = ">=0.7.1" drf-spectacular = ">=0.25.0" +[[package]] +name = "dulwich" +version = "0.23.0" +description = "Python Git Library" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "dulwich-0.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c13b0d5a9009cde23ecb8cb201df6e23e2a7a82c5e2d6ba6443fbb322c9befc6"}, + {file = "dulwich-0.23.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:a68faf8612bf93de1285048d6ad13160f0fb3c5596a86e694e78f4e212886fa5"}, + {file = "dulwich-0.23.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:d971566826f16ec67c70641c1fbdb337323aa5b533799bc5a4641f4750e73b36"}, + {file = "dulwich-0.23.0-cp310-cp310-win32.whl", hash = "sha256:27d970adf539806dfc4fe3e4c9e8dc6ebf0318977a56e24d22f13413535a51ba"}, + {file = "dulwich-0.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:025178533e884ffdb0d9d8db4b8870745d438cbfecb782fd1b56c3b6438e86cf"}, + {file = "dulwich-0.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d68498fdda13ab00791b483daab3bcfe9f9721c037aa458695e6ad81640c57cc"}, + {file = "dulwich-0.23.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:cb7bb930b12471a1cfcea4b3d25a671dc0ad32573f0ad25684684298959a1527"}, + {file = "dulwich-0.23.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a2abbce32fd2bc7902bcc5f69b10bf22576810de21651baaa864b78fd7aec261"}, + {file = "dulwich-0.23.0-cp311-cp311-win32.whl", hash = "sha256:9e3151f10ce2a9ff91bca64c74345217f53bdd947dc958032343822009832f7a"}, + {file = "dulwich-0.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:3ae9f1d9dc92d4e9a3f89ba2c55221f7b6442c5dd93b3f6f539a3c9eb3f37bdd"}, + {file = "dulwich-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:52cdef66a7994d29528ca79ca59452518bbba3fd56a9c61c61f6c467c1c7956e"}, + {file = "dulwich-0.23.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:d473888a6ab9ed5d4a4c3f053cbe5b77f72d54b6efdf5688fed76094316e571e"}, + {file = "dulwich-0.23.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:19fcf20224c641a61c774da92f098fbaae9938c7e17a52841e64092adf7e78f9"}, + {file = "dulwich-0.23.0-cp312-cp312-win32.whl", hash = "sha256:7fc8b76b704ef35cd001e993e3aa4e1d666a2064bf467c07c560f12b2959dcaf"}, + {file = "dulwich-0.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:cb0566b888b578325350b4d67c61a0de35d417e9877560e3a6df88cae4576a59"}, + {file = "dulwich-0.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:624e2223c8b705b3a217f9c8d3bfed3a573093be0b0ba033c46cba8411fb9630"}, + {file = "dulwich-0.23.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:b4eaf326d15bb3fc5316c777b0312f0fe02f6f82a4368cd971d0ce2167b7ec34"}, + {file = "dulwich-0.23.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:d754afaf7c133a015c75cc2be11703138b4be932e0eeeb2c70add56083f31109"}, + {file = "dulwich-0.23.0-cp313-cp313-win32.whl", hash = "sha256:ac53ec438bde3c1f479782c34240479b36cd47230d091979137b7ecc12c0242e"}, + {file = "dulwich-0.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:50d3b4ba45671fb8b7d2afbd02c10b4edbc3290a1f92260e64098b409e9ca35c"}, + {file = "dulwich-0.23.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d8e18ea3fa49f10932077f39c0b960b5045870c550c3d7c74f3cfaac09457cd6"}, + {file = "dulwich-0.23.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:3e6df0eb8cca21f210e3ddce2ccb64482646893dbec2fee9f3411d037595bf7b"}, + {file = "dulwich-0.23.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:90c0064d7df8e7fe83d3a03c7d60b9e07a92698b18442f926199b2c3f0bf34d4"}, + {file = "dulwich-0.23.0-cp39-cp39-win32.whl", hash = "sha256:84eef513aba501cbc1f223863f3b4b351fe732d3fb590cab9bdf5d33eb1a1248"}, + {file = "dulwich-0.23.0-cp39-cp39-win_amd64.whl", hash = "sha256:dce943da48217c26e15790fd6df62d27a7f1d067102780351ebf2635fc0ba482"}, + {file = "dulwich-0.23.0-py3-none-any.whl", hash = "sha256:d8da6694ca332bb48775e35ee2215aa4673821164a91b83062f699c69f7cd135"}, + {file = "dulwich-0.23.0.tar.gz", hash = "sha256:0aa6c2489dd5e978b27e9b75983b7331a66c999f0efc54ebe37cab808ed322ae"}, +] + +[package.dependencies] +urllib3 = ">=1.25" + +[package.extras] +dev = ["dissolve (>=0.1.1)", "mypy (==1.16.0)", "ruff (==0.11.13)"] +fastimport = ["fastimport"] +https = ["urllib3 (>=1.24.1)"] +merge = ["merge3"] +paramiko = ["paramiko"] +pgp = ["gpg"] + [[package]] name = "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]] @@ -1951,22 +2025,23 @@ typing = ["typing-extensions (>=4.7.1) ; python_version < \"3.11\""] [[package]] name = "flask" -version = "3.0.3" +version = "3.1.2" 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.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c"}, + {file = "flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87"}, ] [package.dependencies] -blinker = ">=1.6.2" +blinker = ">=1.9.0" click = ">=8.1.3" -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)"] @@ -1989,128 +2064,128 @@ 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]] 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] @@ -2121,10 +2196,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" @@ -2147,14 +2222,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] @@ -2219,18 +2294,20 @@ files = [ ] [[package]] -name = "grapheme" -version = "0.6.0" +name = "graphemeu" +version = "0.7.2" description = "Unicode grapheme helpers" optional = false -python-versions = "*" +python-versions = ">=3.7" groups = ["main"] files = [ - {file = "grapheme-0.6.0.tar.gz", hash = "sha256:44c2b9f21bbe77cfb05835fec230bd435954275267fea1858013b102f8603cca"}, + {file = "graphemeu-0.7.2-py3-none-any.whl", hash = "sha256:1444520f6899fd30114fc2a39f297d86d10fa0f23bf7579f772f8bc7efaa2542"}, + {file = "graphemeu-0.7.2.tar.gz", hash = "sha256:42bbe373d7c146160f286cd5f76b1a8ad29172d7333ce10705c5cc282462a4f8"}, ] [package.extras] -test = ["pytest", "sphinx", "sphinx-autobuild", "twine", "wheel"] +dev = ["pytest"] +docs = ["sphinx", "sphinx-autobuild"] [[package]] name = "gunicorn" @@ -2268,14 +2345,14 @@ files = [ [[package]] name = "h2" -version = "4.2.0" +version = "4.3.0" description = "Pure-Python HTTP/2 protocol implementation" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "h2-4.2.0-py3-none-any.whl", hash = "sha256:479a53ad425bb29af087f3458a61d30780bc818e4ebcf01f0b536ba916462ed0"}, - {file = "h2-4.2.0.tar.gz", hash = "sha256:c8a52129695e88b1a0578d8d2cc6842bbd79128ac685463b887ee278126ad01f"}, + {file = "h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd"}, + {file = "h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1"}, ] [package.dependencies] @@ -2369,6 +2446,18 @@ files = [ {file = "hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08"}, ] +[[package]] +name = "iamdata" +version = "0.1.202507291" +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.202507291-py3-none-any.whl", hash = "sha256:11dfdacc3ce0312468aa5ccafee461cd39b1deb7be112042deea91cbcd4b292b"}, + {file = "iamdata-0.1.202507291.tar.gz", hash = "sha256:b386ce94819464554dc1258238ee1b232d86f0467edc13fffbf4de7332b3c7ad"}, +] + [[package]] name = "idna" version = "3.10" @@ -2386,14 +2475,14 @@ all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2 [[package]] name = "importlib-metadata" -version = "8.6.1" +version = "8.7.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e"}, - {file = "importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580"}, + {file = "importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"}, + {file = "importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"}, ] [package.dependencies] @@ -2612,14 +2701,14 @@ format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339- [[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"] 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] @@ -2627,18 +2716,19 @@ referencing = ">=0.31.0" [[package]] name = "kombu" -version = "5.5.3" +version = "5.5.4" description = "Messaging library for Python." optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "kombu-5.5.3-py3-none-any.whl", hash = "sha256:5b0dbceb4edee50aa464f59469d34b97864be09111338cfb224a10b6a163909b"}, - {file = "kombu-5.5.3.tar.gz", hash = "sha256:021a0e11fcfcd9b0260ef1fb64088c0e92beb976eb59c1dfca7ddd4ad4562ea2"}, + {file = "kombu-5.5.4-py3-none-any.whl", hash = "sha256:a12ed0557c238897d8e518f1d1fdf84bd1516c5e305af2dacd85c2015115feb8"}, + {file = "kombu-5.5.4.tar.gz", hash = "sha256:886600168275ebeada93b888e831352fe578168342f0d1d5833d88ba0d847363"}, ] [package.dependencies] amqp = ">=5.1.1,<6.0.0" +packaging = "*" tzdata = {version = ">=2025.2", markers = "python_version >= \"3.9\""} vine = "5.1.0" @@ -2649,7 +2739,7 @@ confluentkafka = ["confluent-kafka (>=2.2.0)"] consul = ["python-consul2 (==0.1.5)"] gcpubsub = ["google-cloud-monitoring (>=2.16.0)", "google-cloud-pubsub (>=2.18.4)", "grpcio (==1.67.0)", "protobuf (==4.25.5)"] librabbitmq = ["librabbitmq (>=2.0.0) ; python_version < \"3.11\""] -mongodb = ["pymongo (>=4.1.1)"] +mongodb = ["pymongo (==4.10.1)"] msgpack = ["msgpack (==1.1.0)"] pyro = ["pyro4 (==4.82)"] qpid = ["qpid-python (>=0.26)", "qpid-tools (>=0.26)"] @@ -2845,14 +2935,14 @@ source = ["Cython (>=3.0.11,<3.1.0)"] [[package]] name = "markdown-it-py" -version = "3.0.0" +version = "4.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, - {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, + {file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"}, + {file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"}, ] [package.dependencies] @@ -2860,13 +2950,12 @@ mdurl = ">=0.1,<1.0" [package.extras] benchmarking = ["psutil", "pytest", "pytest-benchmark"] -code-style = ["pre-commit (>=3.0,<4.0)"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"] linkify = ["linkify-it-py (>=1,<3)"] -plugins = ["mdit-py-plugins"] +plugins = ["mdit-py-plugins (>=0.5.0)"] profiling = ["gprof2dot"] -rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] +rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "requests"] [[package]] name = "markupsafe" @@ -3099,23 +3188,23 @@ microsoft-kiota-abstractions = ">=1.9.2,<1.10.0" [[package]] name = "msal" -version = "1.32.0" +version = "1.33.0" description = "The Microsoft Authentication Library (MSAL) for Python library enables your app to access the Microsoft Cloud by supporting authentication of users with Microsoft Azure Active Directory accounts (AAD) and Microsoft Accounts (MSA) using industry standard OAuth2 and OpenID Connect." optional = false python-versions = ">=3.7" groups = ["main"] files = [ - {file = "msal-1.32.0-py3-none-any.whl", hash = "sha256:9dbac5384a10bbbf4dae5c7ea0d707d14e087b92c5aa4954b3feaa2d1aa0bcb7"}, - {file = "msal-1.32.0.tar.gz", hash = "sha256:5445fe3af1da6be484991a7ab32eaa82461dc2347de105b76af92c610c3335c2"}, + {file = "msal-1.33.0-py3-none-any.whl", hash = "sha256:c0cd41cecf8eaed733ee7e3be9e040291eba53b0f262d3ae9c58f38b04244273"}, + {file = "msal-1.33.0.tar.gz", hash = "sha256:836ad80faa3e25a7d71015c990ce61f704a87328b1e73bcbb0623a18cbf17510"}, ] [package.dependencies] -cryptography = ">=2.5,<47" +cryptography = ">=2.5,<48" PyJWT = {version = ">=1.0.0,<3", extras = ["crypto"]} requests = ">=2.0.0,<3" [package.extras] -broker = ["pymsalruntime (>=0.14,<0.18) ; python_version >= \"3.6\" and platform_system == \"Windows\"", "pymsalruntime (>=0.17,<0.18) ; python_version >= \"3.8\" and platform_system == \"Darwin\""] +broker = ["pymsalruntime (>=0.14,<0.19) ; python_version >= \"3.6\" and platform_system == \"Windows\"", "pymsalruntime (>=0.17,<0.19) ; python_version >= \"3.8\" and platform_system == \"Darwin\"", "pymsalruntime (>=0.18,<0.19) ; python_version >= \"3.8\" and platform_system == \"Linux\""] [[package]] name = "msal-extensions" @@ -3137,14 +3226,14 @@ portalocker = ["portalocker (>=1.4,<4)"] [[package]] name = "msgraph-core" -version = "1.3.3" +version = "1.3.5" description = "Core component of the Microsoft Graph Python SDK" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "msgraph_core-1.3.3-py3-none-any.whl", hash = "sha256:9dbbc0c88e174c1d5da1c17d286965d6b26ebaf24996c7af64a39e2069006cf6"}, - {file = "msgraph_core-1.3.3.tar.gz", hash = "sha256:a3226b08b4cf9b6dbb16b868be21d5f82d8ee514ae8e46d9f0cad896159ef8d3"}, + {file = "msgraph_core-1.3.5-py3-none-any.whl", hash = "sha256:bc496c6f99c626bc534012c6fe9afa35c37bcdce0f92acf26e4210f4ff9bb154"}, + {file = "msgraph_core-1.3.5.tar.gz", hash = "sha256:43aec9df1c011f1c6a1e14f2b5e9266c05a723ed750a5d3ea1eb0c0f1deb9975"}, ] [package.dependencies] @@ -3203,116 +3292,122 @@ async = ["aiodns ; python_version >= \"3.5\"", "aiohttp (>=3.0) ; python_version [[package]] name = "multidict" -version = "6.4.3" +version = "6.6.4" 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.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b8aa6f0bd8125ddd04a6593437bad6a7e70f300ff4180a531654aa2ab3f6d58f"}, + {file = "multidict-6.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b9e5853bbd7264baca42ffc53391b490d65fe62849bf2c690fa3f6273dbcd0cb"}, + {file = "multidict-6.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0af5f9dee472371e36d6ae38bde009bd8ce65ac7335f55dcc240379d7bed1495"}, + {file = "multidict-6.6.4-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:d24f351e4d759f5054b641c81e8291e5d122af0fca5c72454ff77f7cbe492de8"}, + {file = "multidict-6.6.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db6a3810eec08280a172a6cd541ff4a5f6a97b161d93ec94e6c4018917deb6b7"}, + {file = "multidict-6.6.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a1b20a9d56b2d81e2ff52ecc0670d583eaabaa55f402e8d16dd062373dbbe796"}, + {file = "multidict-6.6.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8c9854df0eaa610a23494c32a6f44a3a550fb398b6b51a56e8c6b9b3689578db"}, + {file = "multidict-6.6.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4bb7627fd7a968f41905a4d6343b0d63244a0623f006e9ed989fa2b78f4438a0"}, + {file = "multidict-6.6.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caebafea30ed049c57c673d0b36238b1748683be2593965614d7b0e99125c877"}, + {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ad887a8250eb47d3ab083d2f98db7f48098d13d42eb7a3b67d8a5c795f224ace"}, + {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:ed8358ae7d94ffb7c397cecb62cbac9578a83ecefc1eba27b9090ee910e2efb6"}, + {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ecab51ad2462197a4c000b6d5701fc8585b80eecb90583635d7e327b7b6923eb"}, + {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c5c97aa666cf70e667dfa5af945424ba1329af5dd988a437efeb3a09430389fb"}, + {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:9a950b7cf54099c1209f455ac5970b1ea81410f2af60ed9eb3c3f14f0bfcf987"}, + {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:163c7ea522ea9365a8a57832dea7618e6cbdc3cd75f8c627663587459a4e328f"}, + {file = "multidict-6.6.4-cp310-cp310-win32.whl", hash = "sha256:17d2cbbfa6ff20821396b25890f155f40c986f9cfbce5667759696d83504954f"}, + {file = "multidict-6.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:ce9a40fbe52e57e7edf20113a4eaddfacac0561a0879734e636aa6d4bb5e3fb0"}, + {file = "multidict-6.6.4-cp310-cp310-win_arm64.whl", hash = "sha256:01d0959807a451fe9fdd4da3e139cb5b77f7328baf2140feeaf233e1d777b729"}, + {file = "multidict-6.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c7a0e9b561e6460484318a7612e725df1145d46b0ef57c6b9866441bf6e27e0c"}, + {file = "multidict-6.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6bf2f10f70acc7a2446965ffbc726e5fc0b272c97a90b485857e5c70022213eb"}, + {file = "multidict-6.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66247d72ed62d5dd29752ffc1d3b88f135c6a8de8b5f63b7c14e973ef5bda19e"}, + {file = "multidict-6.6.4-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:105245cc6b76f51e408451a844a54e6823bbd5a490ebfe5bdfc79798511ceded"}, + {file = "multidict-6.6.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cbbc54e58b34c3bae389ef00046be0961f30fef7cb0dd9c7756aee376a4f7683"}, + {file = "multidict-6.6.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:56c6b3652f945c9bc3ac6c8178cd93132b8d82dd581fcbc3a00676c51302bc1a"}, + {file = "multidict-6.6.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b95494daf857602eccf4c18ca33337dd2be705bccdb6dddbfc9d513e6addb9d9"}, + {file = "multidict-6.6.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e5b1413361cef15340ab9dc61523e653d25723e82d488ef7d60a12878227ed50"}, + {file = "multidict-6.6.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e167bf899c3d724f9662ef00b4f7fef87a19c22b2fead198a6f68b263618df52"}, + {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aaea28ba20a9026dfa77f4b80369e51cb767c61e33a2d4043399c67bd95fb7c6"}, + {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8c91cdb30809a96d9ecf442ec9bc45e8cfaa0f7f8bdf534e082c2443a196727e"}, + {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1a0ccbfe93ca114c5d65a2471d52d8829e56d467c97b0e341cf5ee45410033b3"}, + {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:55624b3f321d84c403cb7d8e6e982f41ae233d85f85db54ba6286f7295dc8a9c"}, + {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4a1fb393a2c9d202cb766c76208bd7945bc194eba8ac920ce98c6e458f0b524b"}, + {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:43868297a5759a845fa3a483fb4392973a95fb1de891605a3728130c52b8f40f"}, + {file = "multidict-6.6.4-cp311-cp311-win32.whl", hash = "sha256:ed3b94c5e362a8a84d69642dbeac615452e8af9b8eb825b7bc9f31a53a1051e2"}, + {file = "multidict-6.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:d8c112f7a90d8ca5d20213aa41eac690bb50a76da153e3afb3886418e61cb22e"}, + {file = "multidict-6.6.4-cp311-cp311-win_arm64.whl", hash = "sha256:3bb0eae408fa1996d87247ca0d6a57b7fc1dcf83e8a5c47ab82c558c250d4adf"}, + {file = "multidict-6.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0ffb87be160942d56d7b87b0fdf098e81ed565add09eaa1294268c7f3caac4c8"}, + {file = "multidict-6.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d191de6cbab2aff5de6c5723101705fd044b3e4c7cfd587a1929b5028b9714b3"}, + {file = "multidict-6.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38a0956dd92d918ad5feff3db8fcb4a5eb7dba114da917e1a88475619781b57b"}, + {file = "multidict-6.6.4-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6865f6d3b7900ae020b495d599fcf3765653bc927951c1abb959017f81ae8287"}, + {file = "multidict-6.6.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a2088c126b6f72db6c9212ad827d0ba088c01d951cee25e758c450da732c138"}, + {file = "multidict-6.6.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0f37bed7319b848097085d7d48116f545985db988e2256b2e6f00563a3416ee6"}, + {file = "multidict-6.6.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:01368e3c94032ba6ca0b78e7ccb099643466cf24f8dc8eefcfdc0571d56e58f9"}, + {file = "multidict-6.6.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe323540c255db0bffee79ad7f048c909f2ab0edb87a597e1c17da6a54e493c"}, + {file = "multidict-6.6.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8eb3025f17b0a4c3cd08cda49acf312a19ad6e8a4edd9dbd591e6506d999402"}, + {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bbc14f0365534d35a06970d6a83478b249752e922d662dc24d489af1aa0d1be7"}, + {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:75aa52fba2d96bf972e85451b99d8e19cc37ce26fd016f6d4aa60da9ab2b005f"}, + {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4fefd4a815e362d4f011919d97d7b4a1e566f1dde83dc4ad8cfb5b41de1df68d"}, + {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:db9801fe021f59a5b375ab778973127ca0ac52429a26e2fd86aa9508f4d26eb7"}, + {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a650629970fa21ac1fb06ba25dabfc5b8a2054fcbf6ae97c758aa956b8dba802"}, + {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:452ff5da78d4720d7516a3a2abd804957532dd69296cb77319c193e3ffb87e24"}, + {file = "multidict-6.6.4-cp312-cp312-win32.whl", hash = "sha256:8c2fcb12136530ed19572bbba61b407f655e3953ba669b96a35036a11a485793"}, + {file = "multidict-6.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:047d9425860a8c9544fed1b9584f0c8bcd31bcde9568b047c5e567a1025ecd6e"}, + {file = "multidict-6.6.4-cp312-cp312-win_arm64.whl", hash = "sha256:14754eb72feaa1e8ae528468f24250dd997b8e2188c3d2f593f9eba259e4b364"}, + {file = "multidict-6.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f46a6e8597f9bd71b31cc708195d42b634c8527fecbcf93febf1052cacc1f16e"}, + {file = "multidict-6.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:22e38b2bc176c5eb9c0a0e379f9d188ae4cd8b28c0f53b52bce7ab0a9e534657"}, + {file = "multidict-6.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5df8afd26f162da59e218ac0eefaa01b01b2e6cd606cffa46608f699539246da"}, + {file = "multidict-6.6.4-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:49517449b58d043023720aa58e62b2f74ce9b28f740a0b5d33971149553d72aa"}, + {file = "multidict-6.6.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9408439537c5afdca05edd128a63f56a62680f4b3c234301055d7a2000220f"}, + {file = "multidict-6.6.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:87a32d20759dc52a9e850fe1061b6e41ab28e2998d44168a8a341b99ded1dba0"}, + {file = "multidict-6.6.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52e3c8d43cdfff587ceedce9deb25e6ae77daba560b626e97a56ddcad3756879"}, + {file = "multidict-6.6.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ad8850921d3a8d8ff6fbef790e773cecfc260bbfa0566998980d3fa8f520bc4a"}, + {file = "multidict-6.6.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:497a2954adc25c08daff36f795077f63ad33e13f19bfff7736e72c785391534f"}, + {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:024ce601f92d780ca1617ad4be5ac15b501cc2414970ffa2bb2bbc2bd5a68fa5"}, + {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a693fc5ed9bdd1c9e898013e0da4dcc640de7963a371c0bd458e50e046bf6438"}, + {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:190766dac95aab54cae5b152a56520fd99298f32a1266d66d27fdd1b5ac00f4e"}, + {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:34d8f2a5ffdceab9dcd97c7a016deb2308531d5f0fced2bb0c9e1df45b3363d7"}, + {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:59e8d40ab1f5a8597abcef00d04845155a5693b5da00d2c93dbe88f2050f2812"}, + {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:467fe64138cfac771f0e949b938c2e1ada2b5af22f39692aa9258715e9ea613a"}, + {file = "multidict-6.6.4-cp313-cp313-win32.whl", hash = "sha256:14616a30fe6d0a48d0a48d1a633ab3b8bec4cf293aac65f32ed116f620adfd69"}, + {file = "multidict-6.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:40cd05eaeb39e2bc8939451f033e57feaa2ac99e07dbca8afe2be450a4a3b6cf"}, + {file = "multidict-6.6.4-cp313-cp313-win_arm64.whl", hash = "sha256:f6eb37d511bfae9e13e82cb4d1af36b91150466f24d9b2b8a9785816deb16605"}, + {file = "multidict-6.6.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:6c84378acd4f37d1b507dfa0d459b449e2321b3ba5f2338f9b085cf7a7ba95eb"}, + {file = "multidict-6.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0e0558693063c75f3d952abf645c78f3c5dfdd825a41d8c4d8156fc0b0da6e7e"}, + {file = "multidict-6.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3f8e2384cb83ebd23fd07e9eada8ba64afc4c759cd94817433ab8c81ee4b403f"}, + {file = "multidict-6.6.4-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f996b87b420995a9174b2a7c1a8daf7db4750be6848b03eb5e639674f7963773"}, + {file = "multidict-6.6.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc356250cffd6e78416cf5b40dc6a74f1edf3be8e834cf8862d9ed5265cf9b0e"}, + {file = "multidict-6.6.4-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:dadf95aa862714ea468a49ad1e09fe00fcc9ec67d122f6596a8d40caf6cec7d0"}, + {file = "multidict-6.6.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7dd57515bebffd8ebd714d101d4c434063322e4fe24042e90ced41f18b6d3395"}, + {file = "multidict-6.6.4-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:967af5f238ebc2eb1da4e77af5492219fbd9b4b812347da39a7b5f5c72c0fa45"}, + {file = "multidict-6.6.4-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a4c6875c37aae9794308ec43e3530e4aa0d36579ce38d89979bbf89582002bb"}, + {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f683a551e92bdb7fac545b9c6f9fa2aebdeefa61d607510b3533286fcab67f5"}, + {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:3ba5aaf600edaf2a868a391779f7a85d93bed147854925f34edd24cc70a3e141"}, + {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:580b643b7fd2c295d83cad90d78419081f53fd532d1f1eb67ceb7060f61cff0d"}, + {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:37b7187197da6af3ee0b044dbc9625afd0c885f2800815b228a0e70f9a7f473d"}, + {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e1b93790ed0bc26feb72e2f08299691ceb6da5e9e14a0d13cc74f1869af327a0"}, + {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a506a77ddee1efcca81ecbeae27ade3e09cdf21a8ae854d766c2bb4f14053f92"}, + {file = "multidict-6.6.4-cp313-cp313t-win32.whl", hash = "sha256:f93b2b2279883d1d0a9e1bd01f312d6fc315c5e4c1f09e112e4736e2f650bc4e"}, + {file = "multidict-6.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:6d46a180acdf6e87cc41dc15d8f5c2986e1e8739dc25dbb7dac826731ef381a4"}, + {file = "multidict-6.6.4-cp313-cp313t-win_arm64.whl", hash = "sha256:756989334015e3335d087a27331659820d53ba432befdef6a718398b0a8493ad"}, + {file = "multidict-6.6.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:af7618b591bae552b40dbb6f93f5518328a949dac626ee75927bba1ecdeea9f4"}, + {file = "multidict-6.6.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b6819f83aef06f560cb15482d619d0e623ce9bf155115150a85ab11b8342a665"}, + {file = "multidict-6.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4d09384e75788861e046330308e7af54dd306aaf20eb760eb1d0de26b2bea2cb"}, + {file = "multidict-6.6.4-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:a59c63061f1a07b861c004e53869eb1211ffd1a4acbca330e3322efa6dd02978"}, + {file = "multidict-6.6.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:350f6b0fe1ced61e778037fdc7613f4051c8baf64b1ee19371b42a3acdb016a0"}, + {file = "multidict-6.6.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0c5cbac6b55ad69cb6aa17ee9343dfbba903118fd530348c330211dc7aa756d1"}, + {file = "multidict-6.6.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:630f70c32b8066ddfd920350bc236225814ad94dfa493fe1910ee17fe4365cbb"}, + {file = "multidict-6.6.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8d4916a81697faec6cb724a273bd5457e4c6c43d82b29f9dc02c5542fd21fc9"}, + {file = "multidict-6.6.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e42332cf8276bb7645d310cdecca93a16920256a5b01bebf747365f86a1675b"}, + {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f3be27440f7644ab9a13a6fc86f09cdd90b347c3c5e30c6d6d860de822d7cb53"}, + {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:21f216669109e02ef3e2415ede07f4f8987f00de8cdfa0cc0b3440d42534f9f0"}, + {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:d9890d68c45d1aeac5178ded1d1cccf3bc8d7accf1f976f79bf63099fb16e4bd"}, + {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:edfdcae97cdc5d1a89477c436b61f472c4d40971774ac4729c613b4b133163cb"}, + {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:0b2e886624be5773e69cf32bcb8534aecdeb38943520b240fed3d5596a430f2f"}, + {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:be5bf4b3224948032a845d12ab0f69f208293742df96dc14c4ff9b09e508fc17"}, + {file = "multidict-6.6.4-cp39-cp39-win32.whl", hash = "sha256:10a68a9191f284fe9d501fef4efe93226e74df92ce7a24e301371293bd4918ae"}, + {file = "multidict-6.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:ee25f82f53262f9ac93bd7e58e47ea1bdcc3393cef815847e397cba17e284210"}, + {file = "multidict-6.6.4-cp39-cp39-win_arm64.whl", hash = "sha256:f9867e55590e0855bcec60d4f9a092b69476db64573c9fe17e92b0c50614c16a"}, + {file = "multidict-6.6.4-py3-none-any.whl", hash = "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c"}, + {file = "multidict-6.6.4.tar.gz", hash = "sha256:d2d4e4787672911b48350df02ed3fa3fffdc2f2e8ca06dd6afdf34189b76a9dd"}, ] [[package]] @@ -3376,14 +3471,14 @@ files = [ [[package]] name = "narwhals" -version = "1.35.0" +version = "2.1.2" 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-2.1.2-py3-none-any.whl", hash = "sha256:136b2f533a4eb3245c54254f137c5d14cef5c4668cff67dc6e911a602acd3547"}, + {file = "narwhals-2.1.2.tar.gz", hash = "sha256:afb9597e76d5b38c2c4b7c37d27a2418b8cc8049a66b8a5aca9581c92ae8f8bf"}, ] [package.extras] @@ -3392,10 +3487,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)"] -polars = ["polars (>=0.20.3)"] -pyarrow = ["pyarrow (>=11.0.0)"] +pandas = ["pandas (>=1.1.3)"] +polars = ["polars (>=0.20.4)"] +pyarrow = ["pyarrow (>=13.0.0)"] pyspark = ["pyspark (>=3.5.0)"] +pyspark-connect = ["pyspark[connect] (>=3.5.0)"] sqlframe = ["sqlframe (>=3.22.0)"] [[package]] @@ -3467,14 +3563,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] @@ -3484,14 +3580,14 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] [[package]] name = "openai" -version = "1.82.0" +version = "1.101.0" description = "The official Python library for the openai API" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "openai-1.82.0-py3-none-any.whl", hash = "sha256:8c40647fea1816516cb3de5189775b30b5f4812777e40b8768f361f232b61b30"}, - {file = "openai-1.82.0.tar.gz", hash = "sha256:b0a009b9a58662d598d07e91e4219ab4b1e3d8ba2db3f173896a92b9b874d1a7"}, + {file = "openai-1.101.0-py3-none-any.whl", hash = "sha256:6539a446cce154f8d9fb42757acdfd3ed9357ab0d34fcac11096c461da87133b"}, + {file = "openai-1.101.0.tar.gz", hash = "sha256:29f56df2236069686e64aca0e13c24a4ec310545afb25ef7da2ab1a18523f22d"}, ] [package.dependencies] @@ -3505,58 +3601,59 @@ tqdm = ">4" typing-extensions = ">=4.11,<5" [package.extras] +aiohttp = ["aiohttp", "httpx-aiohttp (>=0.1.8)"] datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] realtime = ["websockets (>=13,<16)"] voice-helpers = ["numpy (>=2.0.2)", "sounddevice (>=0.5.1)"] [[package]] name = "opentelemetry-api" -version = "1.32.1" +version = "1.36.0" 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.36.0-py3-none-any.whl", hash = "sha256:02f20bcacf666e1333b6b1f04e647dc1d5111f86b8e510238fcc56d7762cda8c"}, + {file = "opentelemetry_api-1.36.0.tar.gz", hash = "sha256:9a72572b9c416d004d492cbc6e61962c0501eaf945ece9b5a0f56597d8348aa0"}, ] [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.36.0" 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.36.0-py3-none-any.whl", hash = "sha256:19fe048b42e98c5c1ffe85b569b7073576ad4ce0bcb6e9b4c6a39e890a6c45fb"}, + {file = "opentelemetry_sdk-1.36.0.tar.gz", hash = "sha256:19c8c81599f51b71670661ff7495c905d8fdf6976e41622d5245b791b06fa581"}, ] [package.dependencies] -opentelemetry-api = "1.32.1" -opentelemetry-semantic-conventions = "0.53b1" -typing-extensions = ">=3.7.4" +opentelemetry-api = "1.36.0" +opentelemetry-semantic-conventions = "0.57b0" +typing-extensions = ">=4.5.0" [[package]] name = "opentelemetry-semantic-conventions" -version = "0.53b1" +version = "0.57b0" 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.57b0-py3-none-any.whl", hash = "sha256:757f7e76293294f124c827e514c2a3144f191ef175b069ce8d1211e1e38e9e78"}, + {file = "opentelemetry_semantic_conventions-0.57b0.tar.gz", hash = "sha256:609a4a79c7891b4620d64c7aac6898f872d790d75f22019913a660756f27ff32"}, ] [package.dependencies] -deprecated = ">=1.2.6" -opentelemetry-api = "1.32.1" +opentelemetry-api = "1.36.0" +typing-extensions = ">=4.5.0" [[package]] name = "packaging" @@ -3658,14 +3755,14 @@ xml = ["lxml (>=4.9.2)"] [[package]] name = "pbr" -version = "6.1.1" +version = "7.0.1" description = "Python Build Reasonableness" optional = false python-versions = ">=2.6" groups = ["dev"] files = [ - {file = "pbr-6.1.1-py2.py3-none-any.whl", hash = "sha256:38d4daea5d9fa63b3f626131b9d34947fd0c8be9b05a29276870580050a25a76"}, - {file = "pbr-6.1.1.tar.gz", hash = "sha256:93ea72ce6989eb2eed99d0f75721474f69ad88128afdef5ac377eb797c4bf76b"}, + {file = "pbr-7.0.1-py2.py3-none-any.whl", hash = "sha256:32df5156fbeccb6f8a858d1ebc4e465dcf47d6cc7a4895d5df9aa951c712fc35"}, + {file = "pbr-7.0.1.tar.gz", hash = "sha256:3ecbcb11d2b8551588ec816b3756b1eb4394186c3b689b17e04850dfc20f7e57"}, ] [package.dependencies] @@ -3673,14 +3770,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"] 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] @@ -3690,14 +3787,14 @@ type = ["mypy (>=1.14.1)"] [[package]] name = "plotly" -version = "6.0.1" +version = "6.3.0" 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.3.0-py3-none-any.whl", hash = "sha256:7ad806edce9d3cdd882eaebaf97c0c9e252043ed1ed3d382c3e3520ec07806d4"}, + {file = "plotly-6.3.0.tar.gz", hash = "sha256:8840a184d18ccae0f9189c2b9a2943923fd5cae7717b723f36eef78f444e5a73"}, ] [package.dependencies] @@ -3705,23 +3802,28 @@ narwhals = ">=1.15.1" packaging = "*" [package.extras] +dev = ["plotly[dev-optional]"] +dev-build = ["build", "jupyter", "plotly[dev-core]"] +dev-core = ["pytest", "requests", "ruff (==0.11.12)"] +dev-optional = ["anywidget", "colorcet", "fiona (<=1.9.6) ; python_version <= \"3.8\"", "geopandas", "inflect", "numpy", "orjson", "pandas", "pdfrw", "pillow", "plotly-geo", "plotly[dev-build]", "plotly[kaleido]", "polars[timezone]", "pyarrow", "pyshp", "pytz", "scikit-image", "scipy", "shapely", "statsmodels", "vaex ; python_version <= \"3.9\"", "xarray"] express = ["numpy"] +kaleido = ["kaleido (>=1.0.0)"] [[package]] name = "pluggy" -version = "1.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 = ["main", "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 = "prompt-toolkit" @@ -3740,110 +3842,110 @@ wcwidth = "*" [[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]] @@ -3866,26 +3968,26 @@ testing = ["google-api-core (>=1.31.5)"] [[package]] name = "protobuf" -version = "6.31.1" +version = "6.32.0" description = "" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "protobuf-6.31.1-cp310-abi3-win32.whl", hash = "sha256:7fa17d5a29c2e04b7d90e5e32388b8bfd0e7107cd8e616feef7ed3fa6bdab5c9"}, - {file = "protobuf-6.31.1-cp310-abi3-win_amd64.whl", hash = "sha256:426f59d2964864a1a366254fa703b8632dcec0790d8862d30034d8245e1cd447"}, - {file = "protobuf-6.31.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:6f1227473dc43d44ed644425268eb7c2e488ae245d51c6866d19fe158e207402"}, - {file = "protobuf-6.31.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:a40fc12b84c154884d7d4c4ebd675d5b3b5283e155f324049ae396b95ddebc39"}, - {file = "protobuf-6.31.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:4ee898bf66f7a8b0bd21bce523814e6fbd8c6add948045ce958b73af7e8878c6"}, - {file = "protobuf-6.31.1-cp39-cp39-win32.whl", hash = "sha256:0414e3aa5a5f3ff423828e1e6a6e907d6c65c1d5b7e6e975793d5590bdeecc16"}, - {file = "protobuf-6.31.1-cp39-cp39-win_amd64.whl", hash = "sha256:8764cf4587791e7564051b35524b72844f845ad0bb011704c3736cce762d8fe9"}, - {file = "protobuf-6.31.1-py3-none-any.whl", hash = "sha256:720a6c7e6b77288b85063569baae8536671b39f15cc22037ec7045658d80489e"}, - {file = "protobuf-6.31.1.tar.gz", hash = "sha256:d8cac4c982f0b957a4dc73a80e2ea24fab08e679c0de9deb835f4a12d69aca9a"}, + {file = "protobuf-6.32.0-cp310-abi3-win32.whl", hash = "sha256:84f9e3c1ff6fb0308dbacb0950d8aa90694b0d0ee68e75719cb044b7078fe741"}, + {file = "protobuf-6.32.0-cp310-abi3-win_amd64.whl", hash = "sha256:a8bdbb2f009cfc22a36d031f22a625a38b615b5e19e558a7b756b3279723e68e"}, + {file = "protobuf-6.32.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d52691e5bee6c860fff9a1c86ad26a13afbeb4b168cd4445c922b7e2cf85aaf0"}, + {file = "protobuf-6.32.0-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:501fe6372fd1c8ea2a30b4d9be8f87955a64d6be9c88a973996cef5ef6f0abf1"}, + {file = "protobuf-6.32.0-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:75a2aab2bd1aeb1f5dc7c5f33bcb11d82ea8c055c9becbb41c26a8c43fd7092c"}, + {file = "protobuf-6.32.0-cp39-cp39-win32.whl", hash = "sha256:7db8ed09024f115ac877a1427557b838705359f047b2ff2f2b2364892d19dacb"}, + {file = "protobuf-6.32.0-cp39-cp39-win_amd64.whl", hash = "sha256:15eba1b86f193a407607112ceb9ea0ba9569aed24f93333fe9a497cf2fda37d3"}, + {file = "protobuf-6.32.0-py3-none-any.whl", hash = "sha256:ba377e5b67b908c8f3072a57b63e2c6a4cbd18aea4ed98d2584350dbf46f2783"}, + {file = "protobuf-6.32.0.tar.gz", hash = "sha256:a81439049127067fc49ec1d36e25c6ee1d1a2b7be930675f919258d03c04e7d2"}, ] [[package]] name = "prowler" -version = "5.8.0" +version = "5.11.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" @@ -3894,7 +3996,7 @@ files = [] develop = false [package.dependencies] -alive-progress = "3.2.0" +alive-progress = "3.3.0" awsipranges = "0.3.3" azure-identity = "1.21.0" azure-keyvault-keys = "4.10.0" @@ -3904,10 +4006,13 @@ azure-mgmt-compute = "34.0.0" azure-mgmt-containerregistry = "12.0.0" azure-mgmt-containerservice = "34.1.0" azure-mgmt-cosmosdb = "9.7.0" +azure-mgmt-databricks = "2.0.0" azure-mgmt-keyvault = "10.3.1" azure-mgmt-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" @@ -3916,13 +4021,14 @@ azure-mgmt-storage = "22.1.1" azure-mgmt-subscription = "3.1.1" azure-mgmt-web = "8.0.0" azure-storage-blob = "12.24.1" -boto3 = "1.35.99" -botocore = "1.35.99" +boto3 = "1.39.15" +botocore = "1.39.15" 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" +dulwich = "0.23.0" google-api-python-client = "2.163.0" google-auth-httplib2 = ">=0.1,<0.3" jsonschema = "4.23.0" @@ -3931,12 +4037,13 @@ microsoft-kiota-abstractions = "1.9.2" msgraph-sdk = "1.23.0" numpy = "2.0.2" pandas = "2.2.3" -py-ocsf-models = "0.3.1" -pydantic = "1.10.21" +py-iam-expand = "0.1.0" +py-ocsf-models = "0.5.0" +pydantic = ">=2.0,<3.0" pygithub = "2.5.0" python-dateutil = ">=2.9.0.post0,<3.0.0" pytz = "2025.1" -schema = "0.7.7" +schema = "0.7.5" shodan = "1.31.0" slack-sdk = "3.34.0" tabulate = "0.9.0" @@ -3946,7 +4053,7 @@ tzlocal = "5.3.1" type = "git" url = "https://github.com/prowler-cloud/prowler.git" reference = "master" -resolved_reference = "ea97de7f43a2063476b49f7697bb6c7b51137c11" +resolved_reference = "525f152e51f82de2110ed158c8dc489e42c289cf" [[package]] name = "psutil" @@ -4061,21 +4168,36 @@ files = [ ] [[package]] -name = "py-ocsf-models" -version = "0.3.1" -description = "This is a Python implementation of the OCSF models. The models are used to represent the data of the OCSF Schema defined in https://schema.ocsf.io/." +name = "py-iam-expand" +version = "0.1.0" +description = "This is a Python package to expand and deobfuscate IAM policies." optional = false -python-versions = "<3.13,>3.9.1" +python-versions = "<3.14,>3.9.1" groups = ["main"] files = [ - {file = "py_ocsf_models-0.3.1-py3-none-any.whl", hash = "sha256:e722d567a7f3e5190fdd053c2e75a69cf33fab6f5c0a4b7de678768ba340ae3a"}, - {file = "py_ocsf_models-0.3.1.tar.gz", hash = "sha256:60defd2cc86e8882f42dc9c6dacca6dc16d6bc05f9477c2a3486a0d4b5882b94"}, + {file = "py_iam_expand-0.1.0-py3-none-any.whl", hash = "sha256:b845ce7b50ac895b02b4f338e09c62a68ea51849794f76e189b02009bd388510"}, + {file = "py_iam_expand-0.1.0.tar.gz", hash = "sha256:5a2884dc267ac59a02c3a80fefc0b34c309dac681baa0f87c436067c6cf53a96"}, +] + +[package.dependencies] +iamdata = ">=0.1.202504091" + +[[package]] +name = "py-ocsf-models" +version = "0.5.0" +description = "This is a Python implementation of the OCSF models. The models are used to represent the data of the OCSF Schema defined in https://schema.ocsf.io/." +optional = false +python-versions = "<3.14,>3.9.1" +groups = ["main"] +files = [ + {file = "py_ocsf_models-0.5.0-py3-none-any.whl", hash = "sha256:7933253f56782c04c412d976796db429577810b951fe4195351794500b5962d8"}, + {file = "py_ocsf_models-0.5.0.tar.gz", hash = "sha256:bf05e955809d1ec3ab1007e4a4b2a8a0afa74b6e744ea8ffbf386e46b3af0a76"}, ] [package.dependencies] cryptography = "44.0.1" email-validator = "2.2.0" -pydantic = "1.10.21" +pydantic = ">=2.9.2,<3.0.0" [[package]] name = "pyasn1" @@ -4106,14 +4228,14 @@ pyasn1 = ">=0.6.1,<0.7.0" [[package]] name = "pycodestyle" -version = "2.13.0" +version = "2.14.0" description = "Python style guide checker" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pycodestyle-2.13.0-py2.py3-none-any.whl", hash = "sha256:35863c5974a271c7a726ed228a14a4f6daf49df369d8c50cd9a6f58a5e143ba9"}, - {file = "pycodestyle-2.13.0.tar.gz", hash = "sha256:c8415bf09abe81d9c7f872502a6eee881fbe85d8763dd5b9924bb0a01d67efae"}, + {file = "pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d"}, + {file = "pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783"}, ] [[package]] @@ -4173,70 +4295,137 @@ files = [ [[package]] name = "pydantic" -version = "1.10.21" -description = "Data validation and settings management using python type hints" +version = "2.11.7" +description = "Data validation using Python type hints" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "pydantic-1.10.21-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:245e486e0fec53ec2366df9cf1cba36e0bbf066af7cd9c974bbbd9ba10e1e586"}, - {file = "pydantic-1.10.21-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6c54f8d4c151c1de784c5b93dfbb872067e3414619e10e21e695f7bb84d1d1fd"}, - {file = "pydantic-1.10.21-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b64708009cfabd9c2211295144ff455ec7ceb4c4fb45a07a804309598f36187"}, - {file = "pydantic-1.10.21-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a148410fa0e971ba333358d11a6dea7b48e063de127c2b09ece9d1c1137dde4"}, - {file = "pydantic-1.10.21-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:36ceadef055af06e7756eb4b871cdc9e5a27bdc06a45c820cd94b443de019bbf"}, - {file = "pydantic-1.10.21-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c0501e1d12df6ab1211b8cad52d2f7b2cd81f8e8e776d39aa5e71e2998d0379f"}, - {file = "pydantic-1.10.21-cp310-cp310-win_amd64.whl", hash = "sha256:c261127c275d7bce50b26b26c7d8427dcb5c4803e840e913f8d9df3f99dca55f"}, - {file = "pydantic-1.10.21-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8b6350b68566bb6b164fb06a3772e878887f3c857c46c0c534788081cb48adf4"}, - {file = "pydantic-1.10.21-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:935b19fdcde236f4fbf691959fa5c3e2b6951fff132964e869e57c70f2ad1ba3"}, - {file = "pydantic-1.10.21-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b6a04efdcd25486b27f24c1648d5adc1633ad8b4506d0e96e5367f075ed2e0b"}, - {file = "pydantic-1.10.21-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1ba253eb5af8d89864073e6ce8e6c8dec5f49920cff61f38f5c3383e38b1c9f"}, - {file = "pydantic-1.10.21-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:57f0101e6c97b411f287a0b7cf5ebc4e5d3b18254bf926f45a11615d29475793"}, - {file = "pydantic-1.10.21-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e85834f0370d737c77a386ce505c21b06bfe7086c1c568b70e15a568d9670d"}, - {file = "pydantic-1.10.21-cp311-cp311-win_amd64.whl", hash = "sha256:6a497bc66b3374b7d105763d1d3de76d949287bf28969bff4656206ab8a53aa9"}, - {file = "pydantic-1.10.21-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ed4a5f13cf160d64aa331ab9017af81f3481cd9fd0e49f1d707b57fe1b9f3ae"}, - {file = "pydantic-1.10.21-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3b7693bb6ed3fbe250e222f9415abb73111bb09b73ab90d2d4d53f6390e0ccc1"}, - {file = "pydantic-1.10.21-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:185d5f1dff1fead51766da9b2de4f3dc3b8fca39e59383c273f34a6ae254e3e2"}, - {file = "pydantic-1.10.21-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38e6d35cf7cd1727822c79e324fa0677e1a08c88a34f56695101f5ad4d5e20e5"}, - {file = "pydantic-1.10.21-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:1d7c332685eafacb64a1a7645b409a166eb7537f23142d26895746f628a3149b"}, - {file = "pydantic-1.10.21-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c9b782db6f993a36092480eeaab8ba0609f786041b01f39c7c52252bda6d85f"}, - {file = "pydantic-1.10.21-cp312-cp312-win_amd64.whl", hash = "sha256:7ce64d23d4e71d9698492479505674c5c5b92cda02b07c91dfc13633b2eef805"}, - {file = "pydantic-1.10.21-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0067935d35044950be781933ab91b9a708eaff124bf860fa2f70aeb1c4be7212"}, - {file = "pydantic-1.10.21-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5e8148c2ce4894ce7e5a4925d9d3fdce429fb0e821b5a8783573f3611933a251"}, - {file = "pydantic-1.10.21-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4973232c98b9b44c78b1233693e5e1938add5af18042f031737e1214455f9b8"}, - {file = "pydantic-1.10.21-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:662bf5ce3c9b1cef32a32a2f4debe00d2f4839fefbebe1d6956e681122a9c839"}, - {file = "pydantic-1.10.21-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:98737c3ab5a2f8a85f2326eebcd214510f898881a290a7939a45ec294743c875"}, - {file = "pydantic-1.10.21-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0bb58bbe65a43483d49f66b6c8474424d551a3fbe8a7796c42da314bac712738"}, - {file = "pydantic-1.10.21-cp313-cp313-win_amd64.whl", hash = "sha256:e622314542fb48542c09c7bd1ac51d71c5632dd3c92dc82ede6da233f55f4848"}, - {file = "pydantic-1.10.21-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d356aa5b18ef5a24d8081f5c5beb67c0a2a6ff2a953ee38d65a2aa96526b274f"}, - {file = "pydantic-1.10.21-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08caa8c0468172d27c669abfe9e7d96a8b1655ec0833753e117061febaaadef5"}, - {file = "pydantic-1.10.21-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c677aa39ec737fec932feb68e4a2abe142682f2885558402602cd9746a1c92e8"}, - {file = "pydantic-1.10.21-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:79577cc045d3442c4e845df53df9f9202546e2ba54954c057d253fc17cd16cb1"}, - {file = "pydantic-1.10.21-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:b6b73ab347284719f818acb14f7cd80696c6fdf1bd34feee1955d7a72d2e64ce"}, - {file = "pydantic-1.10.21-cp37-cp37m-win_amd64.whl", hash = "sha256:46cffa24891b06269e12f7e1ec50b73f0c9ab4ce71c2caa4ccf1fb36845e1ff7"}, - {file = "pydantic-1.10.21-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:298d6f765e3c9825dfa78f24c1efd29af91c3ab1b763e1fd26ae4d9e1749e5c8"}, - {file = "pydantic-1.10.21-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f2f4a2305f15eff68f874766d982114ac89468f1c2c0b97640e719cf1a078374"}, - {file = "pydantic-1.10.21-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35b263b60c519354afb3a60107d20470dd5250b3ce54c08753f6975c406d949b"}, - {file = "pydantic-1.10.21-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e23a97a6c2f2db88995496db9387cd1727acdacc85835ba8619dce826c0b11a6"}, - {file = "pydantic-1.10.21-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:3c96fed246ccc1acb2df032ff642459e4ae18b315ecbab4d95c95cfa292e8517"}, - {file = "pydantic-1.10.21-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b92893ebefc0151474f682e7debb6ab38552ce56a90e39a8834734c81f37c8a9"}, - {file = "pydantic-1.10.21-cp38-cp38-win_amd64.whl", hash = "sha256:b8460bc256bf0de821839aea6794bb38a4c0fbd48f949ea51093f6edce0be459"}, - {file = "pydantic-1.10.21-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5d387940f0f1a0adb3c44481aa379122d06df8486cc8f652a7b3b0caf08435f7"}, - {file = "pydantic-1.10.21-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:266ecfc384861d7b0b9c214788ddff75a2ea123aa756bcca6b2a1175edeca0fe"}, - {file = "pydantic-1.10.21-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61da798c05a06a362a2f8c5e3ff0341743e2818d0f530eaac0d6898f1b187f1f"}, - {file = "pydantic-1.10.21-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a621742da75ce272d64ea57bd7651ee2a115fa67c0f11d66d9dcfc18c2f1b106"}, - {file = "pydantic-1.10.21-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9e3e4000cd54ef455694b8be9111ea20f66a686fc155feda1ecacf2322b115da"}, - {file = "pydantic-1.10.21-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f198c8206640f4c0ef5a76b779241efb1380a300d88b1bce9bfe95a6362e674d"}, - {file = "pydantic-1.10.21-cp39-cp39-win_amd64.whl", hash = "sha256:e7f0cda108b36a30c8fc882e4fc5b7eec8ef584aa43aa43694c6a7b274fb2b56"}, - {file = "pydantic-1.10.21-py3-none-any.whl", hash = "sha256:db70c920cba9d05c69ad4a9e7f8e9e83011abb2c6490e561de9ae24aee44925c"}, - {file = "pydantic-1.10.21.tar.gz", hash = "sha256:64b48e2b609a6c22178a56c408ee1215a7206077ecb8a193e2fda31858b2362a"}, + {file = "pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b"}, + {file = "pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db"}, ] [package.dependencies] -typing-extensions = ">=4.2.0" +annotated-types = ">=0.6.0" +pydantic-core = "2.33.2" +typing-extensions = ">=4.12.2" +typing-inspection = ">=0.4.0" [package.extras] -dotenv = ["python-dotenv (>=0.10.4)"] -email = ["email-validator (>=1.0.3)"] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"}, + {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pygithub" @@ -4260,14 +4449,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"] 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] @@ -4275,14 +4464,14 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyjwt" -version = "2.9.0" +version = "2.10.1" description = "JSON Web Token implementation in Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850"}, - {file = "pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"}, + {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"}, + {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"}, ] [package.dependencies] @@ -4528,19 +4717,16 @@ testing = ["filelock"] [[package]] name = "python-crontab" -version = "3.2.0" +version = "3.3.0" description = "Python Crontab API" optional = false python-versions = "*" groups = ["main"] files = [ - {file = "python_crontab-3.2.0-py3-none-any.whl", hash = "sha256:82cb9b6a312d41ff66fd3caf3eed7115c28c195bfb50711bc2b4b9592feb9fe5"}, - {file = "python_crontab-3.2.0.tar.gz", hash = "sha256:40067d1dd39ade3460b2ad8557c7651514cd3851deffff61c5c60e1227c5c36b"}, + {file = "python_crontab-3.3.0-py3-none-any.whl", hash = "sha256:739a778b1a771379b75654e53fd4df58e5c63a9279a63b5dfe44c0fcc3ee7884"}, + {file = "python_crontab-3.3.0.tar.gz", hash = "sha256:007c8aee68dddf3e04ec4dce0fac124b93bd68be7470fc95d2a9617a15de291b"}, ] -[package.dependencies] -python-dateutil = "*" - [package.extras] cron-description = ["cron-descriptor"] cron-schedule = ["croniter"] @@ -4607,29 +4793,33 @@ files = [ [[package]] name = "pywin32" -version = "310" +version = "311" description = "Python for Window Extensions" optional = false python-versions = "*" groups = ["main", "dev"] markers = "sys_platform == \"win32\"" files = [ - {file = "pywin32-310-cp310-cp310-win32.whl", hash = "sha256:6dd97011efc8bf51d6793a82292419eba2c71cf8e7250cfac03bba284454abc1"}, - {file = "pywin32-310-cp310-cp310-win_amd64.whl", hash = "sha256:c3e78706e4229b915a0821941a84e7ef420bf2b77e08c9dae3c76fd03fd2ae3d"}, - {file = "pywin32-310-cp310-cp310-win_arm64.whl", hash = "sha256:33babed0cf0c92a6f94cc6cc13546ab24ee13e3e800e61ed87609ab91e4c8213"}, - {file = "pywin32-310-cp311-cp311-win32.whl", hash = "sha256:1e765f9564e83011a63321bb9d27ec456a0ed90d3732c4b2e312b855365ed8bd"}, - {file = "pywin32-310-cp311-cp311-win_amd64.whl", hash = "sha256:126298077a9d7c95c53823934f000599f66ec9296b09167810eb24875f32689c"}, - {file = "pywin32-310-cp311-cp311-win_arm64.whl", hash = "sha256:19ec5fc9b1d51c4350be7bb00760ffce46e6c95eaf2f0b2f1150657b1a43c582"}, - {file = "pywin32-310-cp312-cp312-win32.whl", hash = "sha256:8a75a5cc3893e83a108c05d82198880704c44bbaee4d06e442e471d3c9ea4f3d"}, - {file = "pywin32-310-cp312-cp312-win_amd64.whl", hash = "sha256:bf5c397c9a9a19a6f62f3fb821fbf36cac08f03770056711f765ec1503972060"}, - {file = "pywin32-310-cp312-cp312-win_arm64.whl", hash = "sha256:2349cc906eae872d0663d4d6290d13b90621eaf78964bb1578632ff20e152966"}, - {file = "pywin32-310-cp313-cp313-win32.whl", hash = "sha256:5d241a659c496ada3253cd01cfaa779b048e90ce4b2b38cd44168ad555ce74ab"}, - {file = "pywin32-310-cp313-cp313-win_amd64.whl", hash = "sha256:667827eb3a90208ddbdcc9e860c81bde63a135710e21e4cb3348968e4bd5249e"}, - {file = "pywin32-310-cp313-cp313-win_arm64.whl", hash = "sha256:e308f831de771482b7cf692a1f308f8fca701b2d8f9dde6cc440c7da17e47b33"}, - {file = "pywin32-310-cp38-cp38-win32.whl", hash = "sha256:0867beb8addefa2e3979d4084352e4ac6e991ca45373390775f7084cc0209b9c"}, - {file = "pywin32-310-cp38-cp38-win_amd64.whl", hash = "sha256:30f0a9b3138fb5e07eb4973b7077e1883f558e40c578c6925acc7a94c34eaa36"}, - {file = "pywin32-310-cp39-cp39-win32.whl", hash = "sha256:851c8d927af0d879221e616ae1f66145253537bbdd321a77e8ef701b443a9a1a"}, - {file = "pywin32-310-cp39-cp39-win_amd64.whl", hash = "sha256:96867217335559ac619f00ad70e513c0fcf84b8a3af9fc2bba3b59b97da70475"}, + {file = "pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3"}, + {file = "pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b"}, + {file = "pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b"}, + {file = "pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151"}, + {file = "pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503"}, + {file = "pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2"}, + {file = "pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31"}, + {file = "pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067"}, + {file = "pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852"}, + {file = "pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d"}, + {file = "pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d"}, + {file = "pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a"}, + {file = "pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee"}, + {file = "pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87"}, + {file = "pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42"}, + {file = "pywin32-311-cp38-cp38-win32.whl", hash = "sha256:6c6f2969607b5023b0d9ce2541f8d2cbb01c4f46bc87456017cf63b73f1e2d8c"}, + {file = "pywin32-311-cp38-cp38-win_amd64.whl", hash = "sha256:c8015b09fb9a5e188f83b7b04de91ddca4658cee2ae6f3bc483f0b21a77ef6cd"}, + {file = "pywin32-311-cp39-cp39-win32.whl", hash = "sha256:aba8f82d551a942cb20d4a83413ccbac30790b50efb89a75e4f586ac0bb8056b"}, + {file = "pywin32-311-cp39-cp39-win_amd64.whl", hash = "sha256:e0c4cfb0621281fe40387df582097fd796e80430597cb9944f0ae70447bacd91"}, + {file = "pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d"}, ] [[package]] @@ -4697,22 +4887,23 @@ files = [ [[package]] name = "redis" -version = "5.2.1" +version = "6.4.0" description = "Python client for Redis database and key-value store" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4"}, - {file = "redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f"}, + {file = "redis-6.4.0-py3-none-any.whl", hash = "sha256:f0544fa9604264e9464cdf4814e7d4830f74b165d52f2a330a760a88dd248b7f"}, + {file = "redis-6.4.0.tar.gz", hash = "sha256:b01bc7282b8444e28ec36b261df5375183bb47a07eb9c603f284e89cbc5ef010"}, ] [package.dependencies] async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\""} [package.extras] -hiredis = ["hiredis (>=3.0.0)"] -ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==23.2.1)", "requests (>=2.31.0)"] +hiredis = ["hiredis (>=3.2.0)"] +jwt = ["pyjwt (>=2.9.0)"] +ocsp = ["cryptography (>=36.0.1)", "pyopenssl (>=20.0.1)", "requests (>=2.31.0)"] [[package]] name = "referencing" @@ -4733,14 +4924,14 @@ typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} [[package]] name = "requests" -version = "2.32.4" +version = "2.32.5" description = "Python HTTP for Humans." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, - {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, + {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, + {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, ] [package.dependencies] @@ -4789,29 +4980,26 @@ rsa = ["oauthlib[signedtoken] (>=3.0.0)"] [[package]] name = "retrying" -version = "1.3.4" +version = "1.4.2" 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.2-py3-none-any.whl", hash = "sha256:bbc004aeb542a74f3569aeddf42a2516efefcdaff90df0eb38fbfbf19f179f59"}, + {file = "retrying-1.4.2.tar.gz", hash = "sha256:d102e75d53d8d30b88562d45361d6c6c934da06fab31bd81c0420acb97a8ba39"}, ] -[package.dependencies] -six = ">=1.7.0" - [[package]] name = "rich" -version = "14.0.0" +version = "14.1.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.8.0" groups = ["dev"] files = [ - {file = "rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0"}, - {file = "rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725"}, + {file = "rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f"}, + {file = "rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8"}, ] [package.dependencies] @@ -4823,126 +5011,167 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "rpds-py" -version = "0.24.0" +version = "0.27.0" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false python-versions = ">=3.9" groups = ["main"] 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.27.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:130c1ffa5039a333f5926b09e346ab335f0d4ec393b030a18549a7c7e7c2cea4"}, + {file = "rpds_py-0.27.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a4cf32a26fa744101b67bfd28c55d992cd19438aff611a46cac7f066afca8fd4"}, + {file = "rpds_py-0.27.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64a0fe3f334a40b989812de70160de6b0ec7e3c9e4a04c0bbc48d97c5d3600ae"}, + {file = "rpds_py-0.27.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a0ff7ee28583ab30a52f371b40f54e7138c52ca67f8ca17ccb7ccf0b383cb5f"}, + {file = "rpds_py-0.27.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15ea4d2e182345dd1b4286593601d766411b43f868924afe297570658c31a62b"}, + {file = "rpds_py-0.27.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36184b44bf60a480863e51021c26aca3dfe8dd2f5eeabb33622b132b9d8b8b54"}, + {file = "rpds_py-0.27.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b78430703cfcf5f5e86eb74027a1ed03a93509273d7c705babb547f03e60016"}, + {file = "rpds_py-0.27.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:dbd749cff1defbde270ca346b69b3baf5f1297213ef322254bf2a28537f0b046"}, + {file = "rpds_py-0.27.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bde37765564cd22a676dd8101b657839a1854cfaa9c382c5abf6ff7accfd4ae"}, + {file = "rpds_py-0.27.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1d66f45b9399036e890fb9c04e9f70c33857fd8f58ac8db9f3278cfa835440c3"}, + {file = "rpds_py-0.27.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d85d784c619370d9329bbd670f41ff5f2ae62ea4519761b679d0f57f0f0ee267"}, + {file = "rpds_py-0.27.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5df559e9e7644d9042f626f2c3997b555f347d7a855a15f170b253f6c5bfe358"}, + {file = "rpds_py-0.27.0-cp310-cp310-win32.whl", hash = "sha256:b8a4131698b6992b2a56015f51646711ec5d893a0b314a4b985477868e240c87"}, + {file = "rpds_py-0.27.0-cp310-cp310-win_amd64.whl", hash = "sha256:cbc619e84a5e3ab2d452de831c88bdcad824414e9c2d28cd101f94dbdf26329c"}, + {file = "rpds_py-0.27.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:dbc2ab5d10544eb485baa76c63c501303b716a5c405ff2469a1d8ceffaabf622"}, + {file = "rpds_py-0.27.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7ec85994f96a58cf7ed288caa344b7fe31fd1d503bdf13d7331ead5f70ab60d5"}, + {file = "rpds_py-0.27.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:190d7285cd3bb6d31d37a0534d7359c1ee191eb194c511c301f32a4afa5a1dd4"}, + {file = "rpds_py-0.27.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c10d92fb6d7fd827e44055fcd932ad93dac6a11e832d51534d77b97d1d85400f"}, + {file = "rpds_py-0.27.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd2c1d27ebfe6a015cfa2005b7fe8c52d5019f7bbdd801bc6f7499aab9ae739e"}, + {file = "rpds_py-0.27.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4790c9d5dd565ddb3e9f656092f57268951398cef52e364c405ed3112dc7c7c1"}, + {file = "rpds_py-0.27.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4300e15e7d03660f04be84a125d1bdd0e6b2f674bc0723bc0fd0122f1a4585dc"}, + {file = "rpds_py-0.27.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:59195dc244fc183209cf8a93406889cadde47dfd2f0a6b137783aa9c56d67c85"}, + {file = "rpds_py-0.27.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fae4a01ef8c4cb2bbe92ef2063149596907dc4a881a8d26743b3f6b304713171"}, + {file = "rpds_py-0.27.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e3dc8d4ede2dbae6c0fc2b6c958bf51ce9fd7e9b40c0f5b8835c3fde44f5807d"}, + {file = "rpds_py-0.27.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c3782fb753aa825b4ccabc04292e07897e2fd941448eabf666856c5530277626"}, + {file = "rpds_py-0.27.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:887ab1f12b0d227e9260558a4a2320024b20102207ada65c43e1ffc4546df72e"}, + {file = "rpds_py-0.27.0-cp311-cp311-win32.whl", hash = "sha256:5d6790ff400254137b81b8053b34417e2c46921e302d655181d55ea46df58cf7"}, + {file = "rpds_py-0.27.0-cp311-cp311-win_amd64.whl", hash = "sha256:e24d8031a2c62f34853756d9208eeafa6b940a1efcbfe36e8f57d99d52bb7261"}, + {file = "rpds_py-0.27.0-cp311-cp311-win_arm64.whl", hash = "sha256:08680820d23df1df0a0260f714d12966bc6c42d02e8055a91d61e03f0c47dda0"}, + {file = "rpds_py-0.27.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:19c990fdf5acecbf0623e906ae2e09ce1c58947197f9bced6bbd7482662231c4"}, + {file = "rpds_py-0.27.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6c27a7054b5224710fcfb1a626ec3ff4f28bcb89b899148c72873b18210e446b"}, + {file = "rpds_py-0.27.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09965b314091829b378b60607022048953e25f0b396c2b70e7c4c81bcecf932e"}, + {file = "rpds_py-0.27.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:14f028eb47f59e9169bfdf9f7ceafd29dd64902141840633683d0bad5b04ff34"}, + {file = "rpds_py-0.27.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6168af0be75bba990a39f9431cdfae5f0ad501f4af32ae62e8856307200517b8"}, + {file = "rpds_py-0.27.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab47fe727c13c09d0e6f508e3a49e545008e23bf762a245b020391b621f5b726"}, + {file = "rpds_py-0.27.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fa01b3d5e3b7d97efab65bd3d88f164e289ec323a8c033c5c38e53ee25c007e"}, + {file = "rpds_py-0.27.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:6c135708e987f46053e0a1246a206f53717f9fadfba27174a9769ad4befba5c3"}, + {file = "rpds_py-0.27.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc327f4497b7087d06204235199daf208fd01c82d80465dc5efa4ec9df1c5b4e"}, + {file = "rpds_py-0.27.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e57906e38583a2cba67046a09c2637e23297618dc1f3caddbc493f2be97c93f"}, + {file = "rpds_py-0.27.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f4f69d7a4300fbf91efb1fb4916421bd57804c01ab938ab50ac9c4aa2212f03"}, + {file = "rpds_py-0.27.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b4c4fbbcff474e1e5f38be1bf04511c03d492d42eec0babda5d03af3b5589374"}, + {file = "rpds_py-0.27.0-cp312-cp312-win32.whl", hash = "sha256:27bac29bbbf39601b2aab474daf99dbc8e7176ca3389237a23944b17f8913d97"}, + {file = "rpds_py-0.27.0-cp312-cp312-win_amd64.whl", hash = "sha256:8a06aa1197ec0281eb1d7daf6073e199eb832fe591ffa329b88bae28f25f5fe5"}, + {file = "rpds_py-0.27.0-cp312-cp312-win_arm64.whl", hash = "sha256:e14aab02258cb776a108107bd15f5b5e4a1bbaa61ef33b36693dfab6f89d54f9"}, + {file = "rpds_py-0.27.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:443d239d02d9ae55b74015234f2cd8eb09e59fbba30bf60baeb3123ad4c6d5ff"}, + {file = "rpds_py-0.27.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b8a7acf04fda1f30f1007f3cc96d29d8cf0a53e626e4e1655fdf4eabc082d367"}, + {file = "rpds_py-0.27.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d0f92b78cfc3b74a42239fdd8c1266f4715b573204c234d2f9fc3fc7a24f185"}, + {file = "rpds_py-0.27.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ce4ed8e0c7dbc5b19352b9c2c6131dd23b95fa8698b5cdd076307a33626b72dc"}, + {file = "rpds_py-0.27.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fde355b02934cc6b07200cc3b27ab0c15870a757d1a72fd401aa92e2ea3c6bfe"}, + {file = "rpds_py-0.27.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13bbc4846ae4c993f07c93feb21a24d8ec637573d567a924b1001e81c8ae80f9"}, + {file = "rpds_py-0.27.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be0744661afbc4099fef7f4e604e7f1ea1be1dd7284f357924af12a705cc7d5c"}, + {file = "rpds_py-0.27.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:069e0384a54f427bd65d7fda83b68a90606a3835901aaff42185fcd94f5a9295"}, + {file = "rpds_py-0.27.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4bc262ace5a1a7dc3e2eac2fa97b8257ae795389f688b5adf22c5db1e2431c43"}, + {file = "rpds_py-0.27.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2fe6e18e5c8581f0361b35ae575043c7029d0a92cb3429e6e596c2cdde251432"}, + {file = "rpds_py-0.27.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d93ebdb82363d2e7bec64eecdc3632b59e84bd270d74fe5be1659f7787052f9b"}, + {file = "rpds_py-0.27.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0954e3a92e1d62e83a54ea7b3fdc9efa5d61acef8488a8a3d31fdafbfb00460d"}, + {file = "rpds_py-0.27.0-cp313-cp313-win32.whl", hash = "sha256:2cff9bdd6c7b906cc562a505c04a57d92e82d37200027e8d362518df427f96cd"}, + {file = "rpds_py-0.27.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc79d192fb76fc0c84f2c58672c17bbbc383fd26c3cdc29daae16ce3d927e8b2"}, + {file = "rpds_py-0.27.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b3a5c8089eed498a3af23ce87a80805ff98f6ef8f7bdb70bd1b7dae5105f6ac"}, + {file = "rpds_py-0.27.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:90fb790138c1a89a2e58c9282fe1089638401f2f3b8dddd758499041bc6e0774"}, + {file = "rpds_py-0.27.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:010c4843a3b92b54373e3d2291a7447d6c3fc29f591772cc2ea0e9f5c1da434b"}, + {file = "rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9ce7a9e967afc0a2af7caa0d15a3e9c1054815f73d6a8cb9225b61921b419bd"}, + {file = "rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aa0bf113d15e8abdfee92aa4db86761b709a09954083afcb5bf0f952d6065fdb"}, + {file = "rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb91d252b35004a84670dfeafadb042528b19842a0080d8b53e5ec1128e8f433"}, + {file = "rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db8a6313dbac934193fc17fe7610f70cd8181c542a91382531bef5ed785e5615"}, + {file = "rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce96ab0bdfcef1b8c371ada2100767ace6804ea35aacce0aef3aeb4f3f499ca8"}, + {file = "rpds_py-0.27.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:7451ede3560086abe1aa27dcdcf55cd15c96b56f543fb12e5826eee6f721f858"}, + {file = "rpds_py-0.27.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:32196b5a99821476537b3f7732432d64d93a58d680a52c5e12a190ee0135d8b5"}, + {file = "rpds_py-0.27.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a029be818059870664157194e46ce0e995082ac49926f1423c1f058534d2aaa9"}, + {file = "rpds_py-0.27.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3841f66c1ffdc6cebce8aed64e36db71466f1dc23c0d9a5592e2a782a3042c79"}, + {file = "rpds_py-0.27.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:42894616da0fc0dcb2ec08a77896c3f56e9cb2f4b66acd76fc8992c3557ceb1c"}, + {file = "rpds_py-0.27.0-cp313-cp313t-win32.whl", hash = "sha256:b1fef1f13c842a39a03409e30ca0bf87b39a1e2a305a9924deadb75a43105d23"}, + {file = "rpds_py-0.27.0-cp313-cp313t-win_amd64.whl", hash = "sha256:183f5e221ba3e283cd36fdfbe311d95cd87699a083330b4f792543987167eff1"}, + {file = "rpds_py-0.27.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:f3cd110e02c5bf17d8fb562f6c9df5c20e73029d587cf8602a2da6c5ef1e32cb"}, + {file = "rpds_py-0.27.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8d0e09cf4863c74106b5265c2c310f36146e2b445ff7b3018a56799f28f39f6f"}, + {file = "rpds_py-0.27.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f689ab822f9b5eb6dfc69893b4b9366db1d2420f7db1f6a2adf2a9ca15ad64"}, + {file = "rpds_py-0.27.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e36c80c49853b3ffda7aa1831bf175c13356b210c73128c861f3aa93c3cc4015"}, + {file = "rpds_py-0.27.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6de6a7f622860af0146cb9ee148682ff4d0cea0b8fd3ad51ce4d40efb2f061d0"}, + {file = "rpds_py-0.27.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4045e2fc4b37ec4b48e8907a5819bdd3380708c139d7cc358f03a3653abedb89"}, + {file = "rpds_py-0.27.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da162b718b12c4219eeeeb68a5b7552fbc7aadedf2efee440f88b9c0e54b45d"}, + {file = "rpds_py-0.27.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:0665be515767dc727ffa5f74bd2ef60b0ff85dad6bb8f50d91eaa6b5fb226f51"}, + {file = "rpds_py-0.27.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:203f581accef67300a942e49a37d74c12ceeef4514874c7cede21b012613ca2c"}, + {file = "rpds_py-0.27.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7873b65686a6471c0037139aa000d23fe94628e0daaa27b6e40607c90e3f5ec4"}, + {file = "rpds_py-0.27.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:249ab91ceaa6b41abc5f19513cb95b45c6f956f6b89f1fe3d99c81255a849f9e"}, + {file = "rpds_py-0.27.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2f184336bc1d6abfaaa1262ed42739c3789b1e3a65a29916a615307d22ffd2e"}, + {file = "rpds_py-0.27.0-cp314-cp314-win32.whl", hash = "sha256:d3c622c39f04d5751408f5b801ecb527e6e0a471b367f420a877f7a660d583f6"}, + {file = "rpds_py-0.27.0-cp314-cp314-win_amd64.whl", hash = "sha256:cf824aceaeffff029ccfba0da637d432ca71ab21f13e7f6f5179cd88ebc77a8a"}, + {file = "rpds_py-0.27.0-cp314-cp314-win_arm64.whl", hash = "sha256:86aca1616922b40d8ac1b3073a1ead4255a2f13405e5700c01f7c8d29a03972d"}, + {file = "rpds_py-0.27.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:341d8acb6724c0c17bdf714319c393bb27f6d23d39bc74f94221b3e59fc31828"}, + {file = "rpds_py-0.27.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6b96b0b784fe5fd03beffff2b1533dc0d85e92bab8d1b2c24ef3a5dc8fac5669"}, + {file = "rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c431bfb91478d7cbe368d0a699978050d3b112d7f1d440a41e90faa325557fd"}, + {file = "rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20e222a44ae9f507d0f2678ee3dd0c45ec1e930f6875d99b8459631c24058aec"}, + {file = "rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:184f0d7b342967f6cda94a07d0e1fae177d11d0b8f17d73e06e36ac02889f303"}, + {file = "rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a00c91104c173c9043bc46f7b30ee5e6d2f6b1149f11f545580f5d6fdff42c0b"}, + {file = "rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7a37dd208f0d658e0487522078b1ed68cd6bce20ef4b5a915d2809b9094b410"}, + {file = "rpds_py-0.27.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:92f3b3ec3e6008a1fe00b7c0946a170f161ac00645cde35e3c9a68c2475e8156"}, + {file = "rpds_py-0.27.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a1b3db5fae5cbce2131b7420a3f83553d4d89514c03d67804ced36161fe8b6b2"}, + {file = "rpds_py-0.27.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5355527adaa713ab693cbce7c1e0ec71682f599f61b128cf19d07e5c13c9b1f1"}, + {file = "rpds_py-0.27.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fcc01c57ce6e70b728af02b2401c5bc853a9e14eb07deda30624374f0aebfe42"}, + {file = "rpds_py-0.27.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3001013dae10f806380ba739d40dee11db1ecb91684febb8406a87c2ded23dae"}, + {file = "rpds_py-0.27.0-cp314-cp314t-win32.whl", hash = "sha256:0f401c369186a5743694dd9fc08cba66cf70908757552e1f714bfc5219c655b5"}, + {file = "rpds_py-0.27.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8a1dca5507fa1337f75dcd5070218b20bc68cf8844271c923c1b79dfcbc20391"}, + {file = "rpds_py-0.27.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e0d7151a1bd5d0a203a5008fc4ae51a159a610cb82ab0a9b2c4d80241745582e"}, + {file = "rpds_py-0.27.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:42ccc57ff99166a55a59d8c7d14f1a357b7749f9ed3584df74053fd098243451"}, + {file = "rpds_py-0.27.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e377e4cf8795cdbdff75b8f0223d7b6c68ff4fef36799d88ccf3a995a91c0112"}, + {file = "rpds_py-0.27.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79af163a4b40bbd8cfd7ca86ec8b54b81121d3b213b4435ea27d6568bcba3e9d"}, + {file = "rpds_py-0.27.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2eff8ee57c5996b0d2a07c3601fb4ce5fbc37547344a26945dd9e5cbd1ed27a"}, + {file = "rpds_py-0.27.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7cf9bc4508efb18d8dff6934b602324eb9f8c6644749627ce001d6f38a490889"}, + {file = "rpds_py-0.27.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05284439ebe7d9f5f5a668d4d8a0a1d851d16f7d47c78e1fab968c8ad30cab04"}, + {file = "rpds_py-0.27.0-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:1321bce595ad70e80f97f998db37356b2e22cf98094eba6fe91782e626da2f71"}, + {file = "rpds_py-0.27.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:737005088449ddd3b3df5a95476ee1c2c5c669f5c30eed909548a92939c0e12d"}, + {file = "rpds_py-0.27.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9b2a4e17bfd68536c3b801800941c95a1d4a06e3cada11c146093ba939d9638d"}, + {file = "rpds_py-0.27.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:dc6b0d5a1ea0318ef2def2b6a55dccf1dcaf77d605672347271ed7b829860765"}, + {file = "rpds_py-0.27.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4c3f8a0d4802df34fcdbeb3dfe3a4d8c9a530baea8fafdf80816fcaac5379d83"}, + {file = "rpds_py-0.27.0-cp39-cp39-win32.whl", hash = "sha256:699c346abc73993962cac7bb4f02f58e438840fa5458a048d3a178a7a670ba86"}, + {file = "rpds_py-0.27.0-cp39-cp39-win_amd64.whl", hash = "sha256:be806e2961cd390a89d6c3ce8c2ae34271cfcd05660f716257838bb560f1c3b6"}, + {file = "rpds_py-0.27.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:46f48482c1a4748ab2773f75fffbdd1951eb59794e32788834b945da857c47a8"}, + {file = "rpds_py-0.27.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:419dd9c98bcc9fb0242be89e0c6e922df333b975d4268faa90d58499fd9c9ebe"}, + {file = "rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d42a0ef2bdf6bc81e1cc2d49d12460f63c6ae1423c4f4851b828e454ccf6f1"}, + {file = "rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2e39169ac6aae06dd79c07c8a69d9da867cef6a6d7883a0186b46bb46ccfb0c3"}, + {file = "rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:935afcdea4751b0ac918047a2df3f720212892347767aea28f5b3bf7be4f27c0"}, + {file = "rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8de567dec6d451649a781633d36f5c7501711adee329d76c095be2178855b042"}, + {file = "rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:555ed147cbe8c8f76e72a4c6cd3b7b761cbf9987891b9448808148204aed74a5"}, + {file = "rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:d2cc2b34f9e1d31ce255174da82902ad75bd7c0d88a33df54a77a22f2ef421ee"}, + {file = "rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cb0702c12983be3b2fab98ead349ac63a98216d28dda6f518f52da5498a27a1b"}, + {file = "rpds_py-0.27.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:ba783541be46f27c8faea5a6645e193943c17ea2f0ffe593639d906a327a9bcc"}, + {file = "rpds_py-0.27.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:2406d034635d1497c596c40c85f86ecf2bf9611c1df73d14078af8444fe48031"}, + {file = "rpds_py-0.27.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:dea0808153f1fbbad772669d906cddd92100277533a03845de6893cadeffc8be"}, + {file = "rpds_py-0.27.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d2a81bdcfde4245468f7030a75a37d50400ac2455c3a4819d9d550c937f90ab5"}, + {file = "rpds_py-0.27.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e6491658dd2569f05860bad645569145c8626ac231877b0fb2d5f9bcb7054089"}, + {file = "rpds_py-0.27.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:bec77545d188f8bdd29d42bccb9191682a46fb2e655e3d1fb446d47c55ac3b8d"}, + {file = "rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a4aebf8ca02bbb90a9b3e7a463bbf3bee02ab1c446840ca07b1695a68ce424"}, + {file = "rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:44524b96481a4c9b8e6c46d6afe43fa1fb485c261e359fbe32b63ff60e3884d8"}, + {file = "rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45d04a73c54b6a5fd2bab91a4b5bc8b426949586e61340e212a8484919183859"}, + {file = "rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:343cf24de9ed6c728abefc5d5c851d5de06497caa7ac37e5e65dd572921ed1b5"}, + {file = "rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aed8118ae20515974650d08eb724150dc2e20c2814bcc307089569995e88a14"}, + {file = "rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:af9d4fd79ee1cc8e7caf693ee02737daabfc0fcf2773ca0a4735b356c8ad6f7c"}, + {file = "rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f0396e894bd1e66c74ecbc08b4f6a03dc331140942c4b1d345dd131b68574a60"}, + {file = "rpds_py-0.27.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:59714ab0a5af25d723d8e9816638faf7f4254234decb7d212715c1aa71eee7be"}, + {file = "rpds_py-0.27.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:88051c3b7d5325409f433c5a40328fcb0685fc04e5db49ff936e910901d10114"}, + {file = "rpds_py-0.27.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:181bc29e59e5e5e6e9d63b143ff4d5191224d355e246b5a48c88ce6b35c4e466"}, + {file = "rpds_py-0.27.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9ad08547995a57e74fea6abaf5940d399447935faebbd2612b3b0ca6f987946b"}, + {file = "rpds_py-0.27.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:61490d57e82e23b45c66f96184237994bfafa914433b8cd1a9bb57fecfced59d"}, + {file = "rpds_py-0.27.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7cf5e726b6fa977e428a61880fb108a62f28b6d0c7ef675b117eaff7076df49"}, + {file = "rpds_py-0.27.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dc662bc9375a6a394b62dfd331874c434819f10ee3902123200dbcf116963f89"}, + {file = "rpds_py-0.27.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:299a245537e697f28a7511d01038c310ac74e8ea213c0019e1fc65f52c0dcb23"}, + {file = "rpds_py-0.27.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be3964f7312ea05ed283b20f87cb533fdc555b2e428cc7be64612c0b2124f08c"}, + {file = "rpds_py-0.27.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33ba649a6e55ae3808e4c39e01580dc9a9b0d5b02e77b66bb86ef117922b1264"}, + {file = "rpds_py-0.27.0-pp39-pypy39_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:81f81bbd7cdb4bdc418c09a73809abeda8f263a6bf8f9c7f93ed98b5597af39d"}, + {file = "rpds_py-0.27.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11e8e28c0ba0373d052818b600474cfee2fafa6c9f36c8587d217b13ee28ca7d"}, + {file = "rpds_py-0.27.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:e3acb9c16530362aeaef4e84d57db357002dc5cbfac9a23414c3e73c08301ab2"}, + {file = "rpds_py-0.27.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:2e307cb5f66c59ede95c00e93cd84190a5b7f3533d7953690b2036780622ba81"}, + {file = "rpds_py-0.27.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:f09c9d4c26fa79c1bad927efb05aca2391350b8e61c38cbc0d7d3c814e463124"}, + {file = "rpds_py-0.27.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:af22763a0a1eff106426a6e1f13c4582e0d0ad89c1493ab6c058236174cd6c6a"}, + {file = "rpds_py-0.27.0.tar.gz", hash = "sha256:8b23cf252f180cda89220b378d917180f29d313cd6a07b2431c0d3b776aae86f"}, ] [[package]] @@ -4962,18 +5191,18 @@ pyasn1 = ">=0.1.3" [[package]] name = "ruamel-yaml" -version = "0.18.10" +version = "0.18.15" 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.15-py3-none-any.whl", hash = "sha256:148f6488d698b7a5eded5ea793a025308b25eca97208181b6a026037f391f701"}, + {file = "ruamel.yaml-0.18.15.tar.gz", hash = "sha256:dbfca74b018c4c3fba0b9cc9ee33e53c371194a9000e694995e620490fd40700"}, ] [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"] @@ -4994,7 +5223,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"}, @@ -5003,7 +5231,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"}, @@ -5012,7 +5239,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"}, @@ -5021,7 +5247,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"}, @@ -5030,7 +5255,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"}, @@ -5066,21 +5290,21 @@ files = [ [[package]] name = "s3transfer" -version = "0.10.4" +version = "0.13.1" description = "An Amazon S3 Transfer Manager" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "s3transfer-0.10.4-py3-none-any.whl", hash = "sha256:244a76a24355363a68164241438de1b72f8781664920260c48465896b712a41e"}, - {file = "s3transfer-0.10.4.tar.gz", hash = "sha256:29edc09801743c21eb5ecbc617a152df41d3c287f67b615f73e5f750583666a7"}, + {file = "s3transfer-0.13.1-py3-none-any.whl", hash = "sha256:a981aa7429be23fe6dfc13e80e4020057cbab622b08c0315288758d67cabc724"}, + {file = "s3transfer-0.13.1.tar.gz", hash = "sha256:c3fdba22ba1bd367922f27ec8032d6a1cf5f10c934fb5d68cf60fd5a23d936cf"}, ] [package.dependencies] -botocore = ">=1.33.2,<2.0a.0" +botocore = ">=1.37.4,<2.0a.0" [package.extras] -crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"] +crt = ["botocore[crt] (>=1.37.4,<2.0a.0)"] [[package]] name = "safety" @@ -5139,26 +5363,29 @@ typing-extensions = ">=4.7.1" [[package]] name = "schema" -version = "0.7.7" +version = "0.7.5" description = "Simple data validation library" optional = false python-versions = "*" groups = ["main"] files = [ - {file = "schema-0.7.7-py2.py3-none-any.whl", hash = "sha256:5d976a5b50f36e74e2157b47097b60002bd4d42e65425fcc9c9befadb4255dde"}, - {file = "schema-0.7.7.tar.gz", hash = "sha256:7da553abd2958a19dc2547c388cde53398b39196175a9be59ea1caf5ab0a1807"}, + {file = "schema-0.7.5-py2.py3-none-any.whl", hash = "sha256:f3ffdeeada09ec34bf40d7d79996d9f7175db93b7a5065de0faa7f41083c1e6c"}, + {file = "schema-0.7.5.tar.gz", hash = "sha256:f06717112c61895cabc4707752b88716e8420a8819d71404501e114f91043197"}, ] +[package.dependencies] +contextlib2 = ">=0.5.5" + [[package]] name = "sentry-sdk" -version = "2.26.1" +version = "2.35.0" description = "Python client for Sentry (https://sentry.io)" optional = false python-versions = ">=3.6" groups = ["main"] files = [ - {file = "sentry_sdk-2.26.1-py2.py3-none-any.whl", hash = "sha256:e99390e3f217d13ddcbaeaed08789f1ca614d663b345b9da42e35ad6b60d696a"}, - {file = "sentry_sdk-2.26.1.tar.gz", hash = "sha256:759e019c41551a21519a95e6cef6d91fb4af1054761923dadaee2e6eca9c02c7"}, + {file = "sentry_sdk-2.35.0-py2.py3-none-any.whl", hash = "sha256:6e0c29b9a5d34de8575ffb04d289a987ff3053cf2c98ede445bea995e3830263"}, + {file = "sentry_sdk-2.35.0.tar.gz", hash = "sha256:5ea58d352779ce45d17bc2fa71ec7185205295b83a9dbb5707273deb64720092"}, ] [package.dependencies] @@ -5209,14 +5436,14 @@ unleash = ["UnleashClient (>=6.0.1)"] [[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] @@ -5316,14 +5543,14 @@ doc = ["sphinx"] [[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]] @@ -5396,14 +5623,14 @@ testing = ["mypy", "pytest", "pytest-gitignore", "pytest-mock", "responses", "ru [[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]] @@ -5430,14 +5657,14 @@ telegram = ["requests"] [[package]] name = "typer" -version = "0.15.2" +version = "0.16.1" 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.1-py3-none-any.whl", hash = "sha256:90ee01cb02d9b8395ae21ee3368421faf21fa138cb2a541ed369c08cec5237c9"}, + {file = "typer-0.16.1.tar.gz", hash = "sha256:d358c65a464a7a90f338e3bb7ff0c74ac081449e53884b12ba658cbd72990614"}, ] [package.dependencies] @@ -5448,16 +5675,31 @@ 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.1" +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.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"}, + {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"}, ] +[[package]] +name = "typing-inspection" +version = "0.4.1" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, + {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.0" + [[package]] name = "tzdata" version = "2025.2" @@ -5491,26 +5733,26 @@ devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3) [[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]] name = "urllib3" -version = "2.4.0" +version = "2.5.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813"}, - {file = "urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466"}, + {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, + {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, ] [package.extras] @@ -5586,14 +5828,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"] 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] @@ -5604,103 +5846,105 @@ watchdog = ["watchdog (>=2.3)"] [[package]] name = "wrapt" -version = "1.17.2" +version = "1.17.3" description = "Module for decorators, wrappers and monkey patching." optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984"}, - {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22"}, - {file = "wrapt-1.17.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80dd7db6a7cb57ffbc279c4394246414ec99537ae81ffd702443335a61dbf3a7"}, - {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a6e821770cf99cc586d33833b2ff32faebdbe886bd6322395606cf55153246c"}, - {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b60fb58b90c6d63779cb0c0c54eeb38941bae3ecf7a73c764c52c88c2dcb9d72"}, - {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b870b5df5b71d8c3359d21be8f0d6c485fa0ebdb6477dda51a1ea54a9b558061"}, - {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4011d137b9955791f9084749cba9a367c68d50ab8d11d64c50ba1688c9b457f2"}, - {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1473400e5b2733e58b396a04eb7f35f541e1fb976d0c0724d0223dd607e0f74c"}, - {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3cedbfa9c940fdad3e6e941db7138e26ce8aad38ab5fe9dcfadfed9db7a54e62"}, - {file = "wrapt-1.17.2-cp310-cp310-win32.whl", hash = "sha256:582530701bff1dec6779efa00c516496968edd851fba224fbd86e46cc6b73563"}, - {file = "wrapt-1.17.2-cp310-cp310-win_amd64.whl", hash = "sha256:58705da316756681ad3c9c73fd15499aa4d8c69f9fd38dc8a35e06c12468582f"}, - {file = "wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58"}, - {file = "wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda"}, - {file = "wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438"}, - {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a"}, - {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000"}, - {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6"}, - {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b"}, - {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662"}, - {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72"}, - {file = "wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317"}, - {file = "wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3"}, - {file = "wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925"}, - {file = "wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392"}, - {file = "wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40"}, - {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d"}, - {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b"}, - {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98"}, - {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82"}, - {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae"}, - {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9"}, - {file = "wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9"}, - {file = "wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991"}, - {file = "wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125"}, - {file = "wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998"}, - {file = "wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5"}, - {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8"}, - {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6"}, - {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc"}, - {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2"}, - {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b"}, - {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504"}, - {file = "wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a"}, - {file = "wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845"}, - {file = "wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192"}, - {file = "wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b"}, - {file = "wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0"}, - {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306"}, - {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb"}, - {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681"}, - {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6"}, - {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6"}, - {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f"}, - {file = "wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555"}, - {file = "wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c"}, - {file = "wrapt-1.17.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5c803c401ea1c1c18de70a06a6f79fcc9c5acfc79133e9869e730ad7f8ad8ef9"}, - {file = "wrapt-1.17.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f917c1180fdb8623c2b75a99192f4025e412597c50b2ac870f156de8fb101119"}, - {file = "wrapt-1.17.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ecc840861360ba9d176d413a5489b9a0aff6d6303d7e733e2c4623cfa26904a6"}, - {file = "wrapt-1.17.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb87745b2e6dc56361bfde481d5a378dc314b252a98d7dd19a651a3fa58f24a9"}, - {file = "wrapt-1.17.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58455b79ec2661c3600e65c0a716955adc2410f7383755d537584b0de41b1d8a"}, - {file = "wrapt-1.17.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4e42a40a5e164cbfdb7b386c966a588b1047558a990981ace551ed7e12ca9c2"}, - {file = "wrapt-1.17.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:91bd7d1773e64019f9288b7a5101f3ae50d3d8e6b1de7edee9c2ccc1d32f0c0a"}, - {file = "wrapt-1.17.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:bb90fb8bda722a1b9d48ac1e6c38f923ea757b3baf8ebd0c82e09c5c1a0e7a04"}, - {file = "wrapt-1.17.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:08e7ce672e35efa54c5024936e559469436f8b8096253404faeb54d2a878416f"}, - {file = "wrapt-1.17.2-cp38-cp38-win32.whl", hash = "sha256:410a92fefd2e0e10d26210e1dfb4a876ddaf8439ef60d6434f21ef8d87efc5b7"}, - {file = "wrapt-1.17.2-cp38-cp38-win_amd64.whl", hash = "sha256:95c658736ec15602da0ed73f312d410117723914a5c91a14ee4cdd72f1d790b3"}, - {file = "wrapt-1.17.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99039fa9e6306880572915728d7f6c24a86ec57b0a83f6b2491e1d8ab0235b9a"}, - {file = "wrapt-1.17.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2696993ee1eebd20b8e4ee4356483c4cb696066ddc24bd70bcbb80fa56ff9061"}, - {file = "wrapt-1.17.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:612dff5db80beef9e649c6d803a8d50c409082f1fedc9dbcdfde2983b2025b82"}, - {file = "wrapt-1.17.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c2caa1585c82b3f7a7ab56afef7b3602021d6da34fbc1cf234ff139fed3cd9"}, - {file = "wrapt-1.17.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c958bcfd59bacc2d0249dcfe575e71da54f9dcf4a8bdf89c4cb9a68a1170d73f"}, - {file = "wrapt-1.17.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc78a84e2dfbc27afe4b2bd7c80c8db9bca75cc5b85df52bfe634596a1da846b"}, - {file = "wrapt-1.17.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ba0f0eb61ef00ea10e00eb53a9129501f52385c44853dbd6c4ad3f403603083f"}, - {file = "wrapt-1.17.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1e1fe0e6ab7775fd842bc39e86f6dcfc4507ab0ffe206093e76d61cde37225c8"}, - {file = "wrapt-1.17.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c86563182421896d73858e08e1db93afdd2b947a70064b813d515d66549e15f9"}, - {file = "wrapt-1.17.2-cp39-cp39-win32.whl", hash = "sha256:f393cda562f79828f38a819f4788641ac7c4085f30f1ce1a68672baa686482bb"}, - {file = "wrapt-1.17.2-cp39-cp39-win_amd64.whl", hash = "sha256:36ccae62f64235cf8ddb682073a60519426fdd4725524ae38874adf72b5f2aeb"}, - {file = "wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8"}, - {file = "wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3"}, + {file = "wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04"}, + {file = "wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2"}, + {file = "wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c"}, + {file = "wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775"}, + {file = "wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd"}, + {file = "wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05"}, + {file = "wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418"}, + {file = "wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390"}, + {file = "wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6"}, + {file = "wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18"}, + {file = "wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7"}, + {file = "wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85"}, + {file = "wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f"}, + {file = "wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311"}, + {file = "wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1"}, + {file = "wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5"}, + {file = "wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2"}, + {file = "wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89"}, + {file = "wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77"}, + {file = "wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a"}, + {file = "wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0"}, + {file = "wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba"}, + {file = "wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd"}, + {file = "wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828"}, + {file = "wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9"}, + {file = "wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396"}, + {file = "wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc"}, + {file = "wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe"}, + {file = "wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c"}, + {file = "wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6"}, + {file = "wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0"}, + {file = "wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77"}, + {file = "wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7"}, + {file = "wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277"}, + {file = "wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d"}, + {file = "wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa"}, + {file = "wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050"}, + {file = "wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8"}, + {file = "wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb"}, + {file = "wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16"}, + {file = "wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39"}, + {file = "wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235"}, + {file = "wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c"}, + {file = "wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b"}, + {file = "wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa"}, + {file = "wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7"}, + {file = "wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4"}, + {file = "wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10"}, + {file = "wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6"}, + {file = "wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58"}, + {file = "wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a"}, + {file = "wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067"}, + {file = "wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454"}, + {file = "wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e"}, + {file = "wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f"}, + {file = "wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056"}, + {file = "wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804"}, + {file = "wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977"}, + {file = "wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116"}, + {file = "wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6"}, + {file = "wrapt-1.17.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:70d86fa5197b8947a2fa70260b48e400bf2ccacdcab97bb7de47e3d1e6312225"}, + {file = "wrapt-1.17.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:df7d30371a2accfe4013e90445f6388c570f103d61019b6b7c57e0265250072a"}, + {file = "wrapt-1.17.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:caea3e9c79d5f0d2c6d9ab96111601797ea5da8e6d0723f77eabb0d4068d2b2f"}, + {file = "wrapt-1.17.3-cp38-cp38-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:758895b01d546812d1f42204bd443b8c433c44d090248bf22689df673ccafe00"}, + {file = "wrapt-1.17.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02b551d101f31694fc785e58e0720ef7d9a10c4e62c1c9358ce6f63f23e30a56"}, + {file = "wrapt-1.17.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:656873859b3b50eeebe6db8b1455e99d90c26ab058db8e427046dbc35c3140a5"}, + {file = "wrapt-1.17.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a9a2203361a6e6404f80b99234fe7fb37d1fc73487b5a78dc1aa5b97201e0f22"}, + {file = "wrapt-1.17.3-cp38-cp38-win32.whl", hash = "sha256:55cbbc356c2842f39bcc553cf695932e8b30e30e797f961860afb308e6b1bb7c"}, + {file = "wrapt-1.17.3-cp38-cp38-win_amd64.whl", hash = "sha256:ad85e269fe54d506b240d2d7b9f5f2057c2aa9a2ea5b32c66f8902f768117ed2"}, + {file = "wrapt-1.17.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:30ce38e66630599e1193798285706903110d4f057aab3168a34b7fdc85569afc"}, + {file = "wrapt-1.17.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:65d1d00fbfb3ea5f20add88bbc0f815150dbbde3b026e6c24759466c8b5a9ef9"}, + {file = "wrapt-1.17.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a7c06742645f914f26c7f1fa47b8bc4c91d222f76ee20116c43d5ef0912bba2d"}, + {file = "wrapt-1.17.3-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7e18f01b0c3e4a07fe6dfdb00e29049ba17eadbc5e7609a2a3a4af83ab7d710a"}, + {file = "wrapt-1.17.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f5f51a6466667a5a356e6381d362d259125b57f059103dd9fdc8c0cf1d14139"}, + {file = "wrapt-1.17.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:59923aa12d0157f6b82d686c3fd8e1166fa8cdfb3e17b42ce3b6147ff81528df"}, + {file = "wrapt-1.17.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:46acc57b331e0b3bcb3e1ca3b421d65637915cfcd65eb783cb2f78a511193f9b"}, + {file = "wrapt-1.17.3-cp39-cp39-win32.whl", hash = "sha256:3e62d15d3cfa26e3d0788094de7b64efa75f3a53875cdbccdf78547aed547a81"}, + {file = "wrapt-1.17.3-cp39-cp39-win_amd64.whl", hash = "sha256:1f23fa283f51c890eda8e34e4937079114c74b4c81d2b2f1f1d94948f5cc3d7f"}, + {file = "wrapt-1.17.3-cp39-cp39-win_arm64.whl", hash = "sha256:24c2ed34dc222ed754247a2702b1e1e89fdbaa4016f324b4b8f1a802d4ffe87f"}, + {file = "wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22"}, + {file = "wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0"}, ] [[package]] name = "xlsxwriter" -version = "3.2.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]] @@ -5776,116 +6020,116 @@ lxml = ">=3.8" [[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] @@ -5895,14 +6139,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"] 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"}, ] [package.extras] @@ -5910,10 +6154,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.11,<3.13" -content-hash = "6802b33984c2f8438c9dc02dac0a0c14d5a78af60251bd0c80ca59bc2182c48e" +content-hash = "b954196aba7e108cacb94fd15732be7130b27379add09140fabbb55f7335bb7b" diff --git a/api/pyproject.toml b/api/pyproject.toml index df05eb33ce..3a597281cc 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -30,7 +30,8 @@ dependencies = [ "sentry-sdk[django] (>=2.20.0,<3.0.0)", "uuid6==2024.7.10", "openai (>=1.82.0,<2.0.0)", - "xmlsec==1.3.14" + "xmlsec==1.3.14", + "h2 (==4.3.0)" ] description = "Prowler's API (Django/DRF)" license = "Apache-2.0" @@ -38,7 +39,7 @@ name = "prowler-api" package-mode = false # Needed for the SDK compatibility requires-python = ">=3.11,<3.13" -version = "1.11.0" +version = "1.12.0" [project.scripts] celery = "src.backend.config.settings.celery" diff --git a/api/src/backend/api/migrations/0046_lighthouse_gpt5.py b/api/src/backend/api/migrations/0046_lighthouse_gpt5.py new file mode 100644 index 0000000000..b2244b458c --- /dev/null +++ b/api/src/backend/api/migrations/0046_lighthouse_gpt5.py @@ -0,0 +1,33 @@ +# Generated by Django 5.1.10 on 2025-08-20 09:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("api", "0045_alter_scan_output_location"), + ] + + operations = [ + migrations.AlterField( + model_name="lighthouseconfiguration", + name="model", + field=models.CharField( + choices=[ + ("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"), + ("gpt-5-2025-08-07", "GPT-5 v2025-08-07"), + ("gpt-5", "GPT-5 Default"), + ("gpt-5-mini-2025-08-07", "GPT-5 Mini v2025-08-07"), + ("gpt-5-mini", "GPT-5 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/0047_remove_integration_unique_configuration_per_tenant.py b/api/src/backend/api/migrations/0047_remove_integration_unique_configuration_per_tenant.py new file mode 100644 index 0000000000..42d9a7987b --- /dev/null +++ b/api/src/backend/api/migrations/0047_remove_integration_unique_configuration_per_tenant.py @@ -0,0 +1,16 @@ +# Generated by Django 5.1.10 on 2025-08-20 08:24 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("api", "0046_lighthouse_gpt5"), + ] + + operations = [ + migrations.RemoveConstraint( + model_name="integration", + name="unique_configuration_per_tenant", + ), + ] diff --git a/api/src/backend/api/models.py b/api/src/backend/api/models.py index e08f9919a8..6c20032dbc 100644 --- a/api/src/backend/api/models.py +++ b/api/src/backend/api/models.py @@ -1372,10 +1372,6 @@ class Integration(RowLevelSecurityProtectedModel): db_table = "integrations" constraints = [ - models.UniqueConstraint( - fields=("configuration", "tenant"), - name="unique_configuration_per_tenant", - ), RowLevelSecurityConstraint( field="tenant_id", name="rls_on_%(class)s", diff --git a/api/src/backend/api/specs/v1.yaml b/api/src/backend/api/specs/v1.yaml index f8df5e6256..cf64a00ffd 100644 --- a/api/src/backend/api/specs/v1.yaml +++ b/api/src/backend/api/specs/v1.yaml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: Prowler API - version: 1.11.0 + version: 1.12.0 description: |- Prowler API specification. @@ -8918,6 +8918,19 @@ components: default: output required: - bucket_name + - type: object + title: AWS Security Hub + properties: + send_only_fails: + type: boolean + default: false + description: If true, only findings with status 'FAIL' will + be sent to Security Hub. + archive_previous_findings: + type: boolean + default: false + description: If true, archives findings that are not present in + the current execution. credentials: oneOf: - type: object @@ -9064,6 +9077,19 @@ components: default: output required: - bucket_name + - type: object + title: AWS Security Hub + properties: + send_only_fails: + type: boolean + default: false + description: If true, only findings with status 'FAIL' will + be sent to Security Hub. + archive_previous_findings: + type: boolean + default: false + description: If true, archives findings that are not present + in the current execution. credentials: oneOf: - type: object @@ -9225,6 +9251,19 @@ components: default: output required: - bucket_name + - type: object + title: AWS Security Hub + properties: + send_only_fails: + type: boolean + default: false + description: If true, only findings with status 'FAIL' will + be sent to Security Hub. + archive_previous_findings: + type: boolean + default: false + description: If true, archives findings that are not present in + the current execution. credentials: oneOf: - type: object @@ -9777,6 +9816,10 @@ components: - gpt-4o - gpt-4o-mini-2024-07-18 - gpt-4o-mini + - gpt-5-2025-08-07 + - gpt-5 + - gpt-5-mini-2025-08-07 + - gpt-5-mini type: string description: |- Must be one of the supported model names @@ -9787,6 +9830,10 @@ components: * `gpt-4o` - GPT-4o Default * `gpt-4o-mini-2024-07-18` - GPT-4o Mini v2024-07-18 * `gpt-4o-mini` - GPT-4o Mini Default + * `gpt-5-2025-08-07` - GPT-5 v2025-08-07 + * `gpt-5` - GPT-5 Default + * `gpt-5-mini-2025-08-07` - GPT-5 Mini v2025-08-07 + * `gpt-5-mini` - GPT-5 Mini Default temperature: type: number format: double @@ -9843,6 +9890,10 @@ components: - gpt-4o - gpt-4o-mini-2024-07-18 - gpt-4o-mini + - gpt-5-2025-08-07 + - gpt-5 + - gpt-5-mini-2025-08-07 + - gpt-5-mini type: string description: |- Must be one of the supported model names @@ -9853,6 +9904,10 @@ components: * `gpt-4o` - GPT-4o Default * `gpt-4o-mini-2024-07-18` - GPT-4o Mini v2024-07-18 * `gpt-4o-mini` - GPT-4o Mini Default + * `gpt-5-2025-08-07` - GPT-5 v2025-08-07 + * `gpt-5` - GPT-5 Default + * `gpt-5-mini-2025-08-07` - GPT-5 Mini v2025-08-07 + * `gpt-5-mini` - GPT-5 Mini Default temperature: type: number format: double @@ -9915,6 +9970,10 @@ components: - gpt-4o - gpt-4o-mini-2024-07-18 - gpt-4o-mini + - gpt-5-2025-08-07 + - gpt-5 + - gpt-5-mini-2025-08-07 + - gpt-5-mini type: string description: |- Must be one of the supported model names @@ -9925,6 +9984,10 @@ components: * `gpt-4o` - GPT-4o Default * `gpt-4o-mini-2024-07-18` - GPT-4o Mini v2024-07-18 * `gpt-4o-mini` - GPT-4o Mini Default + * `gpt-5-2025-08-07` - GPT-5 v2025-08-07 + * `gpt-5` - GPT-5 Default + * `gpt-5-mini-2025-08-07` - GPT-5 Mini v2025-08-07 + * `gpt-5-mini` - GPT-5 Mini Default temperature: type: number format: double @@ -9995,6 +10058,10 @@ components: - gpt-4o - gpt-4o-mini-2024-07-18 - gpt-4o-mini + - gpt-5-2025-08-07 + - gpt-5 + - gpt-5-mini-2025-08-07 + - gpt-5-mini type: string description: |- Must be one of the supported model names @@ -10005,6 +10072,10 @@ components: * `gpt-4o` - GPT-4o Default * `gpt-4o-mini-2024-07-18` - GPT-4o Mini v2024-07-18 * `gpt-4o-mini` - GPT-4o Mini Default + * `gpt-5-2025-08-07` - GPT-5 v2025-08-07 + * `gpt-5` - GPT-5 Default + * `gpt-5-mini-2025-08-07` - GPT-5 Mini v2025-08-07 + * `gpt-5-mini` - GPT-5 Mini Default temperature: type: number format: double @@ -10584,6 +10655,19 @@ components: default: output required: - bucket_name + - type: object + title: AWS Security Hub + properties: + send_only_fails: + type: boolean + default: false + description: If true, only findings with status 'FAIL' will + be sent to Security Hub. + archive_previous_findings: + type: boolean + default: false + description: If true, archives findings that are not present + in the current execution. credentials: oneOf: - type: object @@ -10779,6 +10863,10 @@ components: - gpt-4o - gpt-4o-mini-2024-07-18 - gpt-4o-mini + - gpt-5-2025-08-07 + - gpt-5 + - gpt-5-mini-2025-08-07 + - gpt-5-mini type: string description: |- Must be one of the supported model names @@ -10789,6 +10877,10 @@ components: * `gpt-4o` - GPT-4o Default * `gpt-4o-mini-2024-07-18` - GPT-4o Mini v2024-07-18 * `gpt-4o-mini` - GPT-4o Mini Default + * `gpt-5-2025-08-07` - GPT-5 v2025-08-07 + * `gpt-5` - GPT-5 Default + * `gpt-5-mini-2025-08-07` - GPT-5 Mini v2025-08-07 + * `gpt-5-mini` - GPT-5 Mini Default temperature: type: number format: double diff --git a/api/src/backend/api/utils.py b/api/src/backend/api/utils.py index 7bc427c375..7cbb7a0247 100644 --- a/api/src/backend/api/utils.py +++ b/api/src/backend/api/utils.py @@ -11,6 +11,7 @@ from api.models import Integration, Invitation, Processor, Provider, Resource from api.v1.serializers import FindingMetadataSerializer from prowler.providers.aws.aws_provider import AwsProvider from prowler.providers.aws.lib.s3.s3 import S3 +from prowler.providers.aws.lib.security_hub.security_hub import SecurityHub from prowler.providers.azure.azure_provider import AzureProvider from prowler.providers.common.models import Connection from prowler.providers.gcp.gcp_provider import GcpProvider @@ -196,7 +197,34 @@ def prowler_integration_connection_test(integration: Integration) -> Connection: elif ( integration.integration_type == Integration.IntegrationChoices.AWS_SECURITY_HUB ): - pass + # Get the provider associated with this integration + provider_relationship = integration.integrationproviderrelationship_set.first() + if not provider_relationship: + return Connection( + is_connected=False, error="No provider associated with this integration" + ) + + credentials = ( + integration.credentials + if integration.credentials + else provider_relationship.provider.secret.secret + ) + connection = SecurityHub.test_connection( + aws_account_id=provider_relationship.provider.uid, + raise_on_exception=False, + **credentials, + ) + + # Only save regions if connection is successful + if connection.is_connected: + regions_status = {r: True for r in connection.enabled_regions} + regions_status.update({r: False for r in connection.disabled_regions}) + + # Save regions information in the integration configuration + integration.configuration["regions"] = regions_status + integration.save() + + return connection elif integration.integration_type == Integration.IntegrationChoices.JIRA: pass elif integration.integration_type == Integration.IntegrationChoices.SLACK: diff --git a/api/src/backend/api/v1/serializer_utils/integrations.py b/api/src/backend/api/v1/serializer_utils/integrations.py index 1df0550655..d15eee88ff 100644 --- a/api/src/backend/api/v1/serializer_utils/integrations.py +++ b/api/src/backend/api/v1/serializer_utils/integrations.py @@ -52,6 +52,21 @@ class S3ConfigSerializer(BaseValidateSerializer): resource_name = "integrations" +class SecurityHubConfigSerializer(BaseValidateSerializer): + send_only_fails = serializers.BooleanField(default=False) + archive_previous_findings = serializers.BooleanField(default=False) + regions = serializers.DictField(default=dict, read_only=True) + + def to_internal_value(self, data): + validated_data = super().to_internal_value(data) + # Always initialize regions as empty dict + validated_data["regions"] = {} + return validated_data + + class Meta: + resource_name = "integrations" + + class AWSCredentialSerializer(BaseValidateSerializer): role_arn = serializers.CharField(required=False) external_id = serializers.CharField(required=False) @@ -146,6 +161,22 @@ class IntegrationCredentialField(serializers.JSONField): }, "required": ["bucket_name"], }, + { + "type": "object", + "title": "AWS Security Hub", + "properties": { + "send_only_fails": { + "type": "boolean", + "default": False, + "description": "If true, only findings with status 'FAIL' will be sent to Security Hub.", + }, + "archive_previous_findings": { + "type": "boolean", + "default": False, + "description": "If true, archives findings that are not present in the current execution.", + }, + }, + }, ] } ) diff --git a/api/src/backend/api/v1/serializers.py b/api/src/backend/api/v1/serializers.py index e301d2f6c3..30f942b04d 100644 --- a/api/src/backend/api/v1/serializers.py +++ b/api/src/backend/api/v1/serializers.py @@ -46,6 +46,7 @@ from api.v1.serializer_utils.integrations import ( IntegrationConfigField, IntegrationCredentialField, S3ConfigSerializer, + SecurityHubConfigSerializer, ) from api.v1.serializer_utils.processors import ProcessorConfigField from api.v1.serializer_utils.providers import ProviderSecretField @@ -1951,13 +1952,44 @@ class ScheduleDailyCreateSerializer(serializers.Serializer): class BaseWriteIntegrationSerializer(BaseWriteSerializer): def validate(self, attrs): - if Integration.objects.filter( - configuration=attrs.get("configuration") - ).exists(): + if ( + attrs.get("integration_type") == Integration.IntegrationChoices.AMAZON_S3 + and Integration.objects.filter( + configuration=attrs.get("configuration") + ).exists() + ): raise serializers.ValidationError( - {"name": "This integration already exists."} + {"configuration": "This integration already exists."} ) + # Check if any provider already has a SecurityHub integration + integration_type = attrs.get("integration_type") + if hasattr(self, "instance") and self.instance and not integration_type: + integration_type = self.instance.integration_type + + if ( + integration_type == Integration.IntegrationChoices.AWS_SECURITY_HUB + and "providers" in attrs + ): + providers = attrs.get("providers", []) + tenant_id = self.context.get("tenant_id") + for provider in providers: + # For updates, exclude the current instance from the check + query = IntegrationProviderRelationship.objects.filter( + provider=provider, + integration__integration_type=Integration.IntegrationChoices.AWS_SECURITY_HUB, + tenant_id=tenant_id, + ) + if hasattr(self, "instance") and self.instance: + query = query.exclude(integration=self.instance) + + if query.exists(): + raise serializers.ValidationError( + { + "providers": f"Provider {provider.id} already has a Security Hub integration. Only one Security Hub integration is allowed per provider." + } + ) + return super().validate(attrs) @staticmethod @@ -1970,14 +2002,22 @@ class BaseWriteIntegrationSerializer(BaseWriteSerializer): if integration_type == Integration.IntegrationChoices.AMAZON_S3: config_serializer = S3ConfigSerializer credentials_serializers = [AWSCredentialSerializer] - # TODO: This will be required for AWS Security Hub - # if providers and not all( - # provider.provider == Provider.ProviderChoices.AWS - # for provider in providers - # ): - # raise serializers.ValidationError( - # {"providers": "All providers must be AWS for the S3 integration."} - # ) + elif integration_type == Integration.IntegrationChoices.AWS_SECURITY_HUB: + if providers: + if len(providers) > 1: + raise serializers.ValidationError( + { + "providers": "Only one provider is supported for the Security Hub integration." + } + ) + if providers[0].provider != Provider.ProviderChoices.AWS: + raise serializers.ValidationError( + { + "providers": "The provider must be AWS type for the Security Hub integration." + } + ) + config_serializer = SecurityHubConfigSerializer + credentials_serializers = [AWSCredentialSerializer] else: raise serializers.ValidationError( { @@ -2077,6 +2117,16 @@ class IntegrationCreateSerializer(BaseWriteIntegrationSerializer): configuration = attrs.get("configuration") credentials = attrs.get("credentials") + if ( + not providers + and integration_type == Integration.IntegrationChoices.AWS_SECURITY_HUB + ): + raise serializers.ValidationError( + { + "providers": "At least one provider is required for the Security Hub integration." + } + ) + self.validate_integration_data( integration_type, providers, configuration, credentials ) @@ -2131,16 +2181,15 @@ class IntegrationUpdateSerializer(BaseWriteIntegrationSerializer): } def validate(self, attrs): - super().validate(attrs) integration_type = self.instance.integration_type providers = attrs.get("providers") configuration = attrs.get("configuration") or self.instance.configuration credentials = attrs.get("credentials") or self.instance.credentials - validated_attrs = super().validate(attrs) self.validate_integration_data( integration_type, providers, configuration, credentials ) + validated_attrs = super().validate(attrs) return validated_attrs def update(self, instance, validated_data): @@ -2155,6 +2204,13 @@ class IntegrationUpdateSerializer(BaseWriteIntegrationSerializer): ] IntegrationProviderRelationship.objects.bulk_create(new_relationships) + # Preserve regions field for Security Hub integrations + if instance.integration_type == Integration.IntegrationChoices.AWS_SECURITY_HUB: + if "configuration" in validated_data: + # Preserve the existing regions field if it exists + existing_regions = instance.configuration.get("regions", {}) + validated_data["configuration"]["regions"] = existing_regions + return super().update(instance, validated_data) diff --git a/api/src/backend/api/v1/views.py b/api/src/backend/api/v1/views.py index d321d3eef8..28cbc3ab5f 100644 --- a/api/src/backend/api/v1/views.py +++ b/api/src/backend/api/v1/views.py @@ -293,7 +293,7 @@ class SchemaView(SpectacularAPIView): def get(self, request, *args, **kwargs): spectacular_settings.TITLE = "Prowler API" - spectacular_settings.VERSION = "1.11.0" + spectacular_settings.VERSION = "1.12.0" spectacular_settings.DESCRIPTION = ( "Prowler API specification.\n\nThis file is auto-generated." ) diff --git a/api/src/backend/tasks/jobs/export.py b/api/src/backend/tasks/jobs/export.py index f9232e1ed0..441b7a95d4 100644 --- a/api/src/backend/tasks/jobs/export.py +++ b/api/src/backend/tasks/jobs/export.py @@ -13,8 +13,10 @@ from api.models import Scan from prowler.config.config import ( csv_file_suffix, html_file_suffix, + json_asff_file_suffix, json_ocsf_file_suffix, ) +from prowler.lib.outputs.asff.asff import ASFF from prowler.lib.outputs.compliance.aws_well_architected.aws_well_architected import ( AWSWellArchitected, ) @@ -109,6 +111,7 @@ OUTPUT_FORMATS_MAPPING = { "kwargs": {}, }, "json-ocsf": {"class": OCSF, "suffix": json_ocsf_file_suffix, "kwargs": {}}, + "json-asff": {"class": ASFF, "suffix": json_asff_file_suffix, "kwargs": {}}, "html": {"class": HTML, "suffix": html_file_suffix, "kwargs": {"stats": {}}}, } diff --git a/api/src/backend/tasks/jobs/integrations.py b/api/src/backend/tasks/jobs/integrations.py index 52bd744d76..5cc25d09a8 100644 --- a/api/src/backend/tasks/jobs/integrations.py +++ b/api/src/backend/tasks/jobs/integrations.py @@ -2,15 +2,21 @@ import os from glob import glob from celery.utils.log import get_task_logger +from config.django.base import DJANGO_FINDINGS_BATCH_SIZE +from tasks.utils import batched from api.db_utils import rls_transaction -from api.models import Integration +from api.models import Finding, Integration, Provider +from api.utils import initialize_prowler_provider from prowler.lib.outputs.asff.asff import ASFF from prowler.lib.outputs.compliance.generic.generic import GenericCompliance from prowler.lib.outputs.csv.csv import CSV +from prowler.lib.outputs.finding import Finding as FindingOutput from prowler.lib.outputs.html.html import HTML from prowler.lib.outputs.ocsf.ocsf import OCSF +from prowler.providers.aws.aws_provider import AwsProvider from prowler.providers.aws.lib.s3.s3 import S3 +from prowler.providers.aws.lib.security_hub.security_hub import SecurityHub from prowler.providers.common.models import Connection logger = get_task_logger(__name__) @@ -154,3 +160,265 @@ def upload_s3_integration( except Exception as e: logger.error(f"S3 integrations failed for provider {provider_id}: {str(e)}") return False + + +def get_security_hub_client_from_integration( + integration: Integration, tenant_id: str, findings: list +) -> tuple[bool, SecurityHub | Connection]: + """ + Create and return a SecurityHub client using AWS credentials from an integration. + + Args: + integration (Integration): The integration to get the Security Hub client from. + tenant_id (str): The tenant identifier. + findings (list): List of findings in ASFF format to send to Security Hub. + + Returns: + tuple[bool, SecurityHub | Connection]: A tuple containing a boolean indicating + if the connection was successful and the SecurityHub client or connection object. + """ + # Get the provider associated with this integration + with rls_transaction(tenant_id): + provider_relationship = integration.integrationproviderrelationship_set.first() + if not provider_relationship: + return Connection( + is_connected=False, error="No provider associated with this integration" + ) + provider_uid = provider_relationship.provider.uid + provider_secret = provider_relationship.provider.secret.secret + + credentials = ( + integration.credentials if integration.credentials else provider_secret + ) + connection = SecurityHub.test_connection( + aws_account_id=provider_uid, + raise_on_exception=False, + **credentials, + ) + + if connection.is_connected: + all_security_hub_regions = AwsProvider.get_available_aws_service_regions( + "securityhub", connection.partition + ) + + # Create regions status dictionary + regions_status = {} + for region in set(all_security_hub_regions): + regions_status[region] = region in connection.enabled_regions + + # Save regions information in the integration configuration + with rls_transaction(tenant_id): + integration.configuration["regions"] = regions_status + integration.save() + + # Create SecurityHub client with all necessary parameters + security_hub = SecurityHub( + aws_account_id=provider_uid, + findings=findings, + send_only_fails=integration.configuration.get("send_only_fails", False), + aws_security_hub_available_regions=list(connection.enabled_regions), + **credentials, + ) + return True, security_hub + + return False, connection + + +def upload_security_hub_integration( + tenant_id: str, provider_id: str, scan_id: str +) -> bool: + """ + Upload findings to AWS Security Hub using configured integrations. + + This function retrieves findings from the database, transforms them to ASFF format, + and sends them to AWS Security Hub using the configured integration credentials. + + Args: + tenant_id (str): The tenant identifier. + provider_id (str): The provider identifier. + scan_id (str): The scan identifier for which to send findings. + + Returns: + bool: True if all integrations executed successfully, False otherwise. + """ + logger.info(f"Processing Security Hub integrations for provider {provider_id}") + + try: + with rls_transaction(tenant_id): + # Get Security Hub integrations for this provider + integrations = list( + Integration.objects.filter( + integrationproviderrelationship__provider_id=provider_id, + integration_type=Integration.IntegrationChoices.AWS_SECURITY_HUB, + enabled=True, + ) + ) + + if not integrations: + logger.error( + f"No Security Hub integrations found for provider {provider_id}" + ) + return False + + # Get the provider object + provider = Provider.objects.get(id=provider_id) + + # Initialize prowler provider for finding transformation + prowler_provider = initialize_prowler_provider(provider) + + # Process each Security Hub integration + integration_executions = 0 + total_findings_sent = {} # Track findings sent per integration + + for integration in integrations: + try: + # Initialize Security Hub client for this integration + # We'll create the client once and reuse it for all batches + security_hub_client = None + send_only_fails = integration.configuration.get( + "send_only_fails", False + ) + total_findings_sent[integration.id] = 0 + + # Process findings in batches to avoid memory issues + has_findings = False + batch_number = 0 + + with rls_transaction(tenant_id): + qs = ( + Finding.all_objects.filter(tenant_id=tenant_id, scan_id=scan_id) + .order_by("uid") + .iterator() + ) + + for batch, _ in batched(qs, DJANGO_FINDINGS_BATCH_SIZE): + batch_number += 1 + has_findings = True + + # Transform findings for this batch + transformed_findings = [ + FindingOutput.transform_api_finding( + finding, prowler_provider + ) + for finding in batch + ] + + # Convert to ASFF format + asff_transformer = ASFF( + findings=transformed_findings, + file_path="", + file_extension="json", + ) + asff_transformer.transform(transformed_findings) + + # Get the batch of ASFF findings + batch_asff_findings = asff_transformer.data + + if batch_asff_findings: + # Create Security Hub client for first batch or reuse existing + if not security_hub_client: + connected, security_hub = ( + get_security_hub_client_from_integration( + integration, tenant_id, batch_asff_findings + ) + ) + + if not connected: + logger.error( + f"Security Hub connection failed for integration {integration.id}: {security_hub.error}" + ) + integration.connected = False + integration.save() + break # Skip this integration + + security_hub_client = security_hub + logger.info( + f"Sending {'fail' if send_only_fails else 'all'} findings to Security Hub via integration {integration.id}" + ) + else: + # Update findings in existing client for this batch + security_hub_client._findings_per_region = ( + security_hub_client.filter( + batch_asff_findings, send_only_fails + ) + ) + + # Send this batch to Security Hub + try: + findings_sent = ( + security_hub_client.batch_send_to_security_hub() + ) + total_findings_sent[integration.id] += findings_sent + + if findings_sent > 0: + logger.debug( + f"Sent batch {batch_number} with {findings_sent} findings to Security Hub" + ) + except Exception as batch_error: + logger.error( + f"Failed to send batch {batch_number} to Security Hub: {str(batch_error)}" + ) + + # Clear memory after processing each batch + asff_transformer._data.clear() + del batch_asff_findings + del transformed_findings + + if not has_findings: + logger.info( + f"No findings to send to Security Hub for scan {scan_id}" + ) + integration_executions += 1 + elif security_hub_client: + if total_findings_sent[integration.id] > 0: + logger.info( + f"Successfully sent {total_findings_sent[integration.id]} total findings to Security Hub via integration {integration.id}" + ) + integration_executions += 1 + else: + logger.warning( + f"No findings were sent to Security Hub via integration {integration.id}" + ) + + # Archive previous findings if configured to do so + if integration.configuration.get( + "archive_previous_findings", False + ): + logger.info( + f"Archiving previous findings in Security Hub via integration {integration.id}" + ) + try: + findings_archived = ( + security_hub_client.archive_previous_findings() + ) + logger.info( + f"Successfully archived {findings_archived} previous findings in Security Hub" + ) + except Exception as archive_error: + logger.warning( + f"Failed to archive previous findings: {str(archive_error)}" + ) + + except Exception as e: + logger.error( + f"Security Hub integration {integration.id} failed: {str(e)}" + ) + continue + + result = integration_executions == len(integrations) + if result: + logger.info( + f"All Security Hub integrations completed successfully for provider {provider_id}" + ) + else: + logger.error( + f"Some Security Hub integrations failed for provider {provider_id}" + ) + + return result + + except Exception as e: + logger.error( + f"Security Hub integrations failed for provider {provider_id}: {str(e)}" + ) + return False diff --git a/api/src/backend/tasks/tasks.py b/api/src/backend/tasks/tasks.py index b0dff077aa..872db504fb 100644 --- a/api/src/backend/tasks/tasks.py +++ b/api/src/backend/tasks/tasks.py @@ -21,7 +21,10 @@ from tasks.jobs.export import ( _generate_output_directory, _upload_to_s3, ) -from tasks.jobs.integrations import upload_s3_integration +from tasks.jobs.integrations import ( + upload_s3_integration, + upload_security_hub_integration, +) from tasks.jobs.scan import ( aggregate_findings, create_compliance_requirements, @@ -62,6 +65,7 @@ def _perform_scan_complete_tasks(tenant_id: str, scan_id: str, provider_id: str) check_integrations_task.si( tenant_id=tenant_id, provider_id=provider_id, + scan_id=scan_id, ), ).apply_async() @@ -323,12 +327,30 @@ def generate_outputs_task(scan_id: str, provider_id: str, tenant_id: str): ScanSummary.objects.filter(scan_id=scan_id) ) - qs = Finding.all_objects.filter(scan_id=scan_id).order_by("uid").iterator() + # Check if we need to generate ASFF output for AWS providers with SecurityHub integration + generate_asff = False + if provider_type == "aws": + security_hub_integrations = Integration.objects.filter( + integrationproviderrelationship__provider_id=provider_id, + integration_type=Integration.IntegrationChoices.AWS_SECURITY_HUB, + enabled=True, + ) + generate_asff = security_hub_integrations.exists() + + qs = ( + Finding.all_objects.filter(tenant_id=tenant_id, scan_id=scan_id) + .order_by("uid") + .iterator() + ) for batch, is_last in batched(qs, DJANGO_FINDINGS_BATCH_SIZE): fos = [FindingOutput.transform_api_finding(f, prowler_provider) for f in batch] # Outputs for mode, cfg in OUTPUT_FORMATS_MAPPING.items(): + # Skip ASFF generation if not needed + if mode == "json-asff" and not generate_asff: + continue + cls = cfg["class"] suffix = cfg["suffix"] extra = cfg.get("kwargs", {}).copy() @@ -474,17 +496,19 @@ def check_lighthouse_connection_task(lighthouse_config_id: str, tenant_id: str = @shared_task(name="integration-check") -def check_integrations_task(tenant_id: str, provider_id: str): +def check_integrations_task(tenant_id: str, provider_id: str, scan_id: str = None): """ Check and execute all configured integrations for a provider. Args: tenant_id (str): The tenant identifier provider_id (str): The provider identifier + scan_id (str, optional): The scan identifier for integrations that need scan data """ logger.info(f"Checking integrations for provider {provider_id}") try: + integration_tasks = [] with rls_transaction(tenant_id): integrations = Integration.objects.filter( integrationproviderrelationship__provider_id=provider_id, @@ -495,7 +519,16 @@ def check_integrations_task(tenant_id: str, provider_id: str): logger.info(f"No integrations configured for provider {provider_id}") return {"integrations_processed": 0} - integration_tasks = [] + # Security Hub integration + security_hub_integrations = integrations.filter( + integration_type=Integration.IntegrationChoices.AWS_SECURITY_HUB + ) + if security_hub_integrations.exists(): + integration_tasks.append( + security_hub_integration_task.s( + tenant_id=tenant_id, provider_id=provider_id, scan_id=scan_id + ) + ) # TODO: Add other integration types here # slack_integrations = integrations.filter( @@ -541,3 +574,24 @@ def s3_integration_task( output_directory (str): Path to the directory containing output files """ return upload_s3_integration(tenant_id, provider_id, output_directory) + + +@shared_task( + base=RLSTask, + name="integration-security-hub", + queue="integrations", +) +def security_hub_integration_task( + tenant_id: str, + provider_id: str, + scan_id: str, +): + """ + Process Security Hub integrations for a provider. + + Args: + tenant_id (str): The tenant identifier + provider_id (str): The provider identifier + scan_id (str): The scan identifier + """ + return upload_security_hub_integration(tenant_id, provider_id, scan_id) diff --git a/api/src/backend/tasks/tests/test_integrations.py b/api/src/backend/tasks/tests/test_integrations.py index 538f892718..4dd68b9f95 100644 --- a/api/src/backend/tasks/tests/test_integrations.py +++ b/api/src/backend/tasks/tests/test_integrations.py @@ -3,11 +3,14 @@ from unittest.mock import MagicMock, patch import pytest from tasks.jobs.integrations import ( get_s3_client_from_integration, + get_security_hub_client_from_integration, upload_s3_integration, + upload_security_hub_integration, ) from api.models import Integration from api.utils import prowler_integration_connection_test +from prowler.providers.aws.lib.security_hub.security_hub import SecurityHubConnection from prowler.providers.common.models import Connection @@ -17,8 +20,8 @@ class TestS3IntegrationUploads: def test_get_s3_client_from_integration_success(self, mock_s3_class): mock_integration = MagicMock() mock_integration.credentials = { - "aws_access_key_id": "AKIA...", - "aws_secret_access_key": "SECRET", + "aws_access_key_id": "test_key_id", + "aws_secret_access_key": "test_secret_key", } mock_integration.configuration = { "bucket_name": "test-bucket", @@ -336,8 +339,8 @@ class TestS3IntegrationUploads: """Test that S3 client uses output_directory correctly when generating object paths.""" mock_integration = MagicMock() mock_integration.credentials = { - "aws_access_key_id": "AKIA...", - "aws_secret_access_key": "SECRET", + "aws_access_key_id": "test_key_id", + "aws_secret_access_key": "test_secret_key", } mock_integration.configuration = { "bucket_name": "test-bucket", @@ -369,8 +372,8 @@ class TestProwlerIntegrationConnectionTest: integration = MagicMock() integration.integration_type = Integration.IntegrationChoices.AMAZON_S3 integration.credentials = { - "aws_access_key_id": "AKIA...", - "aws_secret_access_key": "SECRET", + "aws_access_key_id": "test_key_id", + "aws_secret_access_key": "test_secret_key", } integration.configuration = {"bucket_name": "test-bucket"} @@ -392,8 +395,8 @@ class TestProwlerIntegrationConnectionTest: integration = MagicMock() integration.integration_type = Integration.IntegrationChoices.AMAZON_S3 integration.credentials = { - "aws_access_key_id": "invalid", - "aws_secret_access_key": "credentials", + "aws_access_key_id": "invalid_key", + "aws_secret_access_key": "invalid_secret", } integration.configuration = {"bucket_name": "test-bucket"} @@ -406,8 +409,8 @@ class TestProwlerIntegrationConnectionTest: assert result.is_connected is False assert result.error == test_exception mock_s3_class.test_connection.assert_called_once_with( - aws_access_key_id="invalid", - aws_secret_access_key="credentials", + aws_access_key_id="invalid_key", + aws_secret_access_key="invalid_secret", bucket_name="test-bucket", raise_on_exception=False, ) @@ -419,8 +422,8 @@ class TestProwlerIntegrationConnectionTest: integration = MagicMock() integration.integration_type = Integration.IntegrationChoices.AMAZON_S3 integration.credentials = { - "aws_access_key_id": "AKIA...", - "aws_secret_access_key": "SECRET", + "aws_access_key_id": "test_key_id", + "aws_secret_access_key": "test_secret_key", } integration.configuration = {"bucket_name": "test-bucket"} @@ -437,30 +440,140 @@ class TestProwlerIntegrationConnectionTest: assert result.is_connected is False assert str(result.error) == "Bucket not found" - @patch("api.utils.AwsProvider") - @patch("api.utils.S3") - def test_aws_security_hub_integration_connection( - self, mock_s3_class, mock_aws_provider + @patch("api.utils.SecurityHub") + def test_aws_security_hub_integration_connection_success( + self, mock_security_hub_class ): - """Test AWS Security Hub integration only validates AWS session.""" + """Test successful AWS Security Hub integration connection.""" integration = MagicMock() integration.integration_type = Integration.IntegrationChoices.AWS_SECURITY_HUB integration.credentials = { - "aws_access_key_id": "AKIA...", - "aws_secret_access_key": "SECRET", + "aws_access_key_id": "test_key_id", + "aws_secret_access_key": "test_secret_key", } - integration.configuration = {"region": "us-east-1"} + integration.configuration = {"send_only_fails": True} - mock_session = MagicMock() - mock_aws_provider.return_value.session.current_session = mock_session + # Mock integration provider relationship + mock_provider = MagicMock() + mock_provider.uid = "123456789012" + mock_relationship = MagicMock() + mock_relationship.provider = mock_provider + integration.integrationproviderrelationship_set.first.return_value = ( + mock_relationship + ) + + # Mock successful SecurityHub connection with regions + mock_connection = SecurityHubConnection( + is_connected=True, + error=None, + enabled_regions={"us-east-1", "us-west-2", "eu-west-1"}, + disabled_regions={"us-east-2", "eu-west-2"}, + ) + mock_security_hub_class.test_connection.return_value = mock_connection - # For AWS Security Hub, the function should return early after AWS session validation result = prowler_integration_connection_test(integration) - # The function should not reach S3 test_connection for AWS_SECURITY_HUB - mock_s3_class.test_connection.assert_not_called() - # Since no exception was raised during AWS session creation, return None (success) - assert result is None + assert result.is_connected is True + mock_security_hub_class.test_connection.assert_called_once_with( + aws_account_id="123456789012", + raise_on_exception=False, + aws_access_key_id="test_key_id", + aws_secret_access_key="test_secret_key", + ) + # Verify regions were saved + assert integration.configuration["regions"]["us-east-1"] is True + assert integration.configuration["regions"]["us-west-2"] is True + assert integration.configuration["regions"]["eu-west-1"] is True + assert integration.configuration["regions"]["us-east-2"] is False + assert integration.configuration["regions"]["eu-west-2"] is False + integration.save.assert_called_once() + + @patch("api.utils.SecurityHub") + def test_aws_security_hub_integration_connection_failure( + self, mock_security_hub_class + ): + """Test AWS Security Hub integration connection failure.""" + integration = MagicMock() + integration.integration_type = Integration.IntegrationChoices.AWS_SECURITY_HUB + integration.credentials = { + "aws_access_key_id": "invalid_key", + "aws_secret_access_key": "invalid_secret", + } + integration.configuration = {"send_only_fails": False} + + # Mock integration provider relationship + mock_provider = MagicMock() + mock_provider.uid = "123456789012" + mock_relationship = MagicMock() + mock_relationship.provider = mock_provider + integration.integrationproviderrelationship_set.first.return_value = ( + mock_relationship + ) + + # Mock failed SecurityHub connection + test_exception = Exception("SecurityHub not enabled") + mock_connection = SecurityHubConnection( + is_connected=False, + error=test_exception, + enabled_regions=set(), + disabled_regions=set(), + ) + mock_security_hub_class.test_connection.return_value = mock_connection + + result = prowler_integration_connection_test(integration) + + assert result.is_connected is False + assert result.error == test_exception + # Verify regions were not saved when connection failed + integration.save.assert_not_called() + + @patch("api.utils.SecurityHub") + def test_aws_security_hub_integration_with_provider_credentials( + self, mock_security_hub_class + ): + """Test AWS Security Hub integration using provider credentials.""" + integration = MagicMock() + integration.integration_type = Integration.IntegrationChoices.AWS_SECURITY_HUB + integration.credentials = None # No custom credentials + integration.configuration = {"send_only_fails": True} + + # Mock integration provider relationship + mock_provider = MagicMock() + mock_provider.uid = "123456789012" + mock_provider.secret.secret = { + "aws_access_key_id": "test_key_id", + "aws_secret_access_key": "test_secret_key", + } + mock_relationship = MagicMock() + mock_relationship.provider = mock_provider + integration.integrationproviderrelationship_set.first.return_value = ( + mock_relationship + ) + + # Mock successful SecurityHub connection with regions + mock_connection = SecurityHubConnection( + is_connected=True, + error=None, + enabled_regions={"us-east-1", "eu-central-1"}, + disabled_regions={"ap-south-1"}, + ) + mock_security_hub_class.test_connection.return_value = mock_connection + + result = prowler_integration_connection_test(integration) + + assert result.is_connected + # Should use provider credentials + mock_security_hub_class.test_connection.assert_called_once_with( + aws_account_id="123456789012", + raise_on_exception=False, + aws_access_key_id="test_key_id", + aws_secret_access_key="test_secret_key", + ) + # Verify regions were saved + assert integration.configuration["regions"]["us-east-1"] + assert integration.configuration["regions"]["eu-central-1"] + assert not integration.configuration["regions"]["ap-south-1"] + integration.save.assert_called_once() def test_unsupported_integration_type(self): """Test unsupported integration type raises ValueError.""" @@ -473,3 +586,810 @@ class TestProwlerIntegrationConnectionTest: ValueError, match="Integration type UNSUPPORTED_TYPE not supported" ): prowler_integration_connection_test(integration) + + +@pytest.mark.django_db +class TestSecurityHubIntegrationUploads: + @patch("tasks.jobs.integrations.AwsProvider") + @patch("tasks.jobs.integrations.SecurityHub.test_connection") + @patch("tasks.jobs.integrations.initialize_prowler_provider") + def test_get_security_hub_client_from_integration_success( + self, mock_initialize_provider, mock_test_connection, mock_aws_provider + ): + """Test successful SecurityHub client creation.""" + # Mock integration + mock_integration = MagicMock() + mock_integration.configuration = {"send_only_fails": True} + mock_integration.credentials = {} # Empty credentials, use provider + + # Mock tenant_id + tenant_id = "550e8400-e29b-41d4-a716-446655440000" # Valid UUID + + # Mock provider relationship + mock_provider = MagicMock() + mock_provider.uid = "123456789012" + mock_provider.secret.secret = { + "aws_access_key_id": "test_key_id", + "aws_secret_access_key": "test_secret_key", + } + mock_relationship = MagicMock() + mock_relationship.provider = mock_provider + mock_integration.integrationproviderrelationship_set.first.return_value = ( + mock_relationship + ) + + # Mock prowler provider + mock_prowler_provider = MagicMock() + mock_prowler_provider.identity.account = "123456789012" + mock_prowler_provider.identity.partition = "aws" + mock_prowler_provider.identity.audited_regions = ["us-east-1", "us-west-2"] + mock_prowler_provider.session.current_session = MagicMock() + mock_prowler_provider.get_available_aws_service_regions.return_value = [ + "us-east-1", + "us-west-2", + ] + mock_initialize_provider.return_value = mock_prowler_provider + + # Mock successful connection with SecurityHub-specific attributes + mock_connection = MagicMock() + mock_connection.is_connected = True + mock_connection.partition = "aws" + mock_connection.enabled_regions = {"us-east-1": True, "us-west-2": True} + mock_test_connection.return_value = mock_connection + + # Mock AwsProvider.get_available_aws_service_regions + mock_aws_provider.get_available_aws_service_regions.return_value = [ + "us-east-1", + "us-west-2", + ] + + # Mock findings + mock_findings = [{"finding": "test"}] + + with patch("tasks.jobs.integrations.SecurityHub") as mock_security_hub_class: + mock_security_hub = MagicMock() + mock_security_hub._enabled_regions = {"us-east-1": True, "us-west-2": True} + mock_security_hub_class.return_value = mock_security_hub + # Configure the test_connection to return our mock_connection + mock_security_hub_class.test_connection = mock_test_connection + + connected, security_hub = get_security_hub_client_from_integration( + mock_integration, tenant_id, mock_findings + ) + + assert connected is True + assert security_hub == mock_security_hub + + # Verify SecurityHub was called once to create the client + assert mock_security_hub_class.call_count == 1 + + # Verify the call has the correct parameters + actual_call = mock_security_hub_class.call_args_list[0] + assert actual_call.kwargs["aws_account_id"] == "123456789012" + assert actual_call.kwargs["findings"] == mock_findings + assert actual_call.kwargs["send_only_fails"] + # Check that available_regions list was passed correctly + assert actual_call.kwargs["aws_security_hub_available_regions"] == [ + "us-east-1", + "us-west-2", + ] + + @patch("tasks.jobs.integrations.SecurityHub.test_connection") + @patch("tasks.jobs.integrations.initialize_prowler_provider") + def test_get_security_hub_client_from_integration_failure( + self, mock_initialize_provider, mock_test_connection + ): + """Test SecurityHub client creation failure.""" + # Mock integration + mock_integration = MagicMock() + mock_integration.configuration = {"send_only_fails": False} + mock_integration.credentials = {} # Empty credentials, use provider + + # Mock tenant_id + tenant_id = "550e8400-e29b-41d4-a716-446655440000" # Valid UUID + + # Mock provider relationship + mock_provider = MagicMock() + mock_provider.uid = "123456789012" + mock_provider.secret.secret = { + "aws_access_key_id": "test_key_id", + "aws_secret_access_key": "test_secret_key", + } + mock_relationship = MagicMock() + mock_relationship.provider = mock_provider + mock_integration.integrationproviderrelationship_set.first.return_value = ( + mock_relationship + ) + + # Mock failed connection + mock_connection = MagicMock() + mock_connection.is_connected = False + mock_connection.error = "Connection failed" + mock_test_connection.return_value = mock_connection + + # Mock findings + mock_findings = [{"finding": "test"}] + + connected, connection = get_security_hub_client_from_integration( + mock_integration, tenant_id, mock_findings + ) + + assert connected is False + assert connection == mock_connection + + # Verify test_connection was called with correct parameters + mock_test_connection.assert_called_once_with( + aws_account_id="123456789012", + raise_on_exception=False, + aws_access_key_id="test_key_id", + aws_secret_access_key="test_secret_key", + ) + + @patch("tasks.jobs.integrations.AwsProvider") + @patch("tasks.jobs.integrations.SecurityHub.test_connection") + @patch("tasks.jobs.integrations.initialize_prowler_provider") + def test_get_security_hub_client_from_integration_no_audited_regions( + self, mock_initialize_provider, mock_test_connection, mock_aws_provider + ): + """Test SecurityHub client creation when no audited regions are specified.""" + # Mock integration + mock_integration = MagicMock() + mock_integration.configuration = {"send_only_fails": False} + mock_integration.credentials = {} # Empty credentials, use provider + + # Mock tenant_id + tenant_id = "550e8400-e29b-41d4-a716-446655440000" # Valid UUID + + # Mock provider relationship + mock_provider = MagicMock() + mock_provider.uid = "123456789012" + mock_provider.secret.secret = { + "aws_access_key_id": "test_key_id", + "aws_secret_access_key": "test_secret_key", + } + mock_relationship = MagicMock() + mock_relationship.provider = mock_provider + mock_integration.integrationproviderrelationship_set.first.return_value = ( + mock_relationship + ) + + # Mock prowler provider with no audited regions + mock_prowler_provider = MagicMock() + mock_prowler_provider.identity.account = "123456789012" + mock_prowler_provider.identity.partition = "aws" + mock_prowler_provider.identity.audited_regions = None + mock_prowler_provider.session.current_session = MagicMock() + mock_prowler_provider.get_available_aws_service_regions.return_value = [ + "us-east-1", + "us-west-2", + "eu-west-1", + ] + mock_initialize_provider.return_value = mock_prowler_provider + + # Mock successful connection with SecurityHub-specific attributes + mock_connection = MagicMock() + mock_connection.is_connected = True + mock_connection.partition = "aws" + mock_connection.enabled_regions = {"us-east-1": True, "us-west-2": True} + mock_test_connection.return_value = mock_connection + + # Mock AwsProvider.get_available_aws_service_regions + mock_aws_provider.get_available_aws_service_regions.return_value = [ + "us-east-1", + "us-west-2", + "eu-west-1", + ] + + # Mock findings + mock_findings = [{"finding": "test"}] + + with patch("tasks.jobs.integrations.SecurityHub") as mock_security_hub_class: + mock_security_hub = MagicMock() + mock_security_hub._enabled_regions = {"us-east-1": True, "us-west-2": True} + mock_security_hub_class.return_value = mock_security_hub + # Configure the test_connection to return our mock_connection + mock_security_hub_class.test_connection = mock_test_connection + + connected, security_hub = get_security_hub_client_from_integration( + mock_integration, tenant_id, mock_findings + ) + + assert connected is True + + # Verify SecurityHub was called once to create the client + assert mock_security_hub_class.call_count == 1 + + # Verify the call has the correct parameters + actual_call = mock_security_hub_class.call_args_list[0] + assert actual_call.kwargs["aws_account_id"] == "123456789012" + assert actual_call.kwargs["findings"] == mock_findings + assert not actual_call.kwargs["send_only_fails"] + # Check that available_regions list was passed correctly + assert actual_call.kwargs["aws_security_hub_available_regions"] == [ + "us-east-1", + "us-west-2", + ] + + @patch("tasks.jobs.integrations.ASFF") + @patch("tasks.jobs.integrations.FindingOutput") + @patch("tasks.jobs.integrations.batched") + @patch("tasks.jobs.integrations.get_security_hub_client_from_integration") + @patch("tasks.jobs.integrations.initialize_prowler_provider") + @patch("tasks.jobs.integrations.rls_transaction") + @patch("tasks.jobs.integrations.Integration") + @patch("tasks.jobs.integrations.Provider") + @patch("tasks.jobs.integrations.Finding") + def test_upload_security_hub_integration_success( + self, + mock_finding_model, + mock_provider_model, + mock_integration_model, + mock_rls, + mock_initialize_provider, + mock_get_security_hub, + mock_batched, + mock_finding_output, + mock_asff, + ): + """Test successful SecurityHub integration upload.""" + tenant_id = "tenant-id" + provider_id = "provider-id" + scan_id = "scan-123" + + # Mock integration + integration = MagicMock() + integration.id = "integration-1" + integration.configuration = { + "send_only_fails": True, + "archive_previous_findings": True, + } + mock_integration_model.objects.filter.return_value = [integration] + + # Mock provider + provider = MagicMock() + mock_provider_model.objects.get.return_value = provider + + # Mock prowler provider + mock_prowler_provider = MagicMock() + mock_initialize_provider.return_value = mock_prowler_provider + + # Mock findings + mock_findings = [MagicMock(), MagicMock()] + mock_finding_model.all_objects.filter.return_value.order_by.return_value.iterator.return_value = iter( + mock_findings + ) + + # Mock batched to return findings in one batch + mock_batched.return_value = [(mock_findings, None)] + + # Mock transformed findings + transformed_findings = [MagicMock(), MagicMock()] + mock_finding_output.transform_api_finding.side_effect = transformed_findings + + # Mock ASFF transformer + mock_asff_instance = MagicMock() + finding1 = MagicMock() + finding1.Compliance.Status = "FAILED" + finding2 = MagicMock() + finding2.Compliance.Status = "FAILED" + mock_asff_instance.data = [finding1, finding2] + mock_asff_instance._data = MagicMock() + mock_asff.return_value = mock_asff_instance + + # Mock SecurityHub client + mock_security_hub = MagicMock() + mock_security_hub.batch_send_to_security_hub.return_value = 2 + mock_security_hub.archive_previous_findings.return_value = 5 + mock_get_security_hub.return_value = (True, mock_security_hub) + + result = upload_security_hub_integration(tenant_id, provider_id, scan_id) + + assert result is True + + # Verify findings were transformed and sent + assert mock_finding_output.transform_api_finding.call_count == 2 + mock_asff.assert_called_once() + mock_asff_instance.transform.assert_called_once_with(transformed_findings) + mock_security_hub.batch_send_to_security_hub.assert_called_once() + mock_security_hub.archive_previous_findings.assert_called_once() + + @patch("tasks.jobs.integrations.get_security_hub_client_from_integration") + @patch("tasks.jobs.integrations.initialize_prowler_provider") + @patch("tasks.jobs.integrations.rls_transaction") + @patch("tasks.jobs.integrations.Integration") + @patch("tasks.jobs.integrations.Provider") + @patch("tasks.jobs.integrations.Finding") + def test_upload_security_hub_integration_no_integrations( + self, + mock_finding_model, + mock_provider_model, + mock_integration_model, + mock_rls, + mock_initialize_provider, + mock_get_security_hub, + ): + """Test SecurityHub upload when no integrations are found.""" + tenant_id = "tenant-id" + provider_id = "provider-id" + scan_id = "scan-123" + + # Mock no integrations found + mock_integration_model.objects.filter.return_value = [] + + result = upload_security_hub_integration(tenant_id, provider_id, scan_id) + + assert result is False + + @patch("tasks.jobs.integrations.batched") + @patch("tasks.jobs.integrations.get_security_hub_client_from_integration") + @patch("tasks.jobs.integrations.initialize_prowler_provider") + @patch("tasks.jobs.integrations.rls_transaction") + @patch("tasks.jobs.integrations.Integration") + @patch("tasks.jobs.integrations.Provider") + @patch("tasks.jobs.integrations.Finding") + def test_upload_security_hub_integration_no_findings( + self, + mock_finding_model, + mock_provider_model, + mock_integration_model, + mock_rls, + mock_initialize_provider, + mock_get_security_hub, + mock_batched, + ): + """Test SecurityHub upload when no findings are found.""" + tenant_id = "tenant-id" + provider_id = "provider-id" + scan_id = "scan-123" + + # Mock integration + integration = MagicMock() + integration.id = "integration-1" + mock_integration_model.objects.filter.return_value = [integration] + + # Mock provider + provider = MagicMock() + mock_provider_model.objects.get.return_value = provider + + # Mock prowler provider + mock_prowler_provider = MagicMock() + mock_initialize_provider.return_value = mock_prowler_provider + + # Mock no findings + mock_finding_model.all_objects.filter.return_value.order_by.return_value.iterator.return_value = iter( + [] + ) + mock_batched.return_value = [] + + result = upload_security_hub_integration(tenant_id, provider_id, scan_id) + + assert result is True # No findings is considered success + + @patch("tasks.jobs.integrations.get_security_hub_client_from_integration") + @patch("tasks.jobs.integrations.initialize_prowler_provider") + @patch("tasks.jobs.integrations.rls_transaction") + @patch("tasks.jobs.integrations.Integration") + @patch("tasks.jobs.integrations.Provider") + @patch("tasks.jobs.integrations.Finding") + def test_upload_security_hub_integration_connection_failure( + self, + mock_finding_model, + mock_provider_model, + mock_integration_model, + mock_rls, + mock_initialize_provider, + mock_get_security_hub, + ): + """Test SecurityHub upload when connection fails.""" + tenant_id = "tenant-id" + provider_id = "provider-id" + scan_id = "scan-123" + + # Mock integration + integration = MagicMock() + integration.id = "integration-1" + integration.connected = True + mock_integration_model.objects.filter.return_value = [integration] + + # Mock provider + provider = MagicMock() + mock_provider_model.objects.get.return_value = provider + + # Mock prowler provider + mock_prowler_provider = MagicMock() + mock_initialize_provider.return_value = mock_prowler_provider + + # Mock findings exist + mock_findings = [MagicMock()] + mock_finding_model.all_objects.filter.return_value.order_by.return_value.iterator.return_value = iter( + mock_findings + ) + + # Mock failed connection + mock_connection = MagicMock() + mock_connection.error = "Connection failed" + mock_get_security_hub.return_value = (False, mock_connection) + + with patch("tasks.jobs.integrations.batched") as mock_batched: + with patch("tasks.jobs.integrations.FindingOutput") as mock_finding_output: + with patch("tasks.jobs.integrations.ASFF") as mock_asff: + # Mock batched and transformation + mock_batched.return_value = [(mock_findings, None)] + transformed_findings = [MagicMock()] + mock_finding_output.transform_api_finding.return_value = ( + transformed_findings[0] + ) + + mock_asff_instance = MagicMock() + finding1 = MagicMock() + finding1.Compliance.Status = "FAILED" + mock_asff_instance.data = [finding1] + mock_asff_instance._data = MagicMock() + mock_asff.return_value = mock_asff_instance + + result = upload_security_hub_integration( + tenant_id, provider_id, scan_id + ) + + assert result is False + # Integration should be marked as disconnected + integration.save.assert_called_once() + assert integration.connected is False + + @patch("tasks.jobs.integrations.ASFF") + @patch("tasks.jobs.integrations.FindingOutput") + @patch("tasks.jobs.integrations.batched") + @patch("tasks.jobs.integrations.get_security_hub_client_from_integration") + @patch("tasks.jobs.integrations.initialize_prowler_provider") + @patch("tasks.jobs.integrations.rls_transaction") + @patch("tasks.jobs.integrations.Integration") + @patch("tasks.jobs.integrations.Provider") + @patch("tasks.jobs.integrations.Finding") + def test_upload_security_hub_integration_skip_archive( + self, + mock_finding_model, + mock_provider_model, + mock_integration_model, + mock_rls, + mock_initialize_provider, + mock_get_security_hub, + mock_batched, + mock_finding_output, + mock_asff, + ): + """Test SecurityHub upload with archive_previous_findings disabled.""" + tenant_id = "tenant-id" + provider_id = "provider-id" + scan_id = "scan-123" + + # Mock integration with archive_previous_findings disabled + integration = MagicMock() + integration.id = "integration-1" + integration.configuration = { + "send_only_fails": False, + "archive_previous_findings": False, + } + mock_integration_model.objects.filter.return_value = [integration] + + # Mock provider + provider = MagicMock() + mock_provider_model.objects.get.return_value = provider + + # Mock prowler provider + mock_prowler_provider = MagicMock() + mock_initialize_provider.return_value = mock_prowler_provider + + # Mock findings + mock_findings = [MagicMock()] + mock_finding_model.all_objects.filter.return_value.order_by.return_value.iterator.return_value = iter( + mock_findings + ) + + # Mock batched and transformation + mock_batched.return_value = [(mock_findings, None)] + transformed_findings = [MagicMock()] + mock_finding_output.transform_api_finding.return_value = transformed_findings[0] + + # Mock ASFF transformer + mock_asff_instance = MagicMock() + finding1 = MagicMock() + finding1.Compliance.Status = "FAILED" + mock_asff_instance.data = [finding1] + mock_asff_instance._data = MagicMock() + mock_asff.return_value = mock_asff_instance + + # Mock SecurityHub client + mock_security_hub = MagicMock() + mock_security_hub.batch_send_to_security_hub.return_value = 1 + mock_get_security_hub.return_value = (True, mock_security_hub) + + result = upload_security_hub_integration(tenant_id, provider_id, scan_id) + + assert result is True + + # Verify archiving was skipped + mock_security_hub.archive_previous_findings.assert_not_called() + mock_security_hub.batch_send_to_security_hub.assert_called_once() + + @patch("tasks.jobs.integrations.ASFF") + @patch("tasks.jobs.integrations.FindingOutput") + @patch("tasks.jobs.integrations.batched") + @patch("tasks.jobs.integrations.get_security_hub_client_from_integration") + @patch("tasks.jobs.integrations.initialize_prowler_provider") + @patch("tasks.jobs.integrations.rls_transaction") + @patch("tasks.jobs.integrations.Integration") + @patch("tasks.jobs.integrations.Provider") + @patch("tasks.jobs.integrations.Finding") + def test_upload_security_hub_integration_archive_failure( + self, + mock_finding_model, + mock_provider_model, + mock_integration_model, + mock_rls, + mock_initialize_provider, + mock_get_security_hub, + mock_batched, + mock_finding_output, + mock_asff, + ): + """Test SecurityHub upload when archiving fails but sending succeeds.""" + tenant_id = "tenant-id" + provider_id = "provider-id" + scan_id = "scan-123" + + # Mock integration + integration = MagicMock() + integration.id = "integration-1" + integration.configuration = { + "send_only_fails": False, + "archive_previous_findings": True, + } + mock_integration_model.objects.filter.return_value = [integration] + + # Mock provider + provider = MagicMock() + mock_provider_model.objects.get.return_value = provider + + # Mock prowler provider + mock_prowler_provider = MagicMock() + mock_initialize_provider.return_value = mock_prowler_provider + + # Mock findings + mock_findings = [MagicMock()] + mock_finding_model.all_objects.filter.return_value.order_by.return_value.iterator.return_value = iter( + mock_findings + ) + + # Mock batched and transformation + mock_batched.return_value = [(mock_findings, None)] + transformed_findings = [MagicMock()] + mock_finding_output.transform_api_finding.return_value = transformed_findings[0] + + # Mock ASFF transformer + mock_asff_instance = MagicMock() + finding1 = MagicMock() + finding1.Compliance.Status = "FAILED" + mock_asff_instance.data = [finding1] + mock_asff_instance._data = MagicMock() + mock_asff.return_value = mock_asff_instance + + # Mock SecurityHub client - sending succeeds, archiving fails + mock_security_hub = MagicMock() + mock_security_hub.batch_send_to_security_hub.return_value = 1 + mock_security_hub.archive_previous_findings.side_effect = Exception( + "Archive failed" + ) + mock_get_security_hub.return_value = (True, mock_security_hub) + + result = upload_security_hub_integration(tenant_id, provider_id, scan_id) + + assert result is True # Should still succeed even if archiving fails + + # Verify both methods were called + mock_security_hub.batch_send_to_security_hub.assert_called_once() + mock_security_hub.archive_previous_findings.assert_called_once() + + @patch("tasks.jobs.integrations.rls_transaction") + @patch("tasks.jobs.integrations.Integration") + @patch("tasks.jobs.integrations.Provider") + @patch("tasks.jobs.integrations.Finding") + def test_upload_security_hub_integration_general_exception( + self, + mock_finding_model, + mock_provider_model, + mock_integration_model, + mock_rls, + ): + """Test SecurityHub upload handles general exceptions.""" + tenant_id = "tenant-id" + provider_id = "provider-id" + scan_id = "scan-123" + + # Mock exception during integration retrieval + mock_integration_model.objects.filter.side_effect = Exception("Database error") + + result = upload_security_hub_integration(tenant_id, provider_id, scan_id) + + assert result is False + + @patch("tasks.jobs.integrations.ASFF") + @patch("tasks.jobs.integrations.FindingOutput") + @patch("tasks.jobs.integrations.batched") + @patch("tasks.jobs.integrations.get_security_hub_client_from_integration") + @patch("tasks.jobs.integrations.initialize_prowler_provider") + @patch("tasks.jobs.integrations.rls_transaction") + @patch("tasks.jobs.integrations.Integration") + @patch("tasks.jobs.integrations.Provider") + @patch("tasks.jobs.integrations.Finding") + def test_upload_security_hub_integration_send_only_fails_filters_findings( + self, + mock_finding_model, + mock_provider_model, + mock_integration_model, + mock_rls, + mock_initialize_provider, + mock_get_security_hub, + mock_batched, + mock_finding_output, + mock_asff, + ): + """Test that send_only_fails=True filters findings to only include FAILED status.""" + tenant_id = "tenant-id" + provider_id = "provider-id" + scan_id = "scan-123" + + # Mock integration with send_only_fails=True + integration = MagicMock() + integration.id = "integration-1" + integration.configuration = { + "send_only_fails": True, + "archive_previous_findings": True, + } + mock_integration_model.objects.filter.return_value = [integration] + + # Mock provider + provider = MagicMock() + mock_provider_model.objects.get.return_value = provider + + # Mock prowler provider + mock_prowler_provider = MagicMock() + mock_initialize_provider.return_value = mock_prowler_provider + + # Mock findings + mock_findings = [MagicMock(), MagicMock()] + mock_finding_model.all_objects.filter.return_value.order_by.return_value.iterator.return_value = iter( + mock_findings + ) + + # Mock batched to return findings in one batch + mock_batched.return_value = [(mock_findings, None)] + + # Mock transformed findings + transformed_findings = [MagicMock(), MagicMock()] + mock_finding_output.transform_api_finding.side_effect = transformed_findings + + # Mock ASFF transformer with mixed findings (FAILED and PASSED) + mock_asff_instance = MagicMock() + failed_finding = MagicMock() + failed_finding.Compliance.Status = "FAILED" + passed_finding = MagicMock() + passed_finding.Compliance.Status = "PASSED" + mock_asff_instance.data = [failed_finding, passed_finding] + mock_asff_instance._data = MagicMock() + mock_asff.return_value = mock_asff_instance + + # Mock SecurityHub client + mock_security_hub = MagicMock() + mock_security_hub.batch_send_to_security_hub.return_value = ( + 1 # Only 1 finding sent (FAILED) + ) + mock_security_hub.archive_previous_findings.return_value = 2 + mock_get_security_hub.return_value = (True, mock_security_hub) + + result = upload_security_hub_integration(tenant_id, provider_id, scan_id) + + assert result is True + + # Verify SecurityHub client was created with ALL findings (both FAILED and PASSED) + # The SecurityHub client internally filters based on send_only_fails configuration + mock_get_security_hub.assert_called_once() + call_args = mock_get_security_hub.call_args[0] + all_findings = call_args[2] # Third argument is the findings list + + # Should contain both FAILED and PASSED findings + assert len(all_findings) == 2 + assert any(f.Compliance.Status == "FAILED" for f in all_findings) + assert any(f.Compliance.Status == "PASSED" for f in all_findings) + + # The SecurityHub client should have been configured with send_only_fails=True + # and will filter internally when sending + mock_security_hub.batch_send_to_security_hub.assert_called_once() + mock_security_hub.archive_previous_findings.assert_called_once() + + @patch("tasks.jobs.integrations.ASFF") + @patch("tasks.jobs.integrations.FindingOutput") + @patch("tasks.jobs.integrations.batched") + @patch("tasks.jobs.integrations.get_security_hub_client_from_integration") + @patch("tasks.jobs.integrations.initialize_prowler_provider") + @patch("tasks.jobs.integrations.rls_transaction") + @patch("tasks.jobs.integrations.Integration") + @patch("tasks.jobs.integrations.Provider") + @patch("tasks.jobs.integrations.Finding") + def test_upload_security_hub_integration_send_only_fails_false_sends_all( + self, + mock_finding_model, + mock_provider_model, + mock_integration_model, + mock_rls, + mock_initialize_provider, + mock_get_security_hub, + mock_batched, + mock_finding_output, + mock_asff, + ): + """Test that send_only_fails=False sends all findings.""" + tenant_id = "tenant-id" + provider_id = "provider-id" + scan_id = "scan-123" + + # Mock integration with send_only_fails=False + integration = MagicMock() + integration.id = "integration-1" + integration.configuration = { + "send_only_fails": False, + "archive_previous_findings": True, + } + mock_integration_model.objects.filter.return_value = [integration] + + # Mock provider + provider = MagicMock() + mock_provider_model.objects.get.return_value = provider + + # Mock prowler provider + mock_prowler_provider = MagicMock() + mock_initialize_provider.return_value = mock_prowler_provider + + # Mock findings + mock_findings = [MagicMock(), MagicMock()] + mock_finding_model.all_objects.filter.return_value.order_by.return_value.iterator.return_value = iter( + mock_findings + ) + + # Mock batched to return findings in one batch + mock_batched.return_value = [(mock_findings, None)] + + # Mock transformed findings + transformed_findings = [MagicMock(), MagicMock()] + mock_finding_output.transform_api_finding.side_effect = transformed_findings + + # Mock ASFF transformer with mixed findings (FAILED and PASSED) + mock_asff_instance = MagicMock() + mock_asff_instance.data = [ + {"Compliance": {"Status": "FAILED"}, "asff": "failed_finding"}, + {"Compliance": {"Status": "PASSED"}, "asff": "passed_finding"}, + ] + mock_asff_instance._data = MagicMock() + mock_asff.return_value = mock_asff_instance + + # Mock SecurityHub client + mock_security_hub = MagicMock() + mock_security_hub.batch_send_to_security_hub.return_value = ( + 2 # Both findings sent + ) + mock_security_hub.archive_previous_findings.return_value = 2 + mock_get_security_hub.return_value = (True, mock_security_hub) + + result = upload_security_hub_integration(tenant_id, provider_id, scan_id) + + assert result is True + + # Verify SecurityHub client was created with all findings + mock_get_security_hub.assert_called_once() + call_args = mock_get_security_hub.call_args[0] + filtered_findings = call_args[2] # Third argument is the findings list + + # Should contain all findings + assert len(filtered_findings) == 2 + + mock_security_hub.batch_send_to_security_hub.assert_called_once() + mock_security_hub.archive_previous_findings.assert_called_once() diff --git a/api/src/backend/tasks/tests/test_tasks.py b/api/src/backend/tasks/tests/test_tasks.py index 37dbc5c83a..b4262027f6 100644 --- a/api/src/backend/tasks/tests/test_tasks.py +++ b/api/src/backend/tasks/tests/test_tasks.py @@ -7,6 +7,7 @@ from tasks.tasks import ( check_integrations_task, generate_outputs_task, s3_integration_task, + security_hub_integration_task, ) from api.models import Integration @@ -521,31 +522,68 @@ class TestCheckIntegrationsTask: enabled=True, ) + @patch("tasks.tasks.security_hub_integration_task") @patch("tasks.tasks.group") @patch("tasks.tasks.rls_transaction") @patch("tasks.tasks.Integration.objects.filter") - def test_check_integrations_s3_success( - self, mock_integration_filter, mock_rls, mock_group + def test_check_integrations_security_hub_success( + self, mock_integration_filter, mock_rls, mock_group, mock_security_hub_task ): - # Mock that we have some integrations - mock_integration_filter.return_value.exists.return_value = True + """Test that SecurityHub integrations are processed correctly.""" + # Mock that we have SecurityHub integrations + mock_integrations = MagicMock() + mock_integrations.exists.return_value = True + + # Mock SecurityHub integrations to return existing integrations + mock_security_hub_integrations = MagicMock() + mock_security_hub_integrations.exists.return_value = True + + # Set up the filter chain + mock_integration_filter.return_value = mock_integrations + mock_integrations.filter.return_value = mock_security_hub_integrations + + # Mock the task signature + mock_task_signature = MagicMock() + mock_security_hub_task.s.return_value = mock_task_signature + + # Mock group job + mock_job = MagicMock() + mock_group.return_value = mock_job + # Ensure rls_transaction is mocked mock_rls.return_value.__enter__.return_value = None - # Since the current implementation doesn't actually create tasks yet (TODO comment), - # we test that no tasks are created but the function returns the correct count + # Execute the function result = check_integrations_task( tenant_id=self.tenant_id, provider_id=self.provider_id, + scan_id="test-scan-id", ) - assert result == {"integrations_processed": 0} + # Should process 1 SecurityHub integration + assert result == {"integrations_processed": 1} + + # Verify the integration filter was called mock_integration_filter.assert_called_once_with( integrationproviderrelationship__provider_id=self.provider_id, enabled=True, ) - # group should not be called since no integration tasks are created yet - mock_group.assert_not_called() + + # Verify SecurityHub integrations were filtered + mock_integrations.filter.assert_called_once_with( + integration_type=Integration.IntegrationChoices.AWS_SECURITY_HUB + ) + + # Verify SecurityHub task was created with correct parameters + mock_security_hub_task.s.assert_called_once_with( + tenant_id=self.tenant_id, + provider_id=self.provider_id, + scan_id="test-scan-id", + ) + + # Verify group was called and job was executed + mock_group.assert_called_once_with([mock_task_signature]) + mock_job.apply_async.assert_called_once() @patch("tasks.tasks.rls_transaction") @patch("tasks.tasks.Integration.objects.filter") @@ -567,6 +605,369 @@ class TestCheckIntegrationsTask: enabled=True, ) + @patch("tasks.tasks.s3_integration_task") + @patch("tasks.tasks.Integration.objects.filter") + @patch("tasks.tasks.ScanSummary.objects.filter") + @patch("tasks.tasks.Provider.objects.get") + @patch("tasks.tasks.initialize_prowler_provider") + @patch("tasks.tasks.Compliance.get_bulk") + @patch("tasks.tasks.get_compliance_frameworks") + @patch("tasks.tasks.Finding.all_objects.filter") + @patch("tasks.tasks._generate_output_directory") + @patch("tasks.tasks.FindingOutput._transform_findings_stats") + @patch("tasks.tasks.FindingOutput.transform_api_finding") + @patch("tasks.tasks._compress_output_files") + @patch("tasks.tasks._upload_to_s3") + @patch("tasks.tasks.Scan.all_objects.filter") + @patch("tasks.tasks.rmtree") + def test_generate_outputs_with_asff_for_aws_with_security_hub( + self, + mock_rmtree, + mock_scan_update, + mock_upload, + mock_compress, + mock_transform_finding, + mock_transform_stats, + mock_generate_dir, + mock_findings, + mock_get_frameworks, + mock_compliance_bulk, + mock_initialize_provider, + mock_provider_get, + mock_scan_summary, + mock_integration_filter, + mock_s3_task, + ): + """Test that ASFF output is generated for AWS providers with SecurityHub integration.""" + # Setup + mock_scan_summary_qs = MagicMock() + mock_scan_summary_qs.exists.return_value = True + mock_scan_summary.return_value = mock_scan_summary_qs + + # Mock AWS provider + mock_provider = MagicMock() + mock_provider.uid = "aws-account-123" + mock_provider.provider = "aws" + mock_provider_get.return_value = mock_provider + + # Mock SecurityHub integration exists + mock_security_hub_integrations = MagicMock() + mock_security_hub_integrations.exists.return_value = True + mock_integration_filter.return_value = mock_security_hub_integrations + + # Mock s3_integration_task + mock_s3_task.apply_async.return_value.get.return_value = True + + # Mock other necessary components + mock_initialize_provider.return_value = MagicMock() + mock_compliance_bulk.return_value = {} + mock_get_frameworks.return_value = [] + mock_generate_dir.return_value = ("out-dir", "comp-dir") + mock_transform_stats.return_value = {"stats": "data"} + + # Mock findings + mock_finding = MagicMock() + mock_findings.return_value.order_by.return_value.iterator.return_value = [ + [mock_finding], + True, + ] + mock_transform_finding.return_value = MagicMock(compliance={}) + + # Track which output formats were created + created_writers = {} + + def track_writer_creation(cls_type): + def factory(*args, **kwargs): + writer = MagicMock() + writer._data = [] + writer.transform = MagicMock() + writer.batch_write_data_to_file = MagicMock() + created_writers[cls_type] = writer + return writer + + return factory + + # Mock OUTPUT_FORMATS_MAPPING with tracking + with patch( + "tasks.tasks.OUTPUT_FORMATS_MAPPING", + { + "csv": { + "class": track_writer_creation("csv"), + "suffix": ".csv", + "kwargs": {}, + }, + "json-asff": { + "class": track_writer_creation("asff"), + "suffix": ".asff.json", + "kwargs": {}, + }, + "json-ocsf": { + "class": track_writer_creation("ocsf"), + "suffix": ".ocsf.json", + "kwargs": {}, + }, + }, + ): + mock_compress.return_value = "/tmp/compressed.zip" + mock_upload.return_value = "s3://bucket/file.zip" + + # Execute + result = generate_outputs_task( + scan_id=self.scan_id, + provider_id=self.provider_id, + tenant_id=self.tenant_id, + ) + + # Verify ASFF was created for AWS with SecurityHub + assert "asff" in created_writers, "ASFF writer should be created" + assert "csv" in created_writers, "CSV writer should be created" + assert "ocsf" in created_writers, "OCSF writer should be created" + + # Verify SecurityHub integration was checked + assert mock_integration_filter.call_count == 2 + mock_integration_filter.assert_any_call( + integrationproviderrelationship__provider_id=self.provider_id, + integration_type=Integration.IntegrationChoices.AWS_SECURITY_HUB, + enabled=True, + ) + + assert result == {"upload": True} + + @patch("tasks.tasks.s3_integration_task") + @patch("tasks.tasks.Integration.objects.filter") + @patch("tasks.tasks.ScanSummary.objects.filter") + @patch("tasks.tasks.Provider.objects.get") + @patch("tasks.tasks.initialize_prowler_provider") + @patch("tasks.tasks.Compliance.get_bulk") + @patch("tasks.tasks.get_compliance_frameworks") + @patch("tasks.tasks.Finding.all_objects.filter") + @patch("tasks.tasks._generate_output_directory") + @patch("tasks.tasks.FindingOutput._transform_findings_stats") + @patch("tasks.tasks.FindingOutput.transform_api_finding") + @patch("tasks.tasks._compress_output_files") + @patch("tasks.tasks._upload_to_s3") + @patch("tasks.tasks.Scan.all_objects.filter") + @patch("tasks.tasks.rmtree") + def test_generate_outputs_no_asff_for_aws_without_security_hub( + self, + mock_rmtree, + mock_scan_update, + mock_upload, + mock_compress, + mock_transform_finding, + mock_transform_stats, + mock_generate_dir, + mock_findings, + mock_get_frameworks, + mock_compliance_bulk, + mock_initialize_provider, + mock_provider_get, + mock_scan_summary, + mock_integration_filter, + mock_s3_task, + ): + """Test that ASFF output is NOT generated for AWS providers without SecurityHub integration.""" + # Setup + mock_scan_summary_qs = MagicMock() + mock_scan_summary_qs.exists.return_value = True + mock_scan_summary.return_value = mock_scan_summary_qs + + # Mock AWS provider + mock_provider = MagicMock() + mock_provider.uid = "aws-account-123" + mock_provider.provider = "aws" + mock_provider_get.return_value = mock_provider + + # Mock NO SecurityHub integration + mock_security_hub_integrations = MagicMock() + mock_security_hub_integrations.exists.return_value = False + mock_integration_filter.return_value = mock_security_hub_integrations + + # Mock other necessary components + mock_initialize_provider.return_value = MagicMock() + mock_compliance_bulk.return_value = {} + mock_get_frameworks.return_value = [] + mock_generate_dir.return_value = ("out-dir", "comp-dir") + mock_transform_stats.return_value = {"stats": "data"} + + # Mock findings + mock_finding = MagicMock() + mock_findings.return_value.order_by.return_value.iterator.return_value = [ + [mock_finding], + True, + ] + mock_transform_finding.return_value = MagicMock(compliance={}) + + # Track which output formats were created + created_writers = {} + + def track_writer_creation(cls_type): + def factory(*args, **kwargs): + writer = MagicMock() + writer._data = [] + writer.transform = MagicMock() + writer.batch_write_data_to_file = MagicMock() + created_writers[cls_type] = writer + return writer + + return factory + + # Mock OUTPUT_FORMATS_MAPPING with tracking + with patch( + "tasks.tasks.OUTPUT_FORMATS_MAPPING", + { + "csv": { + "class": track_writer_creation("csv"), + "suffix": ".csv", + "kwargs": {}, + }, + "json-asff": { + "class": track_writer_creation("asff"), + "suffix": ".asff.json", + "kwargs": {}, + }, + "json-ocsf": { + "class": track_writer_creation("ocsf"), + "suffix": ".ocsf.json", + "kwargs": {}, + }, + }, + ): + mock_compress.return_value = "/tmp/compressed.zip" + mock_upload.return_value = "s3://bucket/file.zip" + + # Execute + result = generate_outputs_task( + scan_id=self.scan_id, + provider_id=self.provider_id, + tenant_id=self.tenant_id, + ) + + # Verify ASFF was NOT created when no SecurityHub integration + assert "asff" not in created_writers, "ASFF writer should NOT be created" + assert "csv" in created_writers, "CSV writer should be created" + assert "ocsf" in created_writers, "OCSF writer should be created" + + # Verify SecurityHub integration was checked + assert mock_integration_filter.call_count == 2 + mock_integration_filter.assert_any_call( + integrationproviderrelationship__provider_id=self.provider_id, + integration_type=Integration.IntegrationChoices.AWS_SECURITY_HUB, + enabled=True, + ) + + assert result == {"upload": True} + + @patch("tasks.tasks.ScanSummary.objects.filter") + @patch("tasks.tasks.Provider.objects.get") + @patch("tasks.tasks.initialize_prowler_provider") + @patch("tasks.tasks.Compliance.get_bulk") + @patch("tasks.tasks.get_compliance_frameworks") + @patch("tasks.tasks.Finding.all_objects.filter") + @patch("tasks.tasks._generate_output_directory") + @patch("tasks.tasks.FindingOutput._transform_findings_stats") + @patch("tasks.tasks.FindingOutput.transform_api_finding") + @patch("tasks.tasks._compress_output_files") + @patch("tasks.tasks._upload_to_s3") + @patch("tasks.tasks.Scan.all_objects.filter") + @patch("tasks.tasks.rmtree") + def test_generate_outputs_no_asff_for_non_aws_provider( + self, + mock_rmtree, + mock_scan_update, + mock_upload, + mock_compress, + mock_transform_finding, + mock_transform_stats, + mock_generate_dir, + mock_findings, + mock_get_frameworks, + mock_compliance_bulk, + mock_initialize_provider, + mock_provider_get, + mock_scan_summary, + ): + """Test that ASFF output is NOT generated for non-AWS providers (e.g., Azure, GCP).""" + # Setup + mock_scan_summary_qs = MagicMock() + mock_scan_summary_qs.exists.return_value = True + mock_scan_summary.return_value = mock_scan_summary_qs + + # Mock Azure provider (non-AWS) + mock_provider = MagicMock() + mock_provider.uid = "azure-subscription-123" + mock_provider.provider = "azure" # Non-AWS provider + mock_provider_get.return_value = mock_provider + + # Mock other necessary components + mock_initialize_provider.return_value = MagicMock() + mock_compliance_bulk.return_value = {} + mock_get_frameworks.return_value = [] + mock_generate_dir.return_value = ("out-dir", "comp-dir") + mock_transform_stats.return_value = {"stats": "data"} + + # Mock findings + mock_finding = MagicMock() + mock_findings.return_value.order_by.return_value.iterator.return_value = [ + [mock_finding], + True, + ] + mock_transform_finding.return_value = MagicMock(compliance={}) + + # Track which output formats were created + created_writers = {} + + def track_writer_creation(cls_type): + def factory(*args, **kwargs): + writer = MagicMock() + writer._data = [] + writer.transform = MagicMock() + writer.batch_write_data_to_file = MagicMock() + created_writers[cls_type] = writer + return writer + + return factory + + # Mock OUTPUT_FORMATS_MAPPING with tracking + with patch( + "tasks.tasks.OUTPUT_FORMATS_MAPPING", + { + "csv": { + "class": track_writer_creation("csv"), + "suffix": ".csv", + "kwargs": {}, + }, + "json-asff": { + "class": track_writer_creation("asff"), + "suffix": ".asff.json", + "kwargs": {}, + }, + "json-ocsf": { + "class": track_writer_creation("ocsf"), + "suffix": ".ocsf.json", + "kwargs": {}, + }, + }, + ): + mock_compress.return_value = "/tmp/compressed.zip" + mock_upload.return_value = "s3://bucket/file.zip" + + # Execute + result = generate_outputs_task( + scan_id=self.scan_id, + provider_id=self.provider_id, + tenant_id=self.tenant_id, + ) + + # Verify ASFF was NOT created for non-AWS provider + assert ( + "asff" not in created_writers + ), "ASFF writer should NOT be created for non-AWS providers" + assert "csv" in created_writers, "CSV writer should be created" + assert "ocsf" in created_writers, "OCSF writer should be created" + + assert result == {"upload": True} + @patch("tasks.tasks.upload_s3_integration") def test_s3_integration_task_success(self, mock_upload): mock_upload.return_value = True @@ -598,3 +999,33 @@ class TestCheckIntegrationsTask: mock_upload.assert_called_once_with( self.tenant_id, self.provider_id, output_directory ) + + @patch("tasks.tasks.upload_security_hub_integration") + def test_security_hub_integration_task_success(self, mock_upload): + """Test successful SecurityHub integration task execution.""" + mock_upload.return_value = True + scan_id = "test-scan-123" + + result = security_hub_integration_task( + tenant_id=self.tenant_id, + provider_id=self.provider_id, + scan_id=scan_id, + ) + + assert result is True + mock_upload.assert_called_once_with(self.tenant_id, self.provider_id, scan_id) + + @patch("tasks.tasks.upload_security_hub_integration") + def test_security_hub_integration_task_failure(self, mock_upload): + """Test SecurityHub integration task handling failure.""" + mock_upload.return_value = False + scan_id = "test-scan-123" + + result = security_hub_integration_task( + tenant_id=self.tenant_id, + provider_id=self.provider_id, + scan_id=scan_id, + ) + + assert result is False + mock_upload.assert_called_once_with(self.tenant_id, self.provider_id, scan_id) diff --git a/docs/about.md b/docs/about.md deleted file mode 100644 index 0bc36e65b0..0000000000 --- a/docs/about.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -hide: -- toc ---- -# About - -## Author -Prowler was created by **Toni de la Fuente** in 2016. - -| ![](img/toni.png)
[![Twitter URL](https://img.shields.io/twitter/url/https/twitter.com/toniblyx.svg?style=social&label=Follow%20%40toniblyx)](https://twitter.com/toniblyx) [![Twitter URL](https://img.shields.io/twitter/url/https/twitter.com/prowlercloud.svg?style=social&label=Follow%20%40prowlercloud)](https://twitter.com/prowlercloud)| -|:--:| -| Toni de la Fuente | - -## Maintainers -Prowler is maintained by the Engineers of the **Prowler Team** : - -| ![](img/nacho.png)[![Twitter URL](https://img.shields.io/twitter/url/https/twitter.com/NachoRivCor.svg?style=social&label=Follow%20%40NachoRivCor)](https://twitter.com/NachoRivCor) | ![](img/sergio.png)[![Twitter URL](https://img.shields.io/twitter/url/https/twitter.com/sergargar1.svg?style=social&label=Follow%20%40sergargar1)](https://twitter.com/sergargar1) |![](img/pepe.png)[![Twitter URL](https://img.shields.io/twitter/url/https/twitter.com/jfagoagas.svg?style=social&label=Follow%20%40jfagoagas)](https://twitter.com/jfagoagas) | -|:--:|:--:|:--: -| Nacho Rivera| Sergio Garcia| Pepe Fagoaga| - -## License - -Prowler is licensed as **Apache License 2.0** as specified in each file. You may obtain a copy of the License at - diff --git a/docs/basic-usage/prowler-app.md b/docs/basic-usage/prowler-app.md new file mode 100644 index 0000000000..8ed7625577 --- /dev/null +++ b/docs/basic-usage/prowler-app.md @@ -0,0 +1,61 @@ +## Access Prowler App + +After [installation](../installation/prowler-app.md), navigate to [http://localhost:3000](http://localhost:3000) and sign up with email and password. + +Sign Up Button +Sign Up + +???+ note "User creation and default tenant behavior" + + When creating a new user, the behavior depends on whether an invitation is provided: + + - **Without an invitation**: + + - A new tenant is automatically created. + - The new user is assigned to this tenant. + - A set of **RBAC admin permissions** is generated and assigned to the user for the newly-created tenant. + + - **With an invitation**: The user is added to the specified tenant with the permissions defined in the invitation. + + This mechanism ensures that the first user in a newly created tenant has administrative permissions within that tenant. + +## Log In + +Access Prowler App by logging in with **email and password**. + +Log In + +## Add Cloud Provider + +Configure a cloud provider for scanning: + +1. Navigate to `Settings > Cloud Providers` and click `Add Account`. +2. Select the cloud provider. +3. Enter the provider's identifier (Optional: Add an alias): + - **AWS**: Account ID + - **GCP**: Project ID + - **Azure**: Subscription ID + - **Kubernetes**: Cluster ID + - **M365**: Domain ID +4. Follow the guided instructions to add and authenticate your credentials. + +## Start a Scan + +Once credentials are successfully added and validated, Prowler initiates a scan of your cloud environment. + +Click `Go to Scans` to monitor progress. + +## View Results + +Review findings during scan execution in the following sections: + +- **Overview** – Provides a high-level summary of your scans. + Overview + +- **Compliance** – Displays compliance insights based on security frameworks. + Compliance + +> For detailed usage instructions, refer to the [Prowler App Guide](../tutorials/prowler-app.md). + +???+ note + Prowler will automatically scan all configured providers every **24 hours**, ensuring your cloud environment stays continuously monitored. diff --git a/docs/basic-usage/prowler-cli.md b/docs/basic-usage/prowler-cli.md new file mode 100644 index 0000000000..0c093968b4 --- /dev/null +++ b/docs/basic-usage/prowler-cli.md @@ -0,0 +1,257 @@ +## Running Prowler + +Running Prowler requires specifying the provider (e.g `aws`, `gcp`, `azure`, `m365`, `github` or `kubernetes`): + +???+ note + If no provider is specified, AWS is used by default for backward compatibility with Prowler v2. + +```console +prowler +``` +![Prowler Execution](../img/short-display.png) + +???+ note + Running the `prowler` command without options will uses environment variable credentials. Refer to the Authentication section of each provider for credential configuration details. + +## Verbose Output + +If you prefer the former verbose output, use: `--verbose`. This allows seeing more info while Prowler is running, minimal output is displayed unless verbosity is enabled. + +## Report Generation + +By default, Prowler generates CSV, JSON-OCSF, and HTML reports. To generate a JSON-ASFF report (used by AWS Security Hub), specify `-M` or `--output-modes`: + +```console +prowler -M csv json-asff json-ocsf html +``` +The HTML report is saved in the output directory, alongside other reports. It will look like this: + +![Prowler Execution](../img/html-output.png) + +## Listing Available Checks and Services + +List all available checks or services within a provider using `-l`/`--list-checks` or `--list-services`. + +```console +prowler --list-checks +prowler --list-services +``` +## Running Specific Checks or Services + +Execute specific checks or services using `-c`/`checks` or `-s`/`services`: + +```console +prowler azure --checks storage_blob_public_access_level_is_disabled +prowler aws --services s3 ec2 +prowler gcp --services iam compute +prowler kubernetes --services etcd apiserver +``` +## Excluding Checks and Services + +Checks and services can be excluded with `-e`/`--excluded-checks` or `--excluded-services`: + +```console +prowler aws --excluded-checks s3_bucket_public_access +prowler azure --excluded-services defender iam +prowler gcp --excluded-services kms +prowler kubernetes --excluded-services controllermanager +``` +## Additional Options + +Explore more advanced time-saving execution methods in the [Miscellaneous](../tutorials/misc.md) section. + +Access the help menu and view all available options with `-h`/`--help`: + +```console +prowler --help +``` + +## AWS + +Use a custom AWS profile with `-p`/`--profile` and/or specific AWS regions with `-f`/`--filter-region`: + +```console +prowler aws --profile custom-profile -f us-east-1 eu-south-2 +``` + +???+ note + By default, `prowler` will scan all AWS regions. + +See more details about AWS Authentication in the [Authentication Section](../tutorials/aws/authentication.md) section. + +## Azure + +Azure requires specifying the auth method: + +```console +# To use service principal authentication +prowler azure --sp-env-auth + +# To use az cli authentication +prowler azure --az-cli-auth + +# To use browser authentication +prowler azure --browser-auth --tenant-id "XXXXXXXX" + +# To use managed identity auth +prowler azure --managed-identity-auth +``` + +See more details about Azure Authentication in the [Authentication Section](../tutorials/azure/authentication.md) + +By default, Prowler scans all accessible subscriptions. Scan specific subscriptions using the following flag (using az cli auth as example): + +```console +prowler azure --az-cli-auth --subscription-ids ... +``` +## Google Cloud + +- **User Account Credentials** + + By default, Prowler uses **User Account credentials**. Configure accounts using: + + - `gcloud init` – Set up a new account. + - `gcloud config set account ` – Switch to an existing account. + + Once configured, obtain access credentials using: `gcloud auth application-default login`. + +- **Service Account Authentication** + + Alternatively, you can use Service Account credentials: + + Generate and download Service Account keys in JSON format. Refer to [Google IAM documentation](https://cloud.google.com/iam/docs/creating-managing-service-account-keys) for details. + + Provide the key file location using this argument: + + ```console + prowler gcp --credentials-file path + ``` + +- **Scanning Specific GCP Projects** + + By default, Prowler scans all accessible GCP projects. Scan specific projects with the `--project-ids` flag: + + ```console + prowler gcp --project-ids ... + ``` + +- **GCP Retry Configuration** + + Configure the maximum number of retry attempts for Google Cloud SDK API calls with the `--gcp-retries-max-attempts` flag: + + ```console + prowler gcp --gcp-retries-max-attempts 5 + ``` + + This is useful when experiencing quota exceeded errors (HTTP 429) to increase the number of automatic retry attempts. + +## Kubernetes + +Prowler enables security scanning of Kubernetes clusters, supporting both **in-cluster** and **external** execution. + +- **Non In-Cluster Execution** + + ```console + prowler kubernetes --kubeconfig-file path + ``` + ???+ note + If no `--kubeconfig-file` is provided, Prowler will use the default KubeConfig file location (`~/.kube/config`). + +- **In-Cluster Execution** + + To run Prowler inside the cluster, apply the provided YAML configuration to deploy a job in a new namespace: + + ```console + kubectl apply -f kubernetes/prowler-sa.yaml + kubectl apply -f kubernetes/job.yaml + kubectl apply -f kubernetes/prowler-role.yaml + kubectl apply -f kubernetes/prowler-rolebinding.yaml + kubectl get pods --namespace prowler-ns --> prowler-XXXXX + kubectl logs prowler-XXXXX --namespace prowler-ns + ``` + + ???+ note + By default, Prowler scans all namespaces in the active Kubernetes context. Use the `--context`flag to specify the context to be scanned and `--namespaces` to restrict scanning to specific namespaces. + +## Microsoft 365 + +Microsoft 365 requires specifying the auth method: + +```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 az cli authentication +prowler m365 --az-cli-auth + +# To use browser authentication +prowler m365 --browser-auth --tenant-id "XXXXXXXX" + +``` + +See more details about M365 Authentication in the [Authentication Section](../tutorials/microsoft365/authentication.md) section. + +## GitHub + +Prowler enables security scanning of your **GitHub account**, including **Repositories**, **Organizations** and **Applications**. + +- **Supported Authentication Methods** + + Authenticate using one of the following methods: + + ```console + # Personal Access Token (PAT): + prowler github --personal-access-token pat + + # OAuth App Token: + prowler github --oauth-app-token oauth_token + + # GitHub App Credentials: + prowler github --github-app-id app_id --github-app-key app_key + ``` + + ???+ note + 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` + 3. `GITHUB_APP_ID` and `GITHUB_APP_KEY` + +## Infrastructure as Code (IaC) + +Prowler's Infrastructure as Code (IaC) provider enables you to scan local or remote infrastructure code for security and compliance issues using [Trivy](https://trivy.dev/). 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 + +# Scan a remote GitHub repository (public or private) +prowler iac --scan-repository-url https://github.com/user/repo.git + +# Authenticate to a private repo with GitHub username and PAT +prowler iac --scan-repository-url https://github.com/user/repo.git \ + --github-username --personal-access-token + +# Authenticate to a private repo with OAuth App Token +prowler iac --scan-repository-url https://github.com/user/repo.git \ + --oauth-app-token + +# 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 + - `--scan-path` and `--scan-repository-url` are mutually exclusive; only one can be specified at a time. + - For remote repository scans, authentication can be provided via CLI flags or environment variables (`GITHUB_OAUTH_APP_TOKEN`, `GITHUB_USERNAME`, `GITHUB_PERSONAL_ACCESS_TOKEN`). CLI flags take precedence. + - The IaC provider does not require cloud authentication for local scans. + - It is ideal for CI/CD pipelines and local development environments. + - For more details on supported scanners, see the [Trivy documentation](https://trivy.dev/latest/docs/scanner/vulnerability/) + +See more details about IaC scanning in the [IaC Tutorial](../tutorials/iac/getting-started-iac.md) section. diff --git a/docs/developer-guide/aws-details.md b/docs/developer-guide/aws-details.md index 819f04fa64..9fa182d689 100644 --- a/docs/developer-guide/aws-details.md +++ b/docs/developer-guide/aws-details.md @@ -2,7 +2,7 @@ In this page you can find all the details about [Amazon Web Services (AWS)](https://aws.amazon.com/) provider implementation in Prowler. -By default, Prowler will audit just one account and organization settings per scan. To configure it, follow the [getting started](../index.md#aws) page. +By default, Prowler will audit just one account and organization settings per scan. To configure it, follow the [AWS getting started guide](../tutorials/aws/getting-started-aws.md). ## AWS Provider Classes Architecture diff --git a/docs/developer-guide/azure-details.md b/docs/developer-guide/azure-details.md index 9d21706acd..d1eb6b73be 100644 --- a/docs/developer-guide/azure-details.md +++ b/docs/developer-guide/azure-details.md @@ -2,7 +2,7 @@ In this page you can find all the details about [Microsoft Azure](https://azure.microsoft.com/) provider implementation in Prowler. -By default, Prowler will audit all the subscriptions that it is able to list in the Microsoft Entra tenant, and tenant Entra ID service. To configure it, follow the [getting started](../index.md#azure) page. +By default, Prowler will audit all the subscriptions that it is able to list in the Microsoft Entra tenant, and tenant Entra ID service. To configure it, follow the [Azure getting started guide](../tutorials/azure/getting-started-azure.md). ## Azure Provider Classes Architecture diff --git a/docs/developer-guide/checks.md b/docs/developer-guide/checks.md index 285eafb542..6f760eb558 100644 --- a/docs/developer-guide/checks.md +++ b/docs/developer-guide/checks.md @@ -265,7 +265,7 @@ Below is a generic example of a check metadata file. **Do not include comments i - For AWS this field must follow the [AWS Security Hub Types](https://docs.aws.amazon.com/securityhub/latest/userguide/asff-required-attributes.html#Types) format. So the common pattern to follow is `namespace/category/classifier`, refer to the attached documentation for the valid values for this fields. - **ServiceName** — The name of the provider service being audited. This field **must** be in lowercase and match with the service folder name. For supported services refer to [Prowler Hub](https://hub.prowler.com/check) or directly to [Prowler Code](https://github.com/prowler-cloud/prowler/tree/master/prowler/providers). - **SubServiceName** — The subservice or resource within the service, if applicable. For more information refer to the [Naming Format for Checks](#naming-format-for-checks) section. -- **ResourceIdTemplate** — A template for the unique resource identifier. For more information refer to the [Prowler's Resource Identification](#prowlers-resource-identification) section. +- **ResourceIdTemplate** — A template for the unique resource identifier. For more information refer to the [Resource Identification in Prowler](#resource-identification-in-prowler) section. - **Severity** — The severity of the finding if the check fails. Must be one of: `critical`, `high`, `medium`, `low`, or `informational`, this field **must** be in lowercase. To get more information about the severity levels refer to the [Prowler's Check Severity Levels](#prowlers-check-severity-levels) section. - **ResourceType** — The type of resource being audited. *For now this field is only standardized for the AWS provider*. - For AWS use the [Security Hub resource types](https://docs.aws.amazon.com/securityhub/latest/userguide/asff-resources.html) or, if not available, the PascalCase version of the [CloudFormation type](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html) (e.g., `AwsEc2Instance`). Use "Other" if no match exists. diff --git a/docs/developer-guide/gcp-details.md b/docs/developer-guide/gcp-details.md index cb77b1f3c6..dc13958a05 100644 --- a/docs/developer-guide/gcp-details.md +++ b/docs/developer-guide/gcp-details.md @@ -2,7 +2,7 @@ This page details the [Google Cloud Platform (GCP)](https://cloud.google.com/) provider implementation in Prowler. -By default, Prowler will audit all the GCP projects that the authenticated identity can access. To configure it, follow the [getting started](../index.md#google-cloud) page. +By default, Prowler will audit all the GCP projects that the authenticated identity can access. To configure it, follow the [GCP getting started guide](../tutorials/gcp/getting-started-gcp.md). ## GCP Provider Classes Architecture diff --git a/docs/developer-guide/github-details.md b/docs/developer-guide/github-details.md index 0dc3c5460d..97a1cffac3 100644 --- a/docs/developer-guide/github-details.md +++ b/docs/developer-guide/github-details.md @@ -2,7 +2,7 @@ This page details the [GitHub](https://github.com/) provider implementation in Prowler. -By default, Prowler will audit the GitHub account - scanning all repositories, organizations, and applications that your configured credentials can access. To configure it, follow the [getting started](../index.md#github) page. +By default, Prowler will audit the GitHub account - scanning all repositories, organizations, and applications that your configured credentials can access. To configure it, follow the [GitHub getting started guide](../tutorials/github/getting-started-github.md). ## GitHub Provider Classes Architecture diff --git a/docs/developer-guide/kubernetes-details.md b/docs/developer-guide/kubernetes-details.md index 8b08b51e64..be4b72f383 100644 --- a/docs/developer-guide/kubernetes-details.md +++ b/docs/developer-guide/kubernetes-details.md @@ -2,7 +2,7 @@ This page details the [Kubernetes](https://kubernetes.io/) provider implementation in Prowler. -By default, Prowler will audit all namespaces in the Kubernetes cluster accessible by the configured context. To configure it, follow the [getting started](../index.md#kubernetes) page. +By default, Prowler will audit all namespaces in the Kubernetes cluster accessible by the configured context. To configure it, see the [In-Cluster Execution](../tutorials/kubernetes/in-cluster.md) or [Non In-Cluster Execution](../tutorials/kubernetes/outside-cluster.md) guides. ## Kubernetes Provider Classes Architecture diff --git a/docs/developer-guide/m365-details.md b/docs/developer-guide/m365-details.md index 0840b9856c..65fcd3f622 100644 --- a/docs/developer-guide/m365-details.md +++ b/docs/developer-guide/m365-details.md @@ -2,7 +2,7 @@ This page details the [Microsoft 365 (M365)](https://www.microsoft.com/en-us/microsoft-365) provider implementation in Prowler. -By default, Prowler will audit the Microsoft Entra ID tenant and its supported services. To configure it, follow the [getting started](../index.md#microsoft-365) page. +By default, Prowler will audit the Microsoft Entra ID tenant and its supported services. To configure it, follow the [M365 getting started guide](../tutorials/microsoft365/getting-started-m365.md). --- @@ -15,7 +15,7 @@ By default, Prowler will audit the Microsoft Entra ID tenant and its supported s - **Required modules:** - [ExchangeOnlineManagement](https://www.powershellgallery.com/packages/ExchangeOnlineManagement/3.6.0) (≥ 3.6.0) - [MicrosoftTeams](https://www.powershellgallery.com/packages/MicrosoftTeams/6.6.0) (≥ 6.6.0) -- If you use Prowler Cloud or the official containers, PowerShell is pre-installed. For local or pip installations, you must install PowerShell and the modules yourself. See [Requirements: Supported PowerShell Versions](../getting-started/requirements.md#supported-powershell-versions) and [Needed PowerShell Modules](../getting-started/requirements.md#needed-powershell-modules). +- If you use Prowler Cloud or the official containers, PowerShell is pre-installed. For local or pip installations, you must install PowerShell and the modules yourself. See [Authentication: Supported PowerShell Versions](../tutorials/microsoft365/authentication.md#supported-powershell-versions) and [Needed PowerShell Modules](../tutorials/microsoft365/authentication.md#required-powershell-modules). - For more details and troubleshooting, see [Use of PowerShell in M365](../tutorials/microsoft365/use-of-powershell.md). --- diff --git a/docs/developer-guide/services.md b/docs/developer-guide/services.md index 4c70ce4b84..7615404cd9 100644 --- a/docs/developer-guide/services.md +++ b/docs/developer-guide/services.md @@ -231,11 +231,11 @@ Before implementing a new service, verify that Prowler's existing permissions fo Provider-Specific Permissions Documentation: -- [AWS](../getting-started/requirements.md#authentication) -- [Azure](../getting-started/requirements.md#needed-permissions) -- [GCP](../getting-started/requirements.md#needed-permissions_1) -- [M365](../getting-started/requirements.md#needed-permissions_2) -- [GitHub](../getting-started/requirements.md#authentication_2) +- [AWS](../tutorials/aws/authentication.md#required-permissions) +- [Azure](../tutorials/azure/authentication.md#required-permissions) +- [GCP](../tutorials/gcp/authentication.md#required-permissions) +- [M365](../tutorials/microsoft365/authentication.md#required-permissions) +- [GitHub](../tutorials/github/authentication.md) ## Best Practices diff --git a/docs/developer-guide/unit-testing.md b/docs/developer-guide/unit-testing.md index 42674c7899..ab4e4e3bf7 100644 --- a/docs/developer-guide/unit-testing.md +++ b/docs/developer-guide/unit-testing.md @@ -39,7 +39,7 @@ To execute the Prowler test suite, install the necessary dependencies listed in ### Prerequisites -If you have not installed Prowler yet, refer to the [developer guide introduction](./introduction.md#get-the-code-and-install-all-dependencies). +If you have not installed Prowler yet, refer to the [developer guide introduction](./introduction.md#getting-the-code-and-installing-all-dependencies). ### Executing Tests @@ -520,7 +520,7 @@ Execute tests on the service `__init__` to ensure correct information retrieval. While service tests resemble *Integration Tests*, as they assess how the service interacts with the provider, they ultimately fall under *Unit Tests*, due to the use of Moto or custom mock objects. -For detailed guidance on test creation and existing service tests, refer to the [AWS checks test](./unit-testing.md#checks) [documentation](https://github.com/prowler-cloud/prowler/tree/master/tests/providers/aws/services). +For detailed guidance on test creation and existing service tests, check the current [AWS checks implementation](https://github.com/prowler-cloud/prowler/tree/master/tests/providers/aws/services). ## GCP diff --git a/docs/getting-started/requirements.md b/docs/getting-started/requirements.md deleted file mode 100644 index 12c8b3c0d9..0000000000 --- a/docs/getting-started/requirements.md +++ /dev/null @@ -1,591 +0,0 @@ -# Prowler Requirements - -Prowler is built in Python and utilizes the following SDKs: - -- [AWS SDK (Boto3)](https://boto3.amazonaws.com/v1/documentation/api/latest/index.html#) -- [Azure SDK](https://azure.github.io/azure-sdk-for-python/) -- [GCP API Python Client](https://github.com/googleapis/google-api-python-client/) -- [Kubernetes SDK](https://github.com/kubernetes-client/python) -- [M365 Graph SDK](https://github.com/microsoftgraph/msgraph-sdk-python) -- [Github REST API SDK](https://github.com/PyGithub/PyGithub) - -## AWS - -Prowler requires AWS credentials to function properly. You can authenticate using any method outlined in the [AWS CLI configuration guide](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html#cli-configure-quickstart-precedence). - -### Authentication Steps - -Ensure your AWS CLI is correctly configured with valid credentials and region settings. You can achieve this via: - -```console -aws configure -``` - -or - -```console -export AWS_ACCESS_KEY_ID="ASXXXXXXX" -export AWS_SECRET_ACCESS_KEY="XXXXXXXXX" -export AWS_SESSION_TOKEN="XXXXXXXXX" -``` - -#### Required IAM Permissions - -The credentials used must be associated with a user or role that has appropriate permissions for security checks. Attach the following AWS managed policies to ensure access: - - - `arn:aws:iam::aws:policy/SecurityAudit` - - `arn:aws:iam::aws:policy/job-function/ViewOnlyAccess` - -#### Additional Permissions - -For certain checks, additional read-only permissions are required. Attach the following custom policy to your role: - -[prowler-additions-policy.json](https://github.com/prowler-cloud/prowler/blob/master/permissions/prowler-additions-policy.json) - -If you intend to send findings to -[AWS Security Hub](https://aws.amazon.com/security-hub), attach the following custom policy: - -[prowler-security-hub.json](https://github.com/prowler-cloud/prowler/blob/master/permissions/prowler-security-hub.json). - -### Multi-Factor Authentication (MFA) - -If your IAM entity requires Multi-Factor Authentication (MFA), you can use the `--mfa` flag. Prowler will prompt you to enter the following values to initiate a new session: - -- **ARN of your MFA device** -- **TOTP (Time-Based One-Time Password)** - -## Azure - -Prowler for Azure supports multiple authentication types. To use a specific method, pass the appropriate flag during execution: - -- [**Service Principal Application**](https://learn.microsoft.com/en-us/entra/identity-platform/app-objects-and-service-principals?tabs=browser#service-principal-object) (**Recommended**) -- Existing **AZ CLI credentials** -- **Interactive browser authentication** -- [**Managed Identity**](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview) authentication - -> ⚠️ **Important:** For Prowler App, only Service Principal authentication is supported. - -### Service Principal Application Authentication - -To allow Prowler to authenticate using a Service Principal Application, set up the following environment variables: - -```console -export AZURE_CLIENT_ID="XXXXXXXXX" -export AZURE_TENANT_ID="XXXXXXXXX" -export AZURE_CLIENT_SECRET="XXXXXXX" -``` - -If you execute Prowler with the `--sp-env-auth` flag and these variables are not set or exported, execution will fail. - -Refer to the [Create Prowler Service Principal](../tutorials/azure/create-prowler-service-principal.md#how-to-create-prowler-service-principal-application) guide for detailed setup instructions. - -### Azure Authentication Methods - -Prowler for Azure supports the following authentication methods: - -- **AZ CLI Authentication (`--az-cli-auth`)** – Automated authentication using stored AZ CLI credentials. -- **Managed Identity Authentication (`--managed-identity-auth`)** – Automated authentication via Azure Managed Identity. -- **Browser Authentication (`--browser-auth`)** – Requires the user to authenticate using the default browser. The `tenant-id` parameter is mandatory for this method. - -### Required Permissions - -Prowler for Azure requires two types of permission scopes: - -#### Microsoft Entra ID Permissions - -These permissions allow Prowler to retrieve metadata from the assumed identity and perform specific Entra checks. While not mandatory for execution, they enhance functionality. - -Required permissions: - -- `Directory.Read.All` -- `Policy.Read.All` -- `UserAuthenticationMethod.Read.All` (used for Entra multifactor authentication checks) - - ???+ note - You can replace `Directory.Read.All` with `Domain.Read.All` that is a more restrictive permission but you won't be able to run the Entra checks related with DirectoryRoles and GetUsers. - - -#### Subscription Scope Permissions - -These permissions are required to perform security checks against Azure resources. The following **RBAC roles** must be assigned per subscription to the entity used by Prowler: - -- `Reader` – Grants read-only access to Azure resources. -- `ProwlerRole` – A custom role with minimal permissions, defined in the [prowler-azure-custom-role](https://github.com/prowler-cloud/prowler/blob/master/permissions/prowler-azure-custom-role.json). - -???+ note - The `assignableScopes` field in the JSON custom role file must be updated to reflect the correct subscription or management group. Use one of the following formats: `/subscriptions/` or `/providers/Microsoft.Management/managementGroups/`. - -### Assigning Permissions - -To properly configure permissions, follow these guides: - -- [Microsoft Entra ID permissions](../tutorials/azure/create-prowler-service-principal.md#assigning-the-proper-permissions) -- [Azure subscription permissions](../tutorials/azure/subscriptions.md#assign-the-appropriate-permissions-to-the-identity-that-is-going-to-be-assumed-by-prowler) - -???+ warning - Some permissions in `ProwlerRole` involve **write access**. If a `ReadOnly` lock is attached to certain resources, you may encounter errors, and findings for those checks will not be available. - -#### Checks Requiring `ProwlerRole` - -The following security checks require the `ProwlerRole` permissions for execution. Ensure the role is assigned to the identity assumed by Prowler before running these checks: - -- `app_function_access_keys_configured` -- `app_function_ftps_deployment_disabled` - -## Google Cloud - -### Authentication - -Prowler follows the same credential discovery process as the [Google authentication libraries](https://cloud.google.com/docs/authentication/application-default-credentials#search_order): - -1. **Environment Variable Authentication** – Uses the [`GOOGLE_APPLICATION_CREDENTIALS` environment variable](https://cloud.google.com/docs/authentication/application-default-credentials#GAC). -2. **Google Cloud CLI Credentials** – Uses credentials configured via the [Google Cloud CLI](https://cloud.google.com/docs/authentication/application-default-credentials#personal). -3. **Service Account Authentication** – Retrieves the attached service account credentials from the metadata server. More details [here](https://cloud.google.com/docs/authentication/application-default-credentials#attached-sa). - -### Required Permissions - -Prowler for Google Cloud requires the following permissions: - -#### IAM Roles -- **Reader (`roles/reader`)** – Must be granted at the **project, folder, or organization** level to allow scanning of target projects. - -#### Project-Level Settings - -At least one project must have the following configurations: - -- **Identity and Access Management (IAM) API (`iam.googleapis.com`)** – Must be enabled via: - - - The [Google Cloud API UI](https://console.cloud.google.com/apis/api/iam.googleapis.com/metrics), or - - The `gcloud` CLI: - ```sh - gcloud services enable iam.googleapis.com --project - ``` - -- **Service Usage Consumer (`roles/serviceusage.serviceUsageConsumer`)** IAM Role – Required for resource scanning. - -- **Quota Project Setting** – Define a quota project using either: - - - The `gcloud` CLI: - ```sh - gcloud auth application-default set-quota-project - ``` - - Setting an environment variable: - ```sh - export GOOGLE_CLOUD_QUOTA_PROJECT= - ``` - -### Default Project Scanning - -By default, Prowler scans **all accessible GCP projects**. To limit the scan to specific projects, use the `--project-ids` flag. - -## Microsoft 365 - -Prowler for Microsoft 365 (M365) supports the following authentication methods: - -- [**Service Principal Application**](https://learn.microsoft.com/en-us/entra/identity-platform/app-objects-and-service-principals?tabs=browser#service-principal-object) (**Recommended**) -- **Service Principal Application with Microsoft User Credentials** -- **Stored AZ CLI credentials** -- **Interactive browser authentication** - -???+ warning - Prowler App supports the **Service Principal** authentication method and the **Service Principal with User Credentials** authentication method, but this last one will be deprecated in September once Microsoft will enforce MFA in all tenants not allowing User authentication without interactive method. - -### Service Principal Authentication (Recommended) - -**Authentication flag:** `--sp-env-auth` - -To enable Prowler to authenticate as the **Service Principal Application**, configure the following environment variables: - -```console -export AZURE_CLIENT_ID="XXXXXXXXX" -export AZURE_CLIENT_SECRET="XXXXXXXXX" -export AZURE_TENANT_ID="XXXXXXXXX" -``` - -If these variables are not set or exported, execution using `--sp-env-auth` will fail. - -Refer to the [Create Prowler Service Principal](../tutorials/microsoft365/getting-started-m365.md#create-the-service-principal-app) guide for setup instructions. - -If the external API permissions described in the mentioned section above are not added only checks that work through MS Graph will be executed. This means that the full provider will not be executed. - -???+ note - In order to scan all the checks from M365 required permissions to the service principal application must be added. Refer to the [External API Permissions Assignment](../tutorials/microsoft365/getting-started-m365.md#grant-powershell-modules-permissions) section for more information. - -### Service Principal and User Credentials Authentication - -Authentication flag: `--env-auth` - -???+ warning - This method is not recommended anymore, we recommend just use the **Service Principal Application** authentication method instead. - -This method builds upon the Service Principal authentication by adding User Credentials. Configure the following environment variables: `M365_USER` and `M365_PASSWORD`. - -```console -export AZURE_CLIENT_ID="XXXXXXXXX" -export AZURE_CLIENT_SECRET="XXXXXXXXX" -export AZURE_TENANT_ID="XXXXXXXXX" -export M365_USER="your_email@example.com" -export M365_PASSWORD="examplepassword" -``` - -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. - - ???+ 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. - - ???+ warning - The user must not be MFA capable. Microsoft does not allow MFA capable users to authenticate programmatically. See [Microsoft documentation](https://learn.microsoft.com/en-us/entra/identity-platform/scenario-desktop-acquire-token-username-password?tabs=dotnet) for more information. - - ???+ warning - Using a tenant domain other than the one assigned — even if it belongs to the same tenant — will cause Prowler to fail, as Microsoft authentication will not succeed. - - Ensure you are using the right domain for the user you are trying to authenticate with. - - ![User Domains](../tutorials/microsoft365/img/user-domains.png) - -- `M365_PASSWORD` must be the user password. - - ???+ note - Before we asked for a encrypted password, but now we ask for the user password directly. Prowler will now handle the password encryption for you. - - - -### Interactive Browser Authentication - -**Authentication flag:** `--browser-auth` - -This authentication method requires the user to authenticate against Azure using the default browser to start the scan. The `--tenant-id` flag is also required. - -With these credentials, you will only be able to run checks that rely on Microsoft Graph. This means you won't be able to run the entire provider. To perform a full M365 security scan, use the **recommended authentication method**. - -Since this is a **delegated permission** authentication method, necessary permissions should be assigned to the user rather than the application. - -### Required Permissions - -To run the full Prowler provider, including PowerShell checks, two types of permission scopes must be set in **Microsoft Entra ID**. - -#### For Service Principal Authentication (`--sp-env-auth`) - Recommended - -When using service principal authentication, you need to add the following **Application Permissions** configured to: - -**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 `Global Reader` role to the app. -- `application_access` from external API `Skype and Teams Tenant Admin API`: Required for Teams PowerShell module app authentication. - -???+ note - `Directory.Read.All` can be replaced with `Domain.Read.All` that is a more restrictive permission but you won't be able to run the Entra checks related with DirectoryRoles and GetUsers. - - > If you do this you will need to add also the `Organization.Read.All` permission to the service principal application in order to authenticate. - -???+ 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, same as the Service Principal Authentication + User Credentials Authentication but this last one will be deprecated in September once Microsoft will enforce MFA in all tenants not allowing User authentication without interactive method. - - -#### 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). - - -#### 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. - -### Assigning Permissions and Roles - -For guidance on assigning the necessary permissions and roles, follow these instructions: -- [Grant API Permissions](../tutorials/microsoft365/getting-started-m365.md#grant-required-graph-api-permissions) -- [Assign Required Roles](../tutorials/microsoft365/getting-started-m365.md#if-using-user-authentication) - -### Supported PowerShell Versions - -PowerShell is required to run certain M365 checks. - -**Supported versions:** -- **PowerShell 7.4 or higher** (7.5 is recommended) - -#### Why Is PowerShell 7.4+ Required? - -- **PowerShell 5.1** (default on some Windows systems) does not support required cmdlets. -- Older [cross-platform PowerShell versions](https://learn.microsoft.com/en-us/powershell/scripting/install/powershell-support-lifecycle?view=powershell-7.5) are **unsupported**, leading to potential errors. - -???+ note - Installing PowerShell is only necessary if you install Prowler via **pip or other sources**. **SDK and API containers include PowerShell by default.** - -### Installing PowerShell - -Installing PowerShell is different depending on your OS. - -- [Windows](https://learn.microsoft.com/es-es/powershell/scripting/install/installing-powershell-on-windows?view=powershell-7.5#install-powershell-using-winget-recommended): you will need to update PowerShell to +7.4 to be able to run prowler, if not some checks will not show findings and the provider could not work as expected. This version of PowerShell is [supported](https://learn.microsoft.com/es-es/powershell/scripting/install/installing-powershell-on-windows?view=powershell-7.4#supported-versions-of-windows) on Windows 10, Windows 11, Windows Server 2016 and higher versions. - -```console -winget install --id Microsoft.PowerShell --source winget -``` - - -- [MacOS](https://learn.microsoft.com/es-es/powershell/scripting/install/installing-powershell-on-macos?view=powershell-7.5#install-the-latest-stable-release-of-powershell): installing PowerShell on MacOS needs to have installed [brew](https://brew.sh/), once you have it is just running the command above, Pwsh is only supported in macOS 15 (Sequoia) x64 and Arm64, macOS 14 (Sonoma) x64 and Arm64, macOS 13 (Ventura) x64 and Arm64 - -```console -brew install powershell/tap/powershell -``` - -Once it's installed run `pwsh` on your terminal to verify it's working. - -- Linux: installing PowerShell on Linux depends on the distro you are using: - - - [Ubuntu](https://learn.microsoft.com/es-es/powershell/scripting/install/install-ubuntu?view=powershell-7.5#installation-via-package-repository-the-package-repository): The required version for installing PowerShell +7.4 on Ubuntu are Ubuntu 22.04 and Ubuntu 24.04. The recommended way to install it is downloading the package available on PMC. You just need to follow the following steps: - - ```console - ################################### - # Prerequisites - - # Update the list of packages - sudo apt-get update - - # Install pre-requisite packages. - sudo apt-get install -y wget apt-transport-https software-properties-common - - # Get the version of Ubuntu - source /etc/os-release - - # Download the Microsoft repository keys - wget -q https://packages.microsoft.com/config/ubuntu/$VERSION_ID/packages-microsoft-prod.deb - - # Register the Microsoft repository keys - sudo dpkg -i packages-microsoft-prod.deb - - # Delete the Microsoft repository keys file - rm packages-microsoft-prod.deb - - # Update the list of packages after we added packages.microsoft.com - sudo apt-get update - - ################################### - # Install PowerShell - sudo apt-get install -y powershell - - # Start PowerShell - pwsh - ``` - - - [Alpine](https://learn.microsoft.com/es-es/powershell/scripting/install/install-alpine?view=powershell-7.5#installation-steps): The only supported version for installing PowerShell +7.4 on Alpine is Alpine 3.20. The unique way to install it is downloading the tar.gz package available on [PowerShell github](https://github.com/PowerShell/PowerShell/releases/download/v7.5.0/powershell-7.5.0-linux-musl-x64.tar.gz). You just need to follow the following steps: - - ```console - # Install the requirements - sudo apk add --no-cache \ - ca-certificates \ - less \ - ncurses-terminfo-base \ - krb5-libs \ - libgcc \ - libintl \ - libssl3 \ - libstdc++ \ - tzdata \ - userspace-rcu \ - zlib \ - icu-libs \ - curl - - apk -X https://dl-cdn.alpinelinux.org/alpine/edge/main add --no-cache \ - lttng-ust \ - openssh-client \ - - # Download the powershell '.tar.gz' archive - curl -L https://github.com/PowerShell/PowerShell/releases/download/v7.5.0/powershell-7.5.0-linux-musl-x64.tar.gz -o /tmp/powershell.tar.gz - - # Create the target folder where powershell will be placed - sudo mkdir -p /opt/microsoft/powershell/7 - - # Expand powershell to the target folder - sudo tar zxf /tmp/powershell.tar.gz -C /opt/microsoft/powershell/7 - - # Set execute permissions - sudo chmod +x /opt/microsoft/powershell/7/pwsh - - # Create the symbolic link that points to pwsh - sudo ln -s /opt/microsoft/powershell/7/pwsh /usr/bin/pwsh - - # Start PowerShell - pwsh - ``` - - - [Debian](https://learn.microsoft.com/es-es/powershell/scripting/install/install-debian?view=powershell-7.5#installation-on-debian-11-or-12-via-the-package-repository): The required version for installing PowerShell +7.4 on Debian are Debian 11 and Debian 12. The recommended way to install it is downloading the package available on PMC. You just need to follow the following steps: - - ```console - ################################### - # Prerequisites - - # Update the list of packages - sudo apt-get update - - # Install pre-requisite packages. - sudo apt-get install -y wget - - # Get the version of Debian - source /etc/os-release - - # Download the Microsoft repository GPG keys - wget -q https://packages.microsoft.com/config/debian/$VERSION_ID/packages-microsoft-prod.deb - - # Register the Microsoft repository GPG keys - sudo dpkg -i packages-microsoft-prod.deb - - # Delete the Microsoft repository GPG keys file - rm packages-microsoft-prod.deb - - # Update the list of packages after we added packages.microsoft.com - sudo apt-get update - - ################################### - # Install PowerShell - sudo apt-get install -y powershell - - # Start PowerShell - pwsh - ``` - - - [Rhel](https://learn.microsoft.com/es-es/powershell/scripting/install/install-rhel?view=powershell-7.5#installation-via-the-package-repository): The required version for installing PowerShell +7.4 on Red Hat are RHEL 8 and RHEL 9. The recommended way to install it is downloading the package available on PMC. You just need to follow the following steps: - - ```console - ################################### - # Prerequisites - - # Get version of RHEL - source /etc/os-release - if [ ${VERSION_ID%.*} -lt 8 ] - then majorver=7 - elif [ ${VERSION_ID%.*} -lt 9 ] - then majorver=8 - else majorver=9 - fi - - # Download the Microsoft RedHat repository package - curl -sSL -O https://packages.microsoft.com/config/rhel/$majorver/packages-microsoft-prod.rpm - - # Register the Microsoft RedHat repository - sudo rpm -i packages-microsoft-prod.rpm - - # Delete the downloaded package after installing - rm packages-microsoft-prod.rpm - - # Update package index files - sudo dnf update - # Install PowerShell - sudo dnf install powershell -y - ``` - -- [Docker](https://learn.microsoft.com/es-es/powershell/scripting/install/powershell-in-docker?view=powershell-7.5#use-powershell-in-a-container): The following command download the latest stable versions of PowerShell: - - ```console - docker pull mcr.microsoft.com/dotnet/sdk:9.0 - ``` - - To start an interactive shell of Pwsh you just need to run: - - ```console - docker run -it mcr.microsoft.com/dotnet/sdk:9.0 pwsh - ``` - -### Required PowerShell Modules - -Prowler relies on several PowerShell cmdlets to retrieve necessary data. -These cmdlets come from different modules that must be installed. - -#### Automatic Installation - -The required modules are automatically installed when running Prowler with the `--init-modules` flag. - -Example command: - -```console -python3 prowler-cli.py m365 --verbose --log-level ERROR --env-auth --init-modules -``` -If the modules are already installed, running this command will not cause issues—it will simply verify that the necessary modules are available. - -???+ note - Prowler installs the modules using `-Scope CurrentUser`. - If you encounter any issues with services not working after the automatic installation, try installing the modules manually using `-Scope AllUsers` (administrator permissions are required for this). - The command needed to install a module manually is: - ```powershell - Install-Module -Name "ModuleName" -Scope AllUsers -Force - ``` - -#### Modules Version - -- [ExchangeOnlineManagement](https://www.powershellgallery.com/packages/ExchangeOnlineManagement/3.6.0) (Minimum version: 3.6.0) Required for 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. - -[MSAL.PS](https://www.powershellgallery.com/packages/MSAL.PS/4.32.0): Required for Exchange module via application authentication. - -## GitHub - -Prowler supports multiple [authentication methods for GitHub](https://docs.github.com/en/rest/authentication/authenticating-to-the-rest-api). - -### Supported Authentication Methods - -- **Personal Access Token (PAT)** -- **OAuth App Token** -- **GitHub App Credentials** - -These options provide flexibility for scanning and analyzing your GitHub account, repositories, organizations, and applications. Choose the authentication method that best suits your security needs. - -???+ 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 or remote 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 for local scans. - -### Authentication - -- For local scans, no authentication is required. -- For remote repository scans, authentication can be provided via: - - [**GitHub Username and Personal Access Token (PAT)**](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic) - - [**GitHub OAuth App Token**](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token) - - [**Git URL**](https://git-scm.com/docs/git-clone#_git_urls) - -### 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) diff --git a/docs/img/overview.png b/docs/img/overview.png deleted file mode 100644 index a4975bb234..0000000000 Binary files a/docs/img/overview.png and /dev/null differ diff --git a/docs/index.md b/docs/index.md index 9884630f47..eae09ba70b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,3 +1,5 @@ +# What is Prowler? + **Prowler** is the open source cloud security platform trusted by thousands to **automate security and compliance** in any cloud environment. With hundreds of ready-to-use checks and compliance frameworks, Prowler delivers real-time, customizable monitoring and seamless integrations, making cloud security simple, scalable, and cost-effective for organizations of any size. The official supported providers right now are: @@ -8,790 +10,15 @@ The official supported providers right now are: - **Kubernetes** - **M365** - **Github** +- **IaC** + +Unofficially, Prowler supports: NHN. Prowler supports **auditing, incident response, continuous monitoring, hardening, forensic readiness, and remediation**. -### Prowler Components +### Products -- **Prowler CLI** (Command Line Interface) – Known as **Prowler Open Source**. -- **Prowler Cloud** – A managed service built on top of Prowler CLI. - More information: [Prowler Cloud](https://prowler.com) - -## Prowler App - -![Prowler App](img/overview.png) - -Prowler App is a web application that simplifies running Prowler. It provides: - -- A **user-friendly interface** for configuring and executing scans. -- A dashboard to **view results** and manage **security findings**. - -### Installation Guide -Refer to the [Quick Start](#prowler-app-installation) section for installation steps. - -## Prowler CLI - -```console -prowler -``` -![Prowler CLI Execution](img/short-display.png) - -## Prowler Dashboard - -```console -prowler dashboard -``` -![Prowler Dashboard](img/dashboard.png) - -Prowler includes hundreds of security controls aligned with widely recognized industry frameworks and standards, including: - -- CIS Benchmarks (AWS, Azure, Microsoft 365, Kubernetes, GitHub) -- NIST SP 800-53 (rev. 4 and 5) and NIST SP 800-171 -- NIST Cybersecurity Framework (CSF) -- CISA Guidelines -- FedRAMP Low & Moderate -- PCI DSS v3.2.1 and v4.0 -- ISO/IEC 27001:2013 and 2022 -- SOC 2 -- GDPR (General Data Protection Regulation) -- HIPAA (Health Insurance Portability and Accountability Act) -- FFIEC (Federal Financial Institutions Examination Council) -- ENS RD2022 (Spanish National Security Framework) -- GxP 21 CFR Part 11 and EU Annex 11 -- RBI Cybersecurity Framework (Reserve Bank of India) -- KISA ISMS-P (Korean Information Security Management System) -- MITRE ATT&CK -- AWS Well-Architected Framework (Security & Reliability Pillars) -- AWS Foundational Technical Review (FTR) -- Microsoft NIS2 Directive (EU) -- Custom threat scoring frameworks (prowler_threatscore) -- Custom security frameworks for enterprise needs - -## Quick Start - -### Prowler App Installation - -Prowler App supports multiple installation methods based on your environment. - -Refer to the [Prowler App Tutorial](tutorials/prowler-app.md) for detailed usage instructions. - -=== "Docker Compose" - - _Requirements_: - - * `Docker Compose` installed: https://docs.docker.com/compose/install/. - - _Commands_: - - ``` bash - curl -LO https://raw.githubusercontent.com/prowler-cloud/prowler/refs/heads/master/docker-compose.yml - curl -LO https://raw.githubusercontent.com/prowler-cloud/prowler/refs/heads/master/.env - docker compose up -d - ``` - - > Containers are built for `linux/amd64`. If your workstation's architecture is different, please set `DOCKER_DEFAULT_PLATFORM=linux/amd64` in your environment or use the `--platform linux/amd64` flag in the docker command. - - > Enjoy Prowler App at http://localhost:3000 by signing up with your email and password. - - ???+ note - You can change the environment variables in the `.env` file. Note that it is not recommended to use the default values in production environments. - - ???+ note - There is a development mode available, you can use the file https://github.com/prowler-cloud/prowler/blob/master/docker-compose-dev.yml to run the app in development mode. - - ???+ warning - Google and GitHub authentication is only available in [Prowler Cloud](https://prowler.com). - -=== "GitHub" - - _Requirements_: - - * `git` installed. - * `poetry` installed: [poetry installation](https://python-poetry.org/docs/#installation). - * `npm` installed: [npm installation](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). - * `Docker Compose` installed: https://docs.docker.com/compose/install/. - - ???+ warning - Make sure to have `api/.env` and `ui/.env.local` files with the required environment variables. You can find the required environment variables in the [`api/.env.template`](https://github.com/prowler-cloud/prowler/blob/master/api/.env.example) and [`ui/.env.template`](https://github.com/prowler-cloud/prowler/blob/master/ui/.env.template) files. - - _Commands to run the API_: - - ``` bash - git clone https://github.com/prowler-cloud/prowler \ - cd prowler/api \ - poetry install \ - eval $(poetry env activate) \ - set -a \ - source .env \ - docker compose up postgres valkey -d \ - cd src/backend \ - python manage.py migrate --database admin \ - gunicorn -c config/guniconf.py config.wsgi:application - ``` - - ???+ important - Starting from Poetry v2.0.0, `poetry shell` has been deprecated in favor of `poetry env activate`. - - If your poetry version is below 2.0.0 you must keep using `poetry shell` to activate your environment. - In case you have any doubts, consult the Poetry environment activation guide: https://python-poetry.org/docs/managing-environments/#activating-the-environment - - > Now, you can access the API documentation at http://localhost:8080/api/v1/docs. - - _Commands to run the API Worker_: - - ``` bash - git clone https://github.com/prowler-cloud/prowler \ - cd prowler/api \ - poetry install \ - eval $(poetry env activate) \ - set -a \ - source .env \ - cd src/backend \ - python -m celery -A config.celery worker -l info -E - ``` - - _Commands to run the API Scheduler_: - - ``` bash - git clone https://github.com/prowler-cloud/prowler \ - cd prowler/api \ - poetry install \ - eval $(poetry env activate) \ - set -a \ - source .env \ - cd src/backend \ - python -m celery -A config.celery beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler - ``` - - _Commands to run the UI_: - - ``` bash - git clone https://github.com/prowler-cloud/prowler \ - cd prowler/ui \ - npm install \ - npm run build \ - npm start - ``` - - > Enjoy Prowler App at http://localhost:3000 by signing up with your email and password. - - ???+ warning - Google and GitHub authentication is only available in [Prowler Cloud](https://prowler.com). - -### Prowler CLI Installation - -Prowler is available as a project in [PyPI](https://pypi.org/project/prowler/). Consequently, it can be installed as Python package with `Python >= 3.9, <= 3.12`: - -=== "pipx" - - [pipx](https://pipx.pypa.io/stable/) is a tool to install Python applications in isolated environments. It is recommended to use `pipx` for a global installation. - - _Requirements_: - - * `Python >= 3.9, <= 3.12` - * `pipx` installed: [pipx installation](https://pipx.pypa.io/stable/installation/). - * AWS, GCP, Azure and/or Kubernetes credentials - - _Commands_: - - ``` bash - pipx install prowler - prowler -v - ``` - - To upgrade Prowler to the latest version, run: - - ``` bash - pipx upgrade prowler - ``` - -=== "pip" - - ???+ warning - This method is not recommended because it will modify the environment which you choose to install. Consider using [pipx](https://docs.prowler.com/projects/prowler-open-source/en/latest/#__tabbed_1_1) for a global installation. - - _Requirements_: - - * `Python >= 3.9, <= 3.12` - * `Python pip >= 21.0.0` - * AWS, GCP, Azure, M365 and/or Kubernetes credentials - - _Commands_: - - ``` bash - pip install prowler - prowler -v - ``` - - To upgrade Prowler to the latest version, run: - - ``` bash - pip install --upgrade prowler - ``` - -=== "Docker" - - _Requirements_: - - * Have `docker` installed: https://docs.docker.com/get-docker/. - * In the command below, change `-v` to your local directory path in order to access the reports. - * AWS, GCP, Azure and/or Kubernetes credentials - - > Containers are built for `linux/amd64`. If your workstation's architecture is different, please set `DOCKER_DEFAULT_PLATFORM=linux/amd64` in your environment or use the `--platform linux/amd64` flag in the docker command. - - _Commands_: - - ``` bash - docker run -ti --rm -v /your/local/dir/prowler-output:/home/prowler/output \ - --name prowler \ - --env AWS_ACCESS_KEY_ID \ - --env AWS_SECRET_ACCESS_KEY \ - --env AWS_SESSION_TOKEN toniblyx/prowler:latest - ``` - -=== "GitHub" - - _Requirements for Developers_: - - * `git` - * `poetry` installed: [poetry installation](https://python-poetry.org/docs/#installation). - * AWS, GCP, Azure and/or Kubernetes credentials - - _Commands_: - - ``` - git clone https://github.com/prowler-cloud/prowler - cd prowler - poetry install - poetry run python prowler-cli.py -v - ``` - ???+ note - If you want to clone Prowler from Windows, use `git config core.longpaths true` to allow long file paths. - -=== "Amazon Linux 2" - - _Requirements_: - - * `Python >= 3.9, <= 3.12` - * AWS, GCP, Azure and/or Kubernetes credentials - - _Commands_: - - ``` - python3 -m pip install --user pipx - python3 -m pipx ensurepath - pipx install prowler - prowler -v - ``` - -=== "Ubuntu" - - _Requirements_: - - * `Ubuntu 23.04` or above, if you are using an older version of Ubuntu check [pipx installation](https://docs.prowler.com/projects/prowler-open-source/en/latest/#__tabbed_1_1) and ensure you have `Python >= 3.9, <= 3.12`. - * `Python >= 3.9, <= 3.12` - * AWS, GCP, Azure and/or Kubernetes credentials - - _Commands_: - - ``` bash - sudo apt update - sudo apt install pipx - pipx ensurepath - pipx install prowler - prowler -v - ``` - -=== "Brew" - - _Requirements_: - - * `Brew` installed in your Mac or Linux - * AWS, GCP, Azure and/or Kubernetes credentials - - _Commands_: - - ``` bash - brew install prowler - prowler -v - ``` - -=== "AWS CloudShell" - - After the migration of AWS CloudShell from Amazon Linux 2 to Amazon Linux 2023 [[1]](https://aws.amazon.com/about-aws/whats-new/2023/12/aws-cloudshell-migrated-al2023/) [[2]](https://docs.aws.amazon.com/cloudshell/latest/userguide/cloudshell-AL2023-migration.html), there is no longer a need to manually compile Python 3.9 as it is already included in AL2023. Prowler can thus be easily installed following the generic method of installation via pip. Follow the steps below to successfully execute Prowler v4 in AWS CloudShell: - - _Requirements_: - - * Open AWS CloudShell `bash`. - - _Commands_: - - ```bash - sudo bash - adduser prowler - su prowler - python3 -m pip install --user pipx - python3 -m pipx ensurepath - pipx install prowler - cd /tmp - prowler aws - ``` - - ???+ note - To download the results from AWS CloudShell, select Actions -> Download File and add the full path of each file. For the CSV file it will be something like `/tmp/output/prowler-output-123456789012-20221220191331.csv` - -=== "Azure CloudShell" - - _Requirements_: - - * Open Azure CloudShell `bash`. - - _Commands_: - - ```bash - python3 -m pip install --user pipx - python3 -m pipx ensurepath - pipx install prowler - cd /tmp - prowler azure --az-cli-auth - ``` - -### Prowler App Update - -You have two options to upgrade your Prowler App installation: - -#### Option 1: Change env file with the following values - -Edit your `.env` file and change the version values: - -```env -PROWLER_UI_VERSION="5.9.0" -PROWLER_API_VERSION="5.9.0" -``` - -#### Option 2: Run the following command - -```bash -docker compose pull --policy always -``` - -The `--policy always` flag ensures that Docker pulls the latest images even if they already exist locally. - - -???+ note "What Gets Preserved During Upgrade" - - Everything is preserved, nothing will be deleted after the update. - -#### Troubleshooting - -If containers don't start, check logs for errors: - -```bash -# Check logs for errors -docker compose logs - -# Verify image versions -docker images | grep prowler -``` - -If you encounter issues, you can rollback to the previous version by changing the `.env` file back to your previous version and running: - -```bash -docker compose pull -docker compose up -d -``` - -## Prowler container versions - -The available versions of Prowler CLI are the following: - -- `latest`: in sync with `master` branch (please note that it is not a stable version) -- `v4-latest`: in sync with `v4` branch (please note that it is not a stable version) -- `v3-latest`: in sync with `v3` branch (please note that it is not a stable version) -- `` (release): you can find the releases [here](https://github.com/prowler-cloud/prowler/releases), those are stable releases. -- `stable`: this tag always point to the latest release. -- `v4-stable`: this tag always point to the latest release for v4. -- `v3-stable`: this tag always point to the latest release for v3. - -The container images are available here: - -- Prowler CLI: - - - [DockerHub](https://hub.docker.com/r/toniblyx/prowler/tags) - - [AWS Public ECR](https://gallery.ecr.aws/prowler-cloud/prowler) - -- Prowler App: - - - [DockerHub - Prowler UI](https://hub.docker.com/r/prowlercloud/prowler-ui/tags) - - [DockerHub - Prowler API](https://hub.docker.com/r/prowlercloud/prowler-api/tags) - -## High level architecture - -You can run Prowler from your workstation, a Kubernetes Job, a Google Compute Engine, an Azure VM, an EC2 instance, Fargate or any other container, CloudShell and many more. - -![Architecture](img/architecture.png) - -### Prowler App - -The **Prowler App** consists of three main components: - -- **Prowler UI**: A user-friendly web interface for running Prowler and viewing results, powered by Next.js. -- **Prowler API**: The backend API that executes Prowler scans and stores the results, built with Django REST Framework. -- **Prowler SDK**: A Python SDK that integrates with Prowler CLI for advanced functionality. - -The app leverages the following supporting infrastructure: - -- **PostgreSQL**: Used for persistent storage of scan results. -- **Celery Workers**: Facilitate asynchronous execution of Prowler scans. -- **Valkey**: An in-memory database serving as a message broker for the Celery workers. - -![Prowler App Architecture](img/prowler-app-architecture.png) - -## Deprecations from v3 - -The following are the deprecations carried out from v3. - -### General - -- `Allowlist` now is called `Mutelist`. -- The `--quiet` option has been deprecated. From now on use the `--status` flag to select the finding's status you want to get: PASS, FAIL or MANUAL. -- All `INFO` finding's status has changed to `MANUAL`. -- The CSV output format is common for all providers. - -Some output formats are now deprecated: - -- The native JSON is replaced for the JSON [OCSF](https://schema.ocsf.io/) v1.1.0, common for all the providers. - -### AWS -- Deprecate the AWS flag `--sts-endpoint-region` since AWS STS regional tokens are used. -- To send only FAILS to AWS Security Hub, now you must use either `--send-sh-only-fails` or `--security-hub --status FAIL`. - -## Basic Usage - -### Prowler App - -#### **Access the App** - -Go to [http://localhost:3000](http://localhost:3000) after installing the app (see [Quick Start](#prowler-app-installation)). Sign up with your email and password. - -Sign Up Button -Sign Up - -???+ note "User creation and default tenant behavior" - - When creating a new user, the behavior depends on whether an invitation is provided: - - - **Without an invitation**: - - - A new tenant is automatically created. - - The new user is assigned to this tenant. - - A set of **RBAC admin permissions** is generated and assigned to the user for the newly-created tenant. - - - **With an invitation**: The user is added to the specified tenant with the permissions defined in the invitation. - - This mechanism ensures that the first user in a newly created tenant has administrative permissions within that tenant. - -#### Log In - -Log in using your **email and password** to access the Prowler App. - -Log In - -#### Add a Cloud Provider - -To configure a cloud provider for scanning: - -1. Navigate to `Settings > Cloud Providers` and click `Add Account`. -2. Select the cloud provider you wish to scan (**AWS, GCP, Azure, Kubernetes**). -3. Enter the provider's identifier (Optional: Add an alias): - - **AWS**: Account ID - - **GCP**: Project ID - - **Azure**: Subscription ID - - **Kubernetes**: Cluster ID - - **M36**: Domain ID -4. Follow the guided instructions to add and authenticate your credentials. - -#### Start a Scan - -Once credentials are successfully added and validated, Prowler initiates a scan of your cloud environment. - -Click `Go to Scans` to monitor progress. - -#### View Results - -While the scan is running, you can review findings in the following sections: - -- **Overview** – Provides a high-level summary of your scans. - Overview - -- **Compliance** – Displays compliance insights based on security frameworks. - Compliance - -> For detailed usage instructions, refer to the [Prowler App Guide](tutorials/prowler-app.md). - -???+ note - Prowler will automatically scan all configured providers every **24 hours**, ensuring your cloud environment stays continuously monitored. - -### Prowler CLI - -#### Running Prowler - -To run Prowler, you will need to specify the provider (e.g `aws`, `gcp`, `azure`, `m365`, `github` or `kubernetes`): - -???+ note - If no provider is specified, AWS is used by default for backward compatibility with Prowler v2. - -```console -prowler -``` -![Prowler Execution](img/short-display.png) - -???+ note - Running the `prowler` command without options will uses environment variable credentials. Refer to the [Requirements](./getting-started/requirements.md) section for credential configuration details. - -#### Verbose Output - -If you prefer the former verbose output, use: `--verbose`. This allows seeing more info while Prowler is running, minimal output is displayed unless verbosity is enabled. - -#### Report Generation - -By default, Prowler generates CSV, JSON-OCSF, and HTML reports. To generate a JSON-ASFF report (used by AWS Security Hub), specify `-M` or `--output-modes`: - -```console -prowler -M csv json-asff json-ocsf html -``` -The HTML report is saved in the output directory, alongside other reports. It will look like this: - -![Prowler Execution](img/html-output.png) - -#### Listing Available Checks and Services - -To view all available checks or services within a provider:, use `-l`/`--list-checks` or `--list-services`. - -```console -prowler --list-checks -prowler --list-services -``` -#### Running Specific Checks or Services - -Execute specific checks or services using `-c`/`checks` or `-s`/`services`: - -```console -prowler azure --checks storage_blob_public_access_level_is_disabled -prowler aws --services s3 ec2 -prowler gcp --services iam compute -prowler kubernetes --services etcd apiserver -``` -#### Excluding Checks and Services - -Checks and services can be excluded with `-e`/`--excluded-checks` or `--excluded-services`: - -```console -prowler aws --excluded-checks s3_bucket_public_access -prowler azure --excluded-services defender iam -prowler gcp --excluded-services kms -prowler kubernetes --excluded-services controllermanager -``` -#### Additional Options - -Explore more advanced time-saving execution methods in the [Miscellaneous](tutorials/misc.md) section. - -To access the help menu and view all available options, use: `-h`/`--help`: - -```console -prowler --help -``` - -#### AWS - -Use a custom AWS profile with `-p`/`--profile` and/or the AWS regions you want to audit with `-f`/`--filter-region`: - -```console -prowler aws --profile custom-profile -f us-east-1 eu-south-2 -``` - -???+ note - By default, `prowler` will scan all AWS regions. - -See more details about AWS Authentication in the [Requirements](getting-started/requirements.md#aws) section. - -#### Azure - -Azure requires specifying the auth method: - -```console -# To use service principal authentication -prowler azure --sp-env-auth - -# To use az cli authentication -prowler azure --az-cli-auth - -# To use browser authentication -prowler azure --browser-auth --tenant-id "XXXXXXXX" - -# To use managed identity auth -prowler azure --managed-identity-auth -``` - -See more details about Azure Authentication in [Requirements](getting-started/requirements.md#azure) - -By default, Prowler scans all the subscriptions for which it has permissions. To scan a single or various specific subscription you can use the following flag (using az cli auth as example): - -```console -prowler azure --az-cli-auth --subscription-ids ... -``` -#### Google Cloud - -- **User Account Credentials** - - By default, Prowler uses **User Account credentials**. You can configure your account using: - - - `gcloud init` – Set up a new account. - - `gcloud config set account ` – Switch to an existing account. - - Once configured, obtain access credentials using: `gcloud auth application-default login`. - -- **Service Account Authentication** - - Alternatively, you can use Service Account credentials: - - Generate and download Service Account keys in JSON format. Refer to [Google IAM documentation](https://cloud.google.com/iam/docs/creating-managing-service-account-keys) for details. - - Provide the key file location using this argument: - - ```console - prowler gcp --credentials-file path - ``` - -- **Scanning Specific GCP Projects** - - By default, Prowler scans all accessible GCP projects. To scan specific projects, use the `--project-ids` flag: - - ```console - prowler gcp --project-ids ... - ``` - -- **GCP Retry Configuration** - - To configure the maximum number of retry attempts for Google Cloud SDK API calls, use the `--gcp-retries-max-attempts` flag: - - ```console - prowler gcp --gcp-retries-max-attempts 5 - ``` - - This is useful when experiencing quota exceeded errors (HTTP 429) to increase the number of automatic retry attempts. - -#### Kubernetes - -Prowler enables security scanning of Kubernetes clusters, supporting both **in-cluster** and **external** execution. - -- **Non In-Cluster Execution** - - ```console - prowler kubernetes --kubeconfig-file path - ``` - ???+ note - If no `--kubeconfig-file` is provided, Prowler will use the default KubeConfig file location (`~/.kube/config`). - -- **In-Cluster Execution** - - To run Prowler inside the cluster, apply the provided YAML configuration to deploy a job in a new namespace: - - ```console - kubectl apply -f kubernetes/prowler-sa.yaml - kubectl apply -f kubernetes/job.yaml - kubectl apply -f kubernetes/prowler-role.yaml - kubectl apply -f kubernetes/prowler-rolebinding.yaml - kubectl get pods --namespace prowler-ns --> prowler-XXXXX - kubectl logs prowler-XXXXX --namespace prowler-ns - ``` - - ???+ note - By default, Prowler scans all namespaces in the active Kubernetes context. Use the `--context`flag to specify the context to be scanned and `--namespaces` to restrict scanning to specific namespaces. - -#### Microsoft 365 - -Microsoft 365 requires specifying the auth method: - -```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 az cli authentication -prowler m365 --az-cli-auth - -# To use browser authentication -prowler m365 --browser-auth --tenant-id "XXXXXXXX" - -``` - -See more details about M365 Authentication in the [Requirements](getting-started/requirements.md#microsoft-365) section. - -#### GitHub - -Prowler enables security scanning of your **GitHub account**, including **Repositories**, **Organizations** and **Applications**. - -- **Supported Authentication Methods** - - Authenticate using one of the following methods: - - ```console - # Personal Access Token (PAT): - prowler github --personal-access-token pat - - # OAuth App Token: - prowler github --oauth-app-token oauth_token - - # GitHub App Credentials: - prowler github --github-app-id app_id --github-app-key app_key - ``` - - ???+ note - 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` - 3. `GITHUB_APP_ID` and `GITHUB_APP_KEY` - -#### Infrastructure as Code (IaC) - -Prowler's Infrastructure as Code (IaC) provider enables you to scan local or remote 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 - -# Scan a remote GitHub repository (public or private) -prowler iac --scan-repository-url https://github.com/user/repo.git - -# Authenticate to a private repo with GitHub username and PAT -prowler iac --scan-repository-url https://github.com/user/repo.git \ - --github-username --personal-access-token - -# Authenticate to a private repo with OAuth App Token -prowler iac --scan-repository-url https://github.com/user/repo.git \ - --oauth-app-token - -# 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 - - `--scan-path` and `--scan-repository-url` are mutually exclusive; only one can be specified at a time. - - For remote repository scans, authentication can be provided via CLI flags or environment variables (`GITHUB_OAUTH_APP_TOKEN`, `GITHUB_USERNAME`, `GITHUB_PERSONAL_ACCESS_TOKEN`). CLI flags take precedence. - - The IaC provider does not require cloud authentication for local scans. - - 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**, refer to the [official repository](https://github.com/prowler-cloud/prowler/blob/8818f47333a0c1c1a457453c87af0ea5b89a385f/README.md). +- **Prowler CLI** (Command Line Interface) +- **Prowler App** (Web Application) +- [**Prowler Cloud**](https://cloud.prowler.com) – A managed service built on top of Prowler App. +- [**Prowler Hub**](https://hub.prowler.com) – A public library of versioned checks, cloud service artifacts, and compliance frameworks. diff --git a/docs/installation/prowler-app.md b/docs/installation/prowler-app.md new file mode 100644 index 0000000000..d8b03427cf --- /dev/null +++ b/docs/installation/prowler-app.md @@ -0,0 +1,173 @@ +### Installation + +Prowler App supports multiple installation methods based on your environment. + +Refer to the [Prowler App Tutorial](../tutorials/prowler-app.md) for detailed usage instructions. + +=== "Docker Compose" + + _Requirements_: + + * `Docker Compose` installed: https://docs.docker.com/compose/install/. + + _Commands_: + + ``` bash + curl -LO https://raw.githubusercontent.com/prowler-cloud/prowler/refs/heads/master/docker-compose.yml + curl -LO https://raw.githubusercontent.com/prowler-cloud/prowler/refs/heads/master/.env + docker compose up -d + ``` + + > Containers are built for `linux/amd64`. If your workstation's architecture is different, please set `DOCKER_DEFAULT_PLATFORM=linux/amd64` in your environment or use the `--platform linux/amd64` flag in the docker command. + + > Enjoy Prowler App at http://localhost:3000 by signing up with your email and password. + + ???+ note + You can change the environment variables in the `.env` file. Note that it is not recommended to use the default values in production environments. + + ???+ note + There is a development mode available, you can use the file https://github.com/prowler-cloud/prowler/blob/master/docker-compose-dev.yml to run the app in development mode. + + ???+ warning + Google and GitHub authentication is only available in [Prowler Cloud](https://prowler.com). + +=== "GitHub" + + _Requirements_: + + * `git` installed. + * `poetry` installed: [poetry installation](https://python-poetry.org/docs/#installation). + * `npm` installed: [npm installation](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). + * `Docker Compose` installed: https://docs.docker.com/compose/install/. + + ???+ warning + Make sure to have `api/.env` and `ui/.env.local` files with the required environment variables. You can find the required environment variables in the [`api/.env.template`](https://github.com/prowler-cloud/prowler/blob/master/api/.env.example) and [`ui/.env.template`](https://github.com/prowler-cloud/prowler/blob/master/ui/.env.template) files. + + _Commands to run the API_: + + ``` bash + git clone https://github.com/prowler-cloud/prowler \ + cd prowler/api \ + poetry install \ + eval $(poetry env activate) \ + set -a \ + source .env \ + docker compose up postgres valkey -d \ + cd src/backend \ + python manage.py migrate --database admin \ + gunicorn -c config/guniconf.py config.wsgi:application + ``` + + ???+ important + Starting from Poetry v2.0.0, `poetry shell` has been deprecated in favor of `poetry env activate`. + + If your poetry version is below 2.0.0 you must keep using `poetry shell` to activate your environment. + In case you have any doubts, consult the Poetry environment activation guide: https://python-poetry.org/docs/managing-environments/#activating-the-environment + + > Now, you can access the API documentation at http://localhost:8080/api/v1/docs. + + _Commands to run the API Worker_: + + ``` bash + git clone https://github.com/prowler-cloud/prowler \ + cd prowler/api \ + poetry install \ + eval $(poetry env activate) \ + set -a \ + source .env \ + cd src/backend \ + python -m celery -A config.celery worker -l info -E + ``` + + _Commands to run the API Scheduler_: + + ``` bash + git clone https://github.com/prowler-cloud/prowler \ + cd prowler/api \ + poetry install \ + eval $(poetry env activate) \ + set -a \ + source .env \ + cd src/backend \ + python -m celery -A config.celery beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler + ``` + + _Commands to run the UI_: + + ``` bash + git clone https://github.com/prowler-cloud/prowler \ + cd prowler/ui \ + npm install \ + npm run build \ + npm start + ``` + + > Enjoy Prowler App at http://localhost:3000 by signing up with your email and password. + + ???+ warning + Google and GitHub authentication is only available in [Prowler Cloud](https://prowler.com). + + +### Update Prowler App + +Upgrade Prowler App installation using one of two options: + +#### Option 1: Update Environment File + +Edit the `.env` file and change version values: + +```env +PROWLER_UI_VERSION="5.9.0" +PROWLER_API_VERSION="5.9.0" +``` + +#### Option 2: Use Docker Compose Pull + +```bash +docker compose pull --policy always +``` + +The `--policy always` flag ensures that Docker pulls the latest images even if they already exist locally. + + +???+ note "What Gets Preserved During Upgrade" + Everything is preserved, nothing will be deleted after the update. + +### Troubleshooting + +If containers don't start, check logs for errors: + +```bash +# Check logs for errors +docker compose logs + +# Verify image versions +docker images | grep prowler +``` + +If you encounter issues, you can rollback to the previous version by changing the `.env` file back to your previous version and running: + +```bash +docker compose pull +docker compose up -d +``` + + +### Container versions + +The available versions of Prowler CLI are the following: + +- `latest`: in sync with `master` branch (please note that it is not a stable version) +- `v4-latest`: in sync with `v4` branch (please note that it is not a stable version) +- `v3-latest`: in sync with `v3` branch (please note that it is not a stable version) +- `` (release): you can find the releases [here](https://github.com/prowler-cloud/prowler/releases), those are stable releases. +- `stable`: this tag always point to the latest release. +- `v4-stable`: this tag always point to the latest release for v4. +- `v3-stable`: this tag always point to the latest release for v3. + +The container images are available here: + +- Prowler App: + + - [DockerHub - Prowler UI](https://hub.docker.com/r/prowlercloud/prowler-ui/tags) + - [DockerHub - Prowler API](https://hub.docker.com/r/prowlercloud/prowler-api/tags) diff --git a/docs/installation/prowler-cli.md b/docs/installation/prowler-cli.md new file mode 100644 index 0000000000..663b6bebaf --- /dev/null +++ b/docs/installation/prowler-cli.md @@ -0,0 +1,208 @@ + +## Installation + +Prowler is available as a project in [PyPI](https://pypi.org/project/prowler/). Install it as a Python package with `Python >= 3.9, <= 3.12`: + +=== "pipx" + + [pipx](https://pipx.pypa.io/stable/) installs Python applications in isolated environments. Use `pipx` for global installation. + + _Requirements_: + + * `Python >= 3.9, <= 3.12` + * `pipx` installed: [pipx installation](https://pipx.pypa.io/stable/installation/). + * AWS, GCP, Azure and/or Kubernetes credentials + + _Commands_: + + ``` bash + pipx install prowler + prowler -v + ``` + + Upgrade Prowler to the latest version: + + ``` bash + pipx upgrade prowler + ``` + +=== "pip" + + ???+ warning + This method modifies the chosen installation environment. Consider using [pipx](https://docs.prowler.com/projects/prowler-open-source/en/latest/#__tabbed_1_1) for global installation. + + _Requirements_: + + * `Python >= 3.9, <= 3.12` + * `Python pip >= 21.0.0` + * AWS, GCP, Azure, M365 and/or Kubernetes credentials + + _Commands_: + + ``` bash + pip install prowler + prowler -v + ``` + + Upgrade Prowler to the latest version: + + ``` bash + pip install --upgrade prowler + ``` + +=== "Docker" + + _Requirements_: + + * Have `docker` installed: https://docs.docker.com/get-docker/. + * In the command below, change `-v` to your local directory path in order to access the reports. + * AWS, GCP, Azure and/or Kubernetes credentials + + > Containers are built for `linux/amd64`. If your workstation's architecture is different, please set `DOCKER_DEFAULT_PLATFORM=linux/amd64` in your environment or use the `--platform linux/amd64` flag in the docker command. + + _Commands_: + + ``` bash + docker run -ti --rm -v /your/local/dir/prowler-output:/home/prowler/output \ + --name prowler \ + --env AWS_ACCESS_KEY_ID \ + --env AWS_SECRET_ACCESS_KEY \ + --env AWS_SESSION_TOKEN toniblyx/prowler:latest + ``` + +=== "GitHub" + + _Requirements for Developers_: + + * `git` + * `poetry` installed: [poetry installation](https://python-poetry.org/docs/#installation). + * AWS, GCP, Azure and/or Kubernetes credentials + + _Commands_: + + ``` + git clone https://github.com/prowler-cloud/prowler + cd prowler + poetry install + poetry run python prowler-cli.py -v + ``` + ???+ note + If you want to clone Prowler from Windows, use `git config core.longpaths true` to allow long file paths. + +=== "Amazon Linux 2" + + _Requirements_: + + * `Python >= 3.9, <= 3.12` + * AWS, GCP, Azure and/or Kubernetes credentials + + _Commands_: + + ``` + python3 -m pip install --user pipx + python3 -m pipx ensurepath + pipx install prowler + prowler -v + ``` + +=== "Ubuntu" + + _Requirements_: + + * `Ubuntu 23.04` or above, if you are using an older version of Ubuntu check [pipx installation](https://docs.prowler.com/projects/prowler-open-source/en/latest/#__tabbed_1_1) and ensure you have `Python >= 3.9, <= 3.12`. + * `Python >= 3.9, <= 3.12` + * AWS, GCP, Azure and/or Kubernetes credentials + + _Commands_: + + ``` bash + sudo apt update + sudo apt install pipx + pipx ensurepath + pipx install prowler + prowler -v + ``` + +=== "Brew" + + _Requirements_: + + * `Brew` installed in your Mac or Linux + * AWS, GCP, Azure and/or Kubernetes credentials + + _Commands_: + + ``` bash + brew install prowler + prowler -v + ``` + +=== "AWS CloudShell" + + After the migration of AWS CloudShell from Amazon Linux 2 to Amazon Linux 2023 [[1]](https://aws.amazon.com/about-aws/whats-new/2023/12/aws-cloudshell-migrated-al2023/) [[2]](https://docs.aws.amazon.com/cloudshell/latest/userguide/cloudshell-AL2023-migration.html), there is no longer a need to manually compile Python 3.9 as it is already included in AL2023. Prowler can thus be easily installed following the generic method of installation via pip. Follow the steps below to successfully execute Prowler v4 in AWS CloudShell: + + _Requirements_: + + * Open AWS CloudShell `bash`. + + _Commands_: + + ```bash + sudo bash + adduser prowler + su prowler + python3 -m pip install --user pipx + python3 -m pipx ensurepath + pipx install prowler + cd /tmp + prowler aws + ``` + + ???+ note + To download the results from AWS CloudShell, select Actions -> Download File and add the full path of each file. For the CSV file it will be something like `/tmp/output/prowler-output-123456789012-20221220191331.csv` + +=== "Azure CloudShell" + + _Requirements_: + + * Open Azure CloudShell `bash`. + + _Commands_: + + ```bash + python3 -m pip install --user pipx + python3 -m pipx ensurepath + pipx install prowler + cd /tmp + prowler azure --az-cli-auth + ``` + + + + + + + + + + + + +## Container versions + +The available versions of Prowler CLI are the following: + +- `latest`: in sync with `master` branch (please note that it is not a stable version) +- `v4-latest`: in sync with `v4` branch (please note that it is not a stable version) +- `v3-latest`: in sync with `v3` branch (please note that it is not a stable version) +- `` (release): you can find the releases [here](https://github.com/prowler-cloud/prowler/releases), those are stable releases. +- `stable`: this tag always point to the latest release. +- `v4-stable`: this tag always point to the latest release for v4. +- `v3-stable`: this tag always point to the latest release for v3. + +The container images are available here: + +- Prowler CLI: + + - [DockerHub](https://hub.docker.com/r/toniblyx/prowler/tags) + - [AWS Public ECR](https://gallery.ecr.aws/prowler-cloud/prowler) diff --git a/docs/img/dashboard.png b/docs/products/img/dashboard.png similarity index 100% rename from docs/img/dashboard.png rename to docs/products/img/dashboard.png diff --git a/docs/products/img/overview.png b/docs/products/img/overview.png new file mode 100644 index 0000000000..724ffc9588 Binary files /dev/null and b/docs/products/img/overview.png differ diff --git a/docs/img/prowler-app-architecture.png b/docs/products/img/prowler-app-architecture.png similarity index 100% rename from docs/img/prowler-app-architecture.png rename to docs/products/img/prowler-app-architecture.png diff --git a/docs/products/prowler-app.md b/docs/products/prowler-app.md new file mode 100644 index 0000000000..fb2eb1686b --- /dev/null +++ b/docs/products/prowler-app.md @@ -0,0 +1,22 @@ +Prowler App is a web application that simplifies running Prowler. It provides: + +- **User-friendly interface** for configuring and executing scans +- Dashboard to **view results** and manage **security findings** + +![Prowler App](img/overview.png) + +## Components + +Prowler App consists of three main components: + +- **Prowler UI**: User-friendly web interface for running Prowler and viewing results, powered by Next.js +- **Prowler API**: Backend API that executes Prowler scans and stores results, built with Django REST Framework +- **Prowler SDK**: Python SDK that integrates with Prowler CLI for advanced functionality + +Supporting infrastructure includes: + +- **PostgreSQL**: Persistent storage of scan results +- **Celery Workers**: Asynchronous execution of Prowler scans +- **Valkey**: In-memory database serving as message broker for Celery workers + +![Prowler App Architecture](img/prowler-app-architecture.png) diff --git a/docs/products/prowler-cli.md b/docs/products/prowler-cli.md new file mode 100644 index 0000000000..633827b01e --- /dev/null +++ b/docs/products/prowler-cli.md @@ -0,0 +1,37 @@ +Prowler CLI is a command-line interface for running Prowler scans from the terminal. + +```console +prowler +``` +![Prowler CLI Execution](../img/short-display.png) + +## Prowler Dashboard + +```console +prowler dashboard +``` +![Prowler Dashboard](img/dashboard.png) + +Prowler includes hundreds of security controls aligned with widely recognized industry frameworks and standards, including: + +- CIS Benchmarks (AWS, Azure, Microsoft 365, Kubernetes, GitHub) +- NIST SP 800-53 (rev. 4 and 5) and NIST SP 800-171 +- NIST Cybersecurity Framework (CSF) +- CISA Guidelines +- FedRAMP Low & Moderate +- PCI DSS v3.2.1 and v4.0 +- ISO/IEC 27001:2013 and 2022 +- SOC 2 +- GDPR (General Data Protection Regulation) +- HIPAA (Health Insurance Portability and Accountability Act) +- FFIEC (Federal Financial Institutions Examination Council) +- ENS RD2022 (Spanish National Security Framework) +- GxP 21 CFR Part 11 and EU Annex 11 +- RBI Cybersecurity Framework (Reserve Bank of India) +- KISA ISMS-P (Korean Information Security Management System) +- MITRE ATT&CK +- AWS Well-Architected Framework (Security & Reliability Pillars) +- AWS Foundational Technical Review (FTR) +- Microsoft NIS2 Directive (EU) +- Custom threat scoring frameworks (prowler_threatscore) +- Custom security frameworks for enterprise needs diff --git a/docs/security.md b/docs/security.md index 207b565378..9e7e4bd0ea 100644 --- a/docs/security.md +++ b/docs/security.md @@ -1,24 +1,87 @@ # Security +## Compliance and Trust +We publish our live SOC 2 Type 2 Compliance data at [https://trust.prowler.com](https://trust.prowler.com) + +As an **AWS Partner**, we have passed the [AWS Foundation Technical Review (FTR)](https://aws.amazon.com/partners/foundational-technical-review/). + + +## Encryption (Prowler Cloud) + +We use encryption everywhere possible. The data and communications used by **Prowler Cloud** are **encrypted at-rest** and **in-transit**. + +## Data Retention Policy (Prowler Cloud) + +Prowler Cloud is GDPR compliant in regards to personal data and the ["right to be forgotten"](https://gdpr.eu/right-to-be-forgotten/). When a user deletes their account their user information will be deleted from Prowler Cloud online and backup systems within 10 calendar days. + ## Software Security -As an **AWS Partner** and we have passed the [AWS Foundation Technical Review (FTR)](https://aws.amazon.com/partners/foundational-technical-review/) and we use the following tools and automation to make sure our code is secure and dependencies up-to-dated: +We follow a **security-by-design approach** throughout our software development lifecycle. All changes go through automated checks at every stage, from local development to production deployment. -- `bandit` for code security review. -- `safety` and `dependabot` for dependencies. -- `hadolint` and `dockle` for our containers security. -- `snyk` in Docker Hub. -- `clair` in Amazon ECR. -- `vulture`, `flake8`, `black` and `pylint` for formatting and best practices. +We enforce [pre-commit](https://github.com/prowler-cloud/prowler/blob/master/.pre-commit-config.yaml) validations to catch issues early, and [our CI/CD pipelines](https://github.com/prowler-cloud/prowler/tree/master/.github) include multiple security gates to ensure code quality, secure configurations, and compliance with internal standards. -## Reporting Vulnerabilities +Our container registries are continuously scanned for vulnerabilities, with findings automatically reported to our security team for assessment and remediation. This process evolves alongside our stack as we adopt new languages, frameworks, and technologies, ensuring our security practices remain comprehensive, proactive, and adaptable. -If you would like to report a vulnerability or have a security concern regarding Prowler Open Source or Prowler Cloud service, please submit the information by contacting to us via [**support.prowler.com**](http://support.prowler.com). +## Reporting Vulnerabilities -The information you share with the Prowler team as part of this process is kept confidential within Prowler. We will only share this information with a third party if the vulnerability you report is found to affect a third-party product, in which case we will share this information with the third-party product's author or manufacturer. Otherwise, we will only share this information as permitted by you. +At Prowler, we consider the security of our open source software and systems a top priority. But no matter how much effort we put into system security, there can still be vulnerabilities present. -We will review the submitted report, and assign it a tracking number. We will then respond to you, acknowledging receipt of the report, and outline the next steps in the process. +If you discover a vulnerability, we would like to know about it so we can take steps to address it as quickly as possible. We would like to ask you to help us better protect our users, our clients and our systems. -You will receive a non-automated response to your initial contact within 24 hours, confirming receipt of your reported vulnerability. +When reporting vulnerabilities, please consider (1) attack scenario / exploitability, and (2) the security impact of the bug. The following issues are considered out of scope: -We will coordinate public notification of any validated vulnerability with you. Where possible, we prefer that our respective public disclosures be posted simultaneously. +- Social engineering support or attacks requiring social engineering. +- Clickjacking on pages with no sensitive actions. +- Cross-Site Request Forgery (CSRF) on unauthenticated forms or forms with no sensitive actions. +- Attacks requiring Man-In-The-Middle (MITM) or physical access to a user's device. +- Previously known vulnerable libraries without a working Proof of Concept (PoC). +- Comma Separated Values (CSV) injection without demonstrating a vulnerability. +- Missing best practices in SSL/TLS configuration. +- Any activity that could lead to the disruption of service (DoS). +- Rate limiting or brute force issues on non-authentication endpoints. +- Missing best practices in Content Security Policy (CSP). +- Missing HttpOnly or Secure flags on cookies. +- Configuration of or missing security headers. +- Missing email best practices, such as invalid, incomplete, or missing SPF/DKIM/DMARC records. +- Vulnerabilities only affecting users of outdated or unpatched browsers (less than two stable versions behind). +- Software version disclosure, banner identification issues, or descriptive error messages. +- Tabnabbing. +- Issues that require unlikely user interaction. +- Improper logout functionality and improper session timeout. +- CORS misconfiguration without an exploitation scenario. +- Broken link hijacking. +- Automated scanning results (e.g., sqlmap, Burp active scanner) that have not been manually verified. +- Content spoofing and text injection issues without a clear attack vector. +- Email spoofing without exploiting security flaws. +- Dead links or broken links. +- User enumeration. + +Testing guidelines: + +- Do not run automated scanners on other customer projects. Running automated scanners can run up costs for our users. Aggressively configured scanners might inadvertently disrupt services, exploit vulnerabilities, lead to system instability or breaches and violate Terms of Service from our upstream providers. Our own security systems won't be able to distinguish hostile reconnaissance from whitehat research. If you wish to run an automated scanner, notify us at support@prowler.com and only run it on your own Prowler app project. Do NOT attack Prowler in usage of other customers. +- Do not take advantage of the vulnerability or problem you have discovered, for example by downloading more data than necessary to demonstrate the vulnerability or deleting or modifying other people's data. + +Reporting guidelines: + +- File a report through our Support Desk at https://support.prowler.com +- If it is about a lack of a security functionality, please file a feature request instead at https://github.com/prowler-cloud/prowler/issues +- Do provide sufficient information to reproduce the problem, so we will be able to resolve it as quickly as possible. +- If you have further questions and want direct interaction with the Prowler team, please contact us at via our Community Slack at goto.prowler.com/slack. + +Disclosure guidelines: + +- In order to protect our users and customers, do not reveal the problem to others until we have researched, addressed and informed our affected customers. +- If you want to publicly share your research about Prowler at a conference, in a blog or any other public forum, you should share a draft with us for review and approval at least 30 days prior to the publication date. Please note that the following should not be included: + - Data regarding any Prowler user or customer projects. + - Prowler customers' data. + - Information about Prowler employees, contractors or partners. + +What we promise: + +- We will respond to your report within 5 business days with our evaluation of the report and an expected resolution date. +- If you have followed the instructions above, we will not take any legal action against you in regard to the report. +- We will handle your report with strict confidentiality, and not pass on your personal details to third parties without your permission. +- We will keep you informed of the progress towards resolving the problem. +- In the public information concerning the problem reported, we will give your name as the discoverer of the problem (unless you desire otherwise). + +We strive to resolve all problems as quickly as possible, and we would like to play an active role in the ultimate publication on the problem after it is resolved. diff --git a/docs/tutorials/aws/authentication.md b/docs/tutorials/aws/authentication.md index 7be6aa479e..dbfc97eaa7 100644 --- a/docs/tutorials/aws/authentication.md +++ b/docs/tutorials/aws/authentication.md @@ -1,6 +1,17 @@ # AWS Authentication in Prowler -Proper authentication is required for Prowler to perform security checks across AWS resources. Ensure that AWS-CLI is correctly configured or manually declare AWS credentials before running scans. +Prowler requires AWS credentials to function properly. Authentication is available through the following methods: + +## Required Permissions +To ensure full functionality, attach the following AWS managed policies to the designated user or role: + +- `arn:aws:iam::aws:policy/SecurityAudit` +- `arn:aws:iam::aws:policy/job-function/ViewOnlyAccess` + +### Additional Permissions + +For certain checks, additional read-only permissions are required. Attach the following custom policy to your role: [prowler-additions-policy.json](https://github.com/prowler-cloud/prowler/blob/master/permissions/prowler-additions-policy.json) + ## Configure AWS Credentials @@ -18,24 +29,13 @@ export AWS_SECRET_ACCESS_KEY="XXXXXXXXX" export AWS_SESSION_TOKEN="XXXXXXXXX" ``` -These credentials must be associated with a user or role with the necessary permissions to perform security checks. +These credentials must be associated with a user or role with the necessary permissions to perform security checks. -## Assign Required AWS Permissions -To ensure full functionality, attach the following AWS managed policies to the designated user or role: -- `arn:aws:iam::aws:policy/SecurityAudit` -- `arn:aws:iam::aws:policy/job-function/ViewOnlyAccess` -???+ note - Some security checks require read-only additional permissions. Attach the following custom policies to the role: [prowler-additions-policy.json](https://github.com/prowler-cloud/prowler/blob/master/permissions/prowler-additions-policy.json). If you want Prowler to send findings to [AWS Security Hub](https://aws.amazon.com/security-hub), make sure to also attach the custom policy: [prowler-security-hub.json](https://github.com/prowler-cloud/prowler/blob/master/permissions/prowler-security-hub.json). +## AWS Profiles -## AWS Profiles and Service Scanning in Prowler - -Prowler supports authentication and security assessments using custom AWS profiles and can optionally scan unused services. - -**Using Custom AWS Profiles** - -Prowler allows you to specify a custom AWS profile using the following command: +Specify a custom AWS profile using the following command: ```console prowler aws -p/--profile @@ -43,7 +43,7 @@ prowler aws -p/--profile ## Multi-Factor Authentication (MFA) -If MFA enforcement is required for your IAM entity, you can use `--mfa`. Prowler will prompt you to enter the following in order to get a new session: +For IAM entities requiring Multi-Factor Authentication (MFA), use the `--mfa` flag. Prowler prompts for the following values to initiate a new session: -- ARN of your MFA device -- TOTP (Time-Based One-Time Password) +- **ARN of your MFA device** +- **TOTP (Time-Based One-Time Password)** diff --git a/docs/tutorials/aws/getting-started-aws.md b/docs/tutorials/aws/getting-started-aws.md index 8414591c67..88ecea3e95 100644 --- a/docs/tutorials/aws/getting-started-aws.md +++ b/docs/tutorials/aws/getting-started-aws.md @@ -97,6 +97,9 @@ This method grants permanent access and is the recommended setup for production ![External ID](./img/prowler-cloud-external-id.png) ![Stack Data](./img/fill-stack-data.png) + !!! info + An **External ID** is required when assuming the *ProwlerScan* role to comply with AWS [confused deputy prevention](https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html). + 6. Acknowledge the IAM resource creation warning and proceed ![Stack Creation Second Step](./img/stack-creation-second-step.png) diff --git a/docs/tutorials/aws/organizations.md b/docs/tutorials/aws/organizations.md index f29baa761a..4a5b9a50ae 100644 --- a/docs/tutorials/aws/organizations.md +++ b/docs/tutorials/aws/organizations.md @@ -1,5 +1,13 @@ # AWS Organizations in Prowler +Prowler can integrate with AWS Organizations to manage the visibility and onboarding of accounts centrally. + +When trusted access is enabled with the Organization, Prowler can discover accounts as they are created and even automate deployment of the Prowler Scan IAM Role. + +> ℹ️ Trusted access can be enabled in the Management Account from the AWS Console under **AWS Organizations → Settings → Trusted access for AWS CloudFormation StackSets**. + +When not using StackSets or Prowler and only needing to scan AWS Organization accounts using the CLI, it is possible to assume a role in each account manually or automate that logic with custom scripts. + ## Retrieving AWS Account Details If AWS Organizations is enabled, Prowler can fetch detailed account information during scans, including: @@ -33,7 +41,7 @@ Prowler will scan the AWS account and get the account details from AWS Organizat ### Handling JSON Output -In Prowler’s JSON output, tags are encoded in Base64 to prevent formatting errors in CSV or JSON outputs. This ensures compatibility when exporting findings. +In Prowler's JSON output, tags are encoded in Base64 to prevent formatting errors in CSV or JSON outputs. This ensures compatibility when exporting findings. ```json "Account Email": "my-prod-account@domain.com", @@ -51,6 +59,67 @@ The additional fields in CSV header output are as follows: - ACCOUNT\_DETAILS\_ORG - ACCOUNT\_DETAILS\_TAGS +## Deploying Prowler IAM Roles Across AWS Organizations + +When onboarding multiple AWS accounts into Prowler Cloud, it is important to deploy the Prowler Scan IAM Role in each account. The most efficient way to do this across an AWS Organization is by leveraging AWS CloudFormation StackSets, which rolls out infrastructure—like IAM roles—to all accounts centrally from the Management or Delegated Admin account. + +When using Infrastructure as Code (IaC), Terraform is recommended to manage this deployment systematically. + +### Recommended Approach + +- **Use StackSets** from the **Management Account** (or a Delegated Admin/Security Account). +- **Use Terraform** to orchestrate the deployment. +- **Use the official CloudFormation template** provided by Prowler. +- Target specific Organizational Units (OUs) or the entire Organization. + +???+ note + A detailed community article this implementation is based on is available here: + [Deploy IAM Roles Across an AWS Organization as Code (Unicrons)](https://unicrons.cloud/en/2024/10/14/deploy-iam-roles-across-an-aws-organization-as-code/) + This guide has been adapted with permission and aligned with Prowler’s IAM role requirements. + +--- + +### Step-by-Step Guide Using Terraform + +Below is a ready Terraform snippet that deploys the [Prowler Scan IAM Role CloudFormation template](https://github.com/prowler-cloud/prowler/blob/master/permissions/templates/cloudformation/prowler-scan-role.yml) across the AWS Organization using StackSets: + +```hcl title="main.tf" +data "aws_caller_identity" "this" {} + +data "aws_organizations_organization" "this" {} + +module "prowler-scan-role" { + source = "unicrons/organization-iam-role/aws" + + stack_set_name = "prowler-scan-role" + stack_set_description = "Deploy Prowler Scan IAM Role across all organization accounts" + template_path = "${path.root}/prowler-scan-role.yaml" + + template_parameters = { + ExternalId = "<< external ID >>" # Replace with the External ID provided by Prowler Cloud + } + + # Specific OU IDs can be specified instead of root + organizational_unit_ids = [data.aws_organizations_organization.this.roots[0].id] +} +``` + +#### `prowler-scan-role.yaml` + +Download or reference the official CloudFormation template directly from GitHub: + +- [prowler-scan-role.yml](https://github.com/prowler-cloud/prowler/blob/master/permissions/templates/cloudformation/prowler-scan-role.yml) + +--- + +### IAM Role: External ID Support + +Include the `ExternalId` parameter in the StackSet if required by the organization's Prowler Cloud setup. This ensures secure cross-account access for scanning. + +--- + +When encountering issues during deployment or needing to target specific OUs or environments (e.g., dev/staging/prod), reach out to the Prowler team via [Slack Community](https://prowler.com/slack) or [Support](mailto:support@prowler.com). + ## Extra: Run Prowler across all accounts in AWS Organizations by assuming roles ### Running Prowler Across All AWS Organization Accounts diff --git a/docs/tutorials/aws/securityhub.md b/docs/tutorials/aws/securityhub.md index bbf907efae..1ece9e61e5 100644 --- a/docs/tutorials/aws/securityhub.md +++ b/docs/tutorials/aws/securityhub.md @@ -21,7 +21,7 @@ AWS Security Hub can be enabled using either of the following methods: #### Enabling AWS Security Hub for Prowler Integration -If AWS Security Hub is already enabled, you can proceed to the [next section](#enable-prowler-integration). +If AWS Security Hub is already enabled, you can proceed to the [next section](#enabling-prowler-integration-in-aws-security-hub). 1. Enable AWS Security Hub via Console: Open the **AWS Security Hub** console: https://console.aws.amazon.com/securityhub/. @@ -33,7 +33,7 @@ If AWS Security Hub is already enabled, you can proceed to the [next section](#e #### Enabling Prowler Integration in AWS Security Hub -If the Prowler integration is already enabled in AWS Security Hub, you can proceed to the [next section](#send-findings) and begin sending findings. +If the Prowler integration is already enabled in AWS Security Hub, you can proceed to the [next section](#sending-findings-to-aws-security-hub) and begin sending findings. Once **AWS Security Hub** is activated, **Prowler** must be enabled as partner integration to allow security findings to be sent to it. diff --git a/docs/tutorials/azure/authentication.md b/docs/tutorials/azure/authentication.md index fbfaabd062..92f613fe16 100644 --- a/docs/tutorials/azure/authentication.md +++ b/docs/tutorials/azure/authentication.md @@ -1,28 +1,77 @@ # Azure Authentication in Prowler -By default, Prowler utilizes the Azure Python SDK identity package for authentication, leveraging the classes `DefaultAzureCredential` and `InteractiveBrowserCredential`. This enables authentication against Azure using the following approaches: +Prowler for Azure supports multiple authentication types. To use a specific method, pass the appropriate flag during execution: -- Service principal authentication via environment variables (Enterprise Application) -- Currently stored AZ CLI credentials -- Interactive browser authentication -- Managed identity authentication +- [**Service Principal Application**](https://learn.microsoft.com/en-us/entra/identity-platform/app-objects-and-service-principals?tabs=browser#service-principal-object) (**Recommended**) +- Existing **AZ CLI credentials** +- **Interactive browser authentication** +- [**Managed Identity**](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview) authentication -Before launching the tool, specify the desired method using the following flags: +> ⚠️ **Important:** For Prowler App, only Service Principal authentication is supported. + +### Service Principal Application Authentication + +Enable Prowler authentication using a Service Principal Application by setting up the following environment variables: ```console -# Service principal authentication: -prowler azure --sp-env-auth - -# AZ CLI authentication -prowler azure --az-cli-auth - -# Browser authentication -prowler azure --browser-auth --tenant-id "XXXXXXXX" - -# Managed identity authentication -prowler azure --managed-identity-auth +export AZURE_CLIENT_ID="XXXXXXXXX" +export AZURE_TENANT_ID="XXXXXXXXX" +export AZURE_CLIENT_SECRET="XXXXXXX" ``` -## Permission Configuration +Execution with the `--sp-env-auth` flag fails if these variables are not set or exported. -To ensure Prowler can access the required resources within your Azure account, proper permissions must be configured. Refer to the [Requirements](../../getting-started/requirements.md) section for details on setting up necessary privileges. +Refer to the [Create Prowler Service Principal](create-prowler-service-principal.md) guide for detailed setup instructions. + +### Azure Authentication Methods + +Prowler for Azure supports the following authentication methods: + +- **AZ CLI Authentication (`--az-cli-auth`)** – Automated authentication using stored AZ CLI credentials. +- **Managed Identity Authentication (`--managed-identity-auth`)** – Automated authentication via Azure Managed Identity. +- **Browser Authentication (`--browser-auth`)** – Requires the user to authenticate using the default browser. The `tenant-id` parameter is mandatory for this method. + +### Required Permissions + +Prowler for Azure requires two types of permission scopes: + +#### Microsoft Entra ID Permissions + +These permissions allow Prowler to retrieve metadata from the assumed identity and perform specific Entra checks. While not mandatory for execution, they enhance functionality. + +Required permissions: + +- `Directory.Read.All` +- `Policy.Read.All` +- `UserAuthenticationMethod.Read.All` (used for Entra multifactor authentication checks) + + ???+ note + Replace `Directory.Read.All` with `Domain.Read.All` for more restrictive permissions. Note that Entra checks related to DirectoryRoles and GetUsers will not run with this permission. + + +#### Subscription Scope Permissions + +These permissions are required to perform security checks against Azure resources. The following **RBAC roles** must be assigned per subscription to the entity used by Prowler: + +- `Reader` – Grants read-only access to Azure resources. +- `ProwlerRole` – A custom role with minimal permissions, defined in the [prowler-azure-custom-role](https://github.com/prowler-cloud/prowler/blob/master/permissions/prowler-azure-custom-role.json). + +???+ note + The `assignableScopes` field in the JSON custom role file must be updated to reflect the correct subscription or management group. Use one of the following formats: `/subscriptions/` or `/providers/Microsoft.Management/managementGroups/`. + +### Assigning Permissions + +To properly configure permissions, follow these guides: + +- [Microsoft Entra ID permissions](create-prowler-service-principal.md#assigning-proper-permissions) +- [Azure subscription permissions](subscriptions.md) + +???+ warning + Some permissions in `ProwlerRole` involve **write access**. If a `ReadOnly` lock is attached to certain resources, you may encounter errors, and findings for those checks will not be available. + +#### Checks Requiring `ProwlerRole` + +The following security checks require the `ProwlerRole` permissions for execution. Ensure the role is assigned to the identity assumed by Prowler before running these checks: + +- `app_function_access_keys_configured` +- `app_function_ftps_deployment_disabled` diff --git a/docs/tutorials/azure/create-prowler-service-principal.md b/docs/tutorials/azure/create-prowler-service-principal.md index 7ab6f58f43..684457fb5f 100644 --- a/docs/tutorials/azure/create-prowler-service-principal.md +++ b/docs/tutorials/azure/create-prowler-service-principal.md @@ -73,7 +73,7 @@ Permissions can be assigned via the Azure Portal or the Azure CLI. 7. Finally, search for "Directory", "Policy" and "UserAuthenticationMethod" select the following permissions: - - `Domain.Read.All` + - `Directory.Read.All` - `Policy.Read.All` diff --git a/docs/tutorials/azure/subscriptions.md b/docs/tutorials/azure/subscriptions.md index e4ee6e5723..50eac293e5 100644 --- a/docs/tutorials/azure/subscriptions.md +++ b/docs/tutorials/azure/subscriptions.md @@ -21,7 +21,7 @@ Prowler allows you to specify one or more subscriptions for scanning (up to N), To perform scans, ensure that the identity assumed by Prowler has the appropriate permissions. -By default, Prowler scans all accessible subscriptions. If you need to audit specific subscriptions, you must assign the necessary role `Reader` for each one. For streamlined and less repetitive role assignments in multi-subscription environments, refer to the [following section](#recommendation-for-multiple-subscriptions). +By default, Prowler scans all accessible subscriptions. If you need to audit specific subscriptions, you must assign the necessary role `Reader` for each one. For streamlined and less repetitive role assignments in multi-subscription environments, refer to the [following section](#recommendation-for-managing-multiple-subscriptions). ### Assigning the Reader Role in Azure Portal @@ -76,7 +76,7 @@ Navigate to the subscription you want to audit with Prowler. Some read-only permissions required for specific security checks are not included in the built-in Reader role. To support these checks, Prowler utilizes a custom role, defined in [prowler-azure-custom-role](https://github.com/prowler-cloud/prowler/blob/master/permissions/prowler-azure-custom-role.json). Once created, this role can be assigned following the same process as the `Reader` role. -The checks requiring this `ProwlerRole` can be found in the [requirements section](../../getting-started/requirements.md#checks-that-require-prowlerrole). +The checks requiring this `ProwlerRole` can be found in this [section](../../tutorials/azure/authentication.md#checks-requiring-prowlerrole). #### Create ProwlerRole via Azure Portal @@ -152,7 +152,7 @@ Scanning multiple subscriptions requires creating and assigning roles for each, ![Create management group](../../img/create-management-group.gif) -2. **Assign Roles**: Assign necessary roles to the management group, similar to the [role assignment process](#assign-the-appropriate-permissions-to-the-identity-that-is-going-to-be-assumed-by-prowler). +2. **Assign Roles**: Assign necessary roles to the management group, similar to the [role assignment process](#assigning-permissions-for-subscription-scans). Role assignment should be done at the management group level instead of per subscription. diff --git a/docs/tutorials/bulk-provider-provisioning.md b/docs/tutorials/bulk-provider-provisioning.md new file mode 100644 index 0000000000..8a420c5bac --- /dev/null +++ b/docs/tutorials/bulk-provider-provisioning.md @@ -0,0 +1,367 @@ +# Bulk Provider Provisioning in Prowler + +Prowler enables automated provisioning of multiple cloud providers through the Bulk Provider Provisioning tool. This approach streamlines the onboarding process for organizations managing numerous cloud accounts, subscriptions, and projects across AWS, Azure, GCP, Kubernetes, Microsoft 365, and GitHub. + +The tool is available in the Prowler repository at: [util/prowler-bulk-provisioning](https://github.com/prowler-cloud/prowler/tree/master/util/prowler-bulk-provisioning) + +![](./img/bulk-provider-provisioning.png) + +## Overview + +The Bulk Provider Provisioning tool automates the creation of cloud providers in Prowler App or Prowler Cloud by: + +* Reading provider configurations from YAML files +* Creating providers with appropriate authentication credentials +* Testing connections to verify successful authentication +* Processing multiple providers concurrently for efficiency + + +## Prerequisites + +### Requirements + +* Python 3.7 or higher +* Prowler API token (from Prowler Cloud or self-hosted Prowler App) + * For self-hosted Prowler App, remember to [point to your API base URL](#custom-api-endpoints) +* Authentication credentials for target cloud providers + +### Installation + +Clone the repository and install the required dependencies: + +```bash +git clone https://github.com/prowler-cloud/prowler.git +cd prowler/util/prowler-bulk-provisioning +pip install -r requirements.txt +``` + +### Authentication Setup + +Configure your Prowler API token: + +```bash +export PROWLER_API_TOKEN="your-prowler-api-token" +``` + +To obtain an API token programmatically: + +```bash +export PROWLER_API_TOKEN=$(curl --location 'https://api.prowler.com/api/v1/tokens' \ + --header 'Content-Type: application/vnd.api+json' \ + --header 'Accept: application/vnd.api+json' \ + --data-raw '{ + "data": { + "type": "tokens", + "attributes": { + "email": "your@email.com", + "password": "your-password" + } + } + }' | jq -r .data.attributes.access) +``` + +## Configuration File Structure + +Create a YAML file listing your cloud providers and credentials: + +```yaml +# providers.yaml +- provider: aws + uid: "123456789012" # AWS Account ID + alias: "production-account" + auth_method: role + credentials: + role_arn: "arn:aws:iam::123456789012:role/ProwlerScanRole" + external_id: "prowler-external-id" + +- provider: azure + uid: "00000000-1111-2222-3333-444444444444" # Subscription ID + alias: "azure-production" + auth_method: service_principal + credentials: + tenant_id: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" + client_id: "ffffffff-1111-2222-3333-444444444444" + client_secret: "your-client-secret" + +- provider: gcp + uid: "my-gcp-project" # Project ID + alias: "gcp-production" + auth_method: service_account + credentials: + service_account_key_json_path: "./service-account.json" +``` + +## Running the Bulk Provisioning Tool + +### Basic Usage + +To provision all providers from your configuration file: + +```bash +python prowler_bulk_provisioning.py providers.yaml +``` + +The tool automatically tests each provider connection after creation (enabled by default). + +### Dry Run Mode + +Test your configuration without making API calls: + +```bash +python prowler_bulk_provisioning.py providers.yaml --dry-run +``` + +### Skip Connection Testing + +To provision providers without testing connections: + +```bash +python prowler_bulk_provisioning.py providers.yaml --test-provider false +``` + +### Test Existing Providers Only + +To verify connections for already provisioned providers: + +```bash +python prowler_bulk_provisioning.py providers.yaml --test-provider-only +``` + +## Provider-Specific Configuration + +### AWS Provider Configuration + +#### Using IAM Role (Recommended) + +```yaml +- provider: aws + uid: "123456789012" + alias: "aws-production" + auth_method: role + credentials: + role_arn: "arn:aws:iam::123456789012:role/ProwlerScanRole" + external_id: "optional-external-id" + session_name: "prowler-scan-session" # optional + duration_seconds: 3600 # optional +``` + +#### Using Access Keys + +```yaml +- provider: aws + uid: "123456789012" + alias: "aws-development" + auth_method: credentials + credentials: + access_key_id: "AKIA..." + secret_access_key: "..." + session_token: "..." # optional for temporary credentials +``` + +### Azure Provider Configuration + +```yaml +- provider: azure + uid: "subscription-uuid" + alias: "azure-production" + auth_method: service_principal + credentials: + tenant_id: "tenant-uuid" + client_id: "client-uuid" + client_secret: "client-secret" +``` + +### GCP Provider Configuration + +#### Using Service Account JSON + +```yaml +- provider: gcp + uid: "project-id" + alias: "gcp-production" + auth_method: service_account + credentials: + service_account_key_json_path: "/path/to/key.json" +``` + +#### Using OAuth2 Credentials + +```yaml +- provider: gcp + uid: "project-id" + alias: "gcp-production" + auth_method: oauth2 + credentials: + client_id: "123456789.apps.googleusercontent.com" + client_secret: "GOCSPX-xxxx" + refresh_token: "1//0exxxxxx" +``` + +### Kubernetes Provider Configuration + +```yaml +- provider: kubernetes + uid: "context-name" + alias: "eks-production" + auth_method: kubeconfig + credentials: + kubeconfig_path: "~/.kube/config" + # OR inline configuration: + # kubeconfig_inline: | + # apiVersion: v1 + # clusters: ... +``` + +### Microsoft 365 Provider Configuration + +```yaml +- provider: m365 + uid: "domain.onmicrosoft.com" + alias: "m365-tenant" + auth_method: service_principal + credentials: + tenant_id: "tenant-uuid" + client_id: "client-uuid" + client_secret: "client-secret" +``` + +### GitHub Provider Configuration + +#### Using Personal Access Token + +```yaml +- provider: github + uid: "organization-name" + alias: "github-org" + auth_method: personal_access_token + credentials: + token: "ghp_..." +``` + +#### Using GitHub App + +```yaml +- provider: github + uid: "organization-name" + alias: "github-org" + auth_method: github_app + credentials: + app_id: "123456" + private_key_path: "/path/to/private-key.pem" +``` + +## Advanced Configuration + +### Concurrent Processing + +Adjust the number of concurrent provider creations: + +```bash +python prowler_bulk_provisioning.py providers.yaml --concurrency 10 +``` + +### Custom API Endpoints + +For self-hosted Prowler App installations: + +```bash +python prowler_bulk_provisioning.py providers.yaml \ + --base-url http://localhost:8080/api/v1 +``` + +### Timeout Configuration + +Set custom timeout for API requests: + +```bash +python prowler_bulk_provisioning.py providers.yaml --timeout 120 +``` + +## Bulk Provider Management + +### Deleting Multiple Providers + +To remove all providers from your Prowler account: + +```bash +python nuke_providers.py --confirm +``` + +Filter deletions by provider type: + +```bash +python nuke_providers.py --confirm --filter-provider aws +``` + +Filter deletions by alias pattern: + +```bash +python nuke_providers.py --confirm --filter-alias "test-*" +``` + +## Configuration File Format + +The tool uses YAML format for provider configuration files. Each provider entry requires: + +* `provider`: The cloud provider type (aws, azure, gcp, kubernetes, m365, github) +* `uid`: Unique identifier for the provider (account ID, subscription ID, project ID, etc.) +* `alias`: A friendly name for the provider +* `auth_method`: Authentication method to use +* `credentials`: Authentication credentials specific to the provider and method + +Example YAML structure: + +```yaml +- provider: aws + uid: "123456789012" + alias: "production" + auth_method: role + credentials: + role_arn: "arn:aws:iam::123456789012:role/ProwlerScan" +``` + +## Example Output + +Successful provider provisioning: + +``` +[1] ✅ Created provider (id=db9a8985-f9ec-4dd8-b5a0-e05ab3880bed) +[1] ✅ Created secret (id=466f76c6-5878-4602-a4bc-13f9522c1fd2) +[1] ✅ Connection test: Connected + +[2] ✅ Created provider (id=7a99f789-0cf5-4329-8279-2d443a962676) +[2] ✅ Created secret (id=c5702180-f7c4-40fd-be0e-f6433479b126) +[2] ⚠️ Connection test: Not connected + +Done. Success: 2 Failures: 0 +``` + +## Troubleshooting + +### Invalid API Token + +``` +Error: 401 Unauthorized +Solution: Verify your PROWLER_API_TOKEN or --token parameter +``` + +### Network Timeouts + +``` +Error: Connection timeout +Solution: Increase timeout with --timeout 120 +``` + +### Provider Already Exists + +``` +Error: Provider with this UID already exists +Solution: Use different UID or delete existing provider first +``` + +### Authentication Failures + +``` +Connection test: Not connected +Solution: Verify credentials and IAM permissions +``` diff --git a/docs/tutorials/gcp/authentication.md b/docs/tutorials/gcp/authentication.md index 9fcbf25960..5b8ec7b92a 100644 --- a/docs/tutorials/gcp/authentication.md +++ b/docs/tutorials/gcp/authentication.md @@ -1,40 +1,40 @@ # GCP Authentication in Prowler -## Default Authentication +## Required Permissions -By default, Prowler uses your User Account credentials. You can configure authentication as follows: +Prowler for Google Cloud requires the following permissions: -- `gcloud init` to use a new account, or -- `gcloud config set account ` to use an existing account. +### IAM Roles +- **Reader (`roles/reader`)** – Must be granted at the **project, folder, or organization** level to allow scanning of target projects. -Then, obtain your access credentials using: `gcloud auth application-default login`. +### Project-Level Settings -## Using Service Account Keys +At least one project must have the following configurations: -Alternatively, Service Account keys can be generated and downloaded in JSON format. Follow the steps in the Google Cloud IAM guide (https://cloud.google.com/iam/docs/creating-managing-service-account-keys) to create and manage service account keys. Provide the path to the key file using: +- **Identity and Access Management (IAM) API (`iam.googleapis.com`)** – Must be enabled via: -```console -prowler gcp --credentials-file path -``` + - The [Google Cloud API UI](https://console.cloud.google.com/apis/api/iam.googleapis.com/metrics), or + - The `gcloud` CLI: + ```sh + gcloud services enable iam.googleapis.com --project + ``` + +- **Service Usage Consumer (`roles/serviceusage.serviceUsageConsumer`)** IAM Role – Required for resource scanning. + +- **Quota Project Setting** – Define a quota project using either: + + - The `gcloud` CLI: + ```sh + gcloud auth application-default set-quota-project + ``` + - Setting an environment variable: + ```sh + export GOOGLE_CLOUD_QUOTA_PROJECT= + ``` ???+ note `prowler` will scan the GCP project associated with the credentials. -## Using an access token - -If you already have an access token (e.g., generated with `gcloud auth print-access-token`), you can run Prowler with: - -```bash -export CLOUDSDK_AUTH_ACCESS_TOKEN=$(gcloud auth print-access-token) -prowler gcp --project-ids -``` - -???+ note - If using this method, it's recommended to also set the default project explicitly: - ```bash - export GOOGLE_CLOUD_PROJECT= - ``` - ## Credentials lookup order Prowler follows the same credential search process as [Google authentication libraries](https://cloud.google.com/docs/authentication/application-default-credentials#search_order), checking credentials in this order: @@ -52,26 +52,27 @@ Prowler follows the same credential search process as [Google authentication lib Prowler will use the enabled Google Cloud APIs to get the information needed to perform the checks. -## Required Permissions - -To ensure full functionality, Prowler for Google Cloud needs the following permissions to be set: - -- **Reader (`roles/reader`) IAM role**: granted at the project / folder / org level in order to scan the target projects - -- **Project level settings**: you need to have at least one project with the below settings: - - Identity and Access Management (IAM) API (`iam.googleapis.com`) enabled by either using the - [Google Cloud API UI](https://console.cloud.google.com/apis/api/iam.googleapis.com/metrics) or - by using the gcloud CLI `gcloud services enable iam.googleapis.com --project ` command - - Set the quota project to be this project by either running `gcloud auth application-default set-quota-project ` or by setting an environment variable: - `export GOOGLE_CLOUD_QUOTA_PROJECT=` -The above settings must be associated to a user or service account. +## Using an Access Token + +For existing access tokens (e.g., generated with `gcloud auth print-access-token`), run Prowler with: + +```bash +export CLOUDSDK_AUTH_ACCESS_TOKEN=$(gcloud auth print-access-token) +prowler gcp --project-ids +``` ???+ note - Prowler will use the enabled Google Cloud APIs to get the information needed to perform the checks. + When using this method, also set the default project explicitly: + ```bash + export GOOGLE_CLOUD_PROJECT= + ``` -## Impersonating a GCP Service Account in Prowler + + + +## Impersonating a GCP Service Account To impersonate a GCP service account, use the `--impersonate-service-account` argument followed by the service account email: diff --git a/docs/tutorials/github/authentication.md b/docs/tutorials/github/authentication.md index 71ff22f9c7..adb26be402 100644 --- a/docs/tutorials/github/authentication.md +++ b/docs/tutorials/github/authentication.md @@ -1,4 +1,4 @@ -# GitHub Authentication +# Github Authentication in Prowler Prowler supports multiple methods to [authenticate with GitHub](https://docs.github.com/en/rest/authentication/authenticating-to-the-rest-api). These include: @@ -6,7 +6,7 @@ Prowler supports multiple methods to [authenticate with GitHub](https://docs.git - **OAuth App Token** - **GitHub App Credentials** -This flexibility allows you to scan and analyze your GitHub account, including repositories, organizations, and applications, using the method that best suits your use case. +This flexibility enables scanning and analysis of GitHub accounts, including repositories, organizations, and applications, using the method that best suits the use case. ## Supported Login Methods @@ -44,4 +44,4 @@ If no login method is explicitly provided, Prowler will automatically attempt to 3. `GITHUB_APP_ID` and `GITHUB_APP_KEY` (where the key is the content of the private key file) ???+ note - Ensure the corresponding environment variables are set up before running Prowler for automatic detection if you don't plan to specify the login method. + Ensure the corresponding environment variables are set up before running Prowler for automatic detection when not specifying the login method. diff --git a/docs/tutorials/github/getting-started-github.md b/docs/tutorials/github/getting-started-github.md index b4f22d0366..48be3859bc 100644 --- a/docs/tutorials/github/getting-started-github.md +++ b/docs/tutorials/github/getting-started-github.md @@ -11,7 +11,7 @@ This guide explains how to set up authentication with GitHub for Prowler. The do ### 1. Personal Access Token (PAT) -Personal Access Tokens provide the simplest GitHub authentication method and support individual user authentication or testing scenarios. +Personal Access Tokens provide the simplest GitHub authentication method, but it can only access resources owned by a single user or organization. ???+ warning "Classic Tokens Deprecated" GitHub has deprecated Personal Access Tokens (classic) in favor of fine-grained Personal Access Tokens. We recommend using fine-grained tokens as they provide better security through more granular permissions and resource-specific access control. @@ -186,7 +186,10 @@ GitHub Apps provide the recommended integration method for accessing multiple re - **Account permissions**: - Email addresses (Read) -4. **Generate Private Key** +4. **Where can this GitHub App be installed?** + - Select "Any account" to be able to install the GitHub App in any organization. + +5. **Generate Private Key** - Scroll to the "Private keys" section after app creation - Click "Generate a private key" - Download the `.pem` file and store securely diff --git a/docs/tutorials/iac/authentication.md b/docs/tutorials/iac/authentication.md new file mode 100644 index 0000000000..9258e8cc91 --- /dev/null +++ b/docs/tutorials/iac/authentication.md @@ -0,0 +1,11 @@ +# IaC Authentication in Prowler + +Prowler's Infrastructure as Code (IaC) provider enables you to scan local or remote infrastructure code for security and compliance issues using [Trivy](https://trivy.dev/). This provider supports a wide range of IaC frameworks and requires no cloud authentication for local scans. + +### Authentication + +- For local scans, no authentication is required. +- For remote repository scans, authentication can be provided via: + - [**GitHub Username and Personal Access Token (PAT)**](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic) + - [**GitHub OAuth App Token**](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token) + - [**Git URL**](https://git-scm.com/docs/git-clone#_git_urls) diff --git a/docs/tutorials/iac/getting-started-iac.md b/docs/tutorials/iac/getting-started-iac.md index 052f72bae9..4dac4eea56 100644 --- a/docs/tutorials/iac/getting-started-iac.md +++ b/docs/tutorials/iac/getting-started-iac.md @@ -1,32 +1,22 @@ # Getting Started with the IaC Provider -Prowler's Infrastructure as Code (IaC) provider enables you to scan local or remote 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. +Prowler's Infrastructure as Code (IaC) provider enables you to scan local or remote infrastructure code for security and compliance issues using [Trivy](https://trivy.dev/). This provider supports a wide range of IaC frameworks, allowing you to assess your code before deployment. -## Supported Frameworks +## Supported Scanners -The IaC provider leverages Checkov to support multiple frameworks, including: +The IaC provider leverages Trivy to support multiple scanners, 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) +- Vulnerability +- Misconfiguration +- Secret +- License ## How It Works - The IaC provider scans your local directory (or a specified path) for supported IaC files, or scan a remote repository. - No cloud credentials or authentication are required for local scans. - For remote repository scans, authentication can be provided via [git URL](https://git-scm.com/docs/git-clone#_git_urls), CLI flags or environment variables. -- Mutelist logic is handled by Checkov, not Prowler. +- Mutelist logic is handled by Trivy, not Prowler. - Results are output in the same formats as other Prowler providers (CSV, JSON, HTML, etc.). ## Usage @@ -67,12 +57,12 @@ You can provide authentication for private repositories using one of the followi #### Mutually Exclusive Flags - `--scan-path` and `--scan-repository-url` are mutually exclusive. Only one can be specified at a time. -### Specify Frameworks +### Specify Scanners -Scan only Terraform and Kubernetes files: +Scan only vulnerability and misconfiguration scanners: ```sh -prowler iac --scan-path ./my-iac-directory --frameworks terraform kubernetes +prowler iac --scan-path ./my-iac-directory --scanners vuln misconfig ``` ### Exclude Paths @@ -95,4 +85,4 @@ prowler iac --scan-path ./iac --output-formats csv json html - For remote repository scans, authentication is optional but required for private repos. - CLI flags override environment variables for 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). +- For more details on supported scanners, see the [Trivy documentation](https://trivy.dev/latest/docs/scanner/vulnerability/). diff --git a/docs/tutorials/img/bulk-provider-provisioning.png b/docs/tutorials/img/bulk-provider-provisioning.png new file mode 100644 index 0000000000..c43c746874 Binary files /dev/null and b/docs/tutorials/img/bulk-provider-provisioning.png differ diff --git a/docs/tutorials/microsoft365/authentication.md b/docs/tutorials/microsoft365/authentication.md index fe25125f0b..c8f78c1c78 100644 --- a/docs/tutorials/microsoft365/authentication.md +++ b/docs/tutorials/microsoft365/authentication.md @@ -1,29 +1,361 @@ # Microsoft 365 Authentication for Prowler -By default, Prowler utilizes the MsGraph Python SDK identity package for authentication, leveraging the class `ClientSecretCredential`. This enables authentication against Microsoft 365 using the following approaches: +Prowler for Microsoft 365 (M365) supports the following authentication methods: -- Service principal authentication by environment variables (Enterprise Application) -- Service principal and Microsoft user credentials by environment variabled (using PowerShell requires this authentication method) -- Current CLI credentials stored -- Interactive browser authentication +- [**Service Principal Application**](https://learn.microsoft.com/en-us/entra/identity-platform/app-objects-and-service-principals?tabs=browser#service-principal-object) (**Recommended**) +- **Service Principal Application with Microsoft User Credentials** +- **Stored AZ CLI credentials** +- **Interactive browser authentication** +???+ warning + Prowler App supports the **Service Principal** authentication method and the **Service Principal with User Credentials** authentication method, but this last one will be deprecated in September once Microsoft will enforce MFA in all tenants not allowing User authentication without interactive method. -To launch the tool first you need to specify which method is used through the following flags: +### Service Principal Authentication (Recommended) + +**Authentication flag:** `--sp-env-auth` + +Enable Prowler authentication as the **Service Principal Application** by configuring the following environment variables: ```console -# To use service principal (app) authentication and Microsoft user credentials -prowler m365 --env-auth - -# To use service principal authentication -prowler m365 --sp-env-auth - -# To use cli authentication -prowler m365 --az-cli-auth - -# To use browser authentication -prowler m365 --browser-auth --tenant-id "XXXXXXXX" +export AZURE_CLIENT_ID="XXXXXXXXX" +export AZURE_CLIENT_SECRET="XXXXXXXXX" +export AZURE_TENANT_ID="XXXXXXXXX" ``` -## Permission Configuration +If these variables are not set or exported, execution using `--sp-env-auth` will fail. -To ensure Prowler can access the required resources within your Microsoft 365 account, proper permissions must be configured. Refer to the [Requirements](../../getting-started/requirements.md#needed-permissions_2) section for details on setting up necessary privileges. +Refer to the [Create Prowler Service Principal](getting-started-m365.md#create-the-service-principal-app) guide for setup instructions. + +If the external API permissions described in the mentioned section above are not added only checks that work through MS Graph will be executed. This means that the full provider will not be executed. + +???+ note + In order to scan all the checks from M365 required permissions to the service principal application must be added. Refer to the [External API Permissions Assignment](getting-started-m365.md#grant-powershell-modules-permissions) section for more information. + +### Service Principal and User Credentials Authentication + +Authentication flag: `--env-auth` + +???+ warning + This method is not recommended anymore, we recommend just use the **Service Principal Application** authentication method instead. + +This method builds upon the Service Principal authentication by adding User Credentials. Configure the following environment variables: `M365_USER` and `M365_PASSWORD`. + +```console +export AZURE_CLIENT_ID="XXXXXXXXX" +export AZURE_CLIENT_SECRET="XXXXXXXXX" +export AZURE_TENANT_ID="XXXXXXXXX" +export M365_USER="your_email@example.com" +export M365_PASSWORD="examplepassword" +``` + +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 + Newly created users must sign in with the account first, as Microsoft prompts for password change. Without completing this step, user authentication fails because Microsoft marks the initial password as expired. + + ???+ warning + The user must not be MFA capable. Microsoft does not allow MFA capable users to authenticate programmatically. See [Microsoft documentation](https://learn.microsoft.com/en-us/entra/identity-platform/scenario-desktop-acquire-token-username-password?tabs=dotnet) for more information. + + ???+ warning + Using a tenant domain other than the one assigned — even if it belongs to the same tenant — will cause Prowler to fail, as Microsoft authentication will not succeed. + + Ensure the correct domain is used for the authenticating user. + + ![User Domains](img/user-domains.png) + +- `M365_PASSWORD` must be the user password. + + ???+ note + Previously an encrypted password was required, but now the user password is accepted directly. Prowler handles the password encryption. + + + +### Interactive Browser Authentication + +**Authentication flag:** `--browser-auth` + +This authentication method requires authentication against Azure using the default browser to start the scan. The `--tenant-id` flag is also required. + +These credentials only enable checks that rely on Microsoft Graph. The entire provider cannot be run with this method. To perform a full M365 security scan, use the **recommended authentication method**. + +Since this is a **delegated permission** authentication method, necessary permissions should be assigned to the user rather than the application. + +### Required Permissions + +To run the full Prowler provider, including PowerShell checks, two types of permission scopes must be set in **Microsoft Entra ID**. + +#### Service Principal Authentication (`--sp-env-auth`) - Recommended + +When using service principal authentication, add the following **Application Permissions**: + +**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 `Global Reader` role to the app. +- `application_access` from external API `Skype and Teams Tenant Admin API`: Required for Teams PowerShell module app authentication. + +???+ note + `Directory.Read.All` can be replaced with `Domain.Read.All` that is a more restrictive permission but you won't be able to run the Entra checks related with DirectoryRoles and GetUsers. + + > If you do this you will need to add also the `Organization.Read.All` permission to the service principal application in order to authenticate. + +???+ 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, same as the Service Principal Authentication + User Credentials Authentication but this last one will be deprecated in September once Microsoft will enforce MFA in all tenants not allowing User authentication without interactive method. + + +#### 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). + + +#### 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. + +### Assigning Permissions and Roles + +For guidance on assigning the necessary permissions and roles, follow these instructions: +- [Grant API Permissions](getting-started-m365.md#grant-required-graph-api-permissions) +- [Assign Required Roles](getting-started-m365.md#if-using-user-authentication) + +### Supported PowerShell Versions + +PowerShell is required to run certain M365 checks. + +**Supported versions:** +- **PowerShell 7.4 or higher** (7.5 is recommended) + +#### Why Is PowerShell 7.4+ Required? + +- **PowerShell 5.1** (default on some Windows systems) does not support required cmdlets. +- Older [cross-platform PowerShell versions](https://learn.microsoft.com/en-us/powershell/scripting/install/powershell-support-lifecycle?view=powershell-7.5) are **unsupported**, leading to potential errors. + +???+ note + Installing PowerShell is only necessary if you install Prowler via **pip or other sources**. **SDK and API containers include PowerShell by default.** + +### Installing PowerShell + +Installing PowerShell is different depending on your OS. + +- [Windows](https://learn.microsoft.com/es-es/powershell/scripting/install/installing-powershell-on-windows?view=powershell-7.5#install-powershell-using-winget-recommended): you will need to update PowerShell to +7.4 to be able to run prowler, if not some checks will not show findings and the provider could not work as expected. This version of PowerShell is [supported](https://learn.microsoft.com/es-es/powershell/scripting/install/installing-powershell-on-windows?view=powershell-7.4#supported-versions-of-windows) on Windows 10, Windows 11, Windows Server 2016 and higher versions. + +```console +winget install --id Microsoft.PowerShell --source winget +``` + + +- [MacOS](https://learn.microsoft.com/es-es/powershell/scripting/install/installing-powershell-on-macos?view=powershell-7.5#install-the-latest-stable-release-of-powershell): installing PowerShell on MacOS needs to have installed [brew](https://brew.sh/), once you have it is just running the command above, Pwsh is only supported in macOS 15 (Sequoia) x64 and Arm64, macOS 14 (Sonoma) x64 and Arm64, macOS 13 (Ventura) x64 and Arm64 + +```console +brew install powershell/tap/powershell +``` + +Once it's installed run `pwsh` on your terminal to verify it's working. + +- Linux: installing PowerShell on Linux depends on the distro you are using: + + - [Ubuntu](https://learn.microsoft.com/es-es/powershell/scripting/install/install-ubuntu?view=powershell-7.5#installation-via-package-repository-the-package-repository): The required version for installing PowerShell +7.4 on Ubuntu are Ubuntu 22.04 and Ubuntu 24.04. The recommended way to install it is downloading the package available on PMC. You just need to follow the following steps: + + ```console + ################################### + # Prerequisites + + # Update the list of packages + sudo apt-get update + + # Install pre-requisite packages. + sudo apt-get install -y wget apt-transport-https software-properties-common + + # Get the version of Ubuntu + source /etc/os-release + + # Download the Microsoft repository keys + wget -q https://packages.microsoft.com/config/ubuntu/$VERSION_ID/packages-microsoft-prod.deb + + # Register the Microsoft repository keys + sudo dpkg -i packages-microsoft-prod.deb + + # Delete the Microsoft repository keys file + rm packages-microsoft-prod.deb + + # Update the list of packages after we added packages.microsoft.com + sudo apt-get update + + ################################### + # Install PowerShell + sudo apt-get install -y powershell + + # Start PowerShell + pwsh + ``` + + - [Alpine](https://learn.microsoft.com/es-es/powershell/scripting/install/install-alpine?view=powershell-7.5#installation-steps): The only supported version for installing PowerShell +7.4 on Alpine is Alpine 3.20. The unique way to install it is downloading the tar.gz package available on [PowerShell github](https://github.com/PowerShell/PowerShell/releases/download/v7.5.0/powershell-7.5.0-linux-musl-x64.tar.gz). You just need to follow the following steps: + + ```console + # Install the requirements + sudo apk add --no-cache \ + ca-certificates \ + less \ + ncurses-terminfo-base \ + krb5-libs \ + libgcc \ + libintl \ + libssl3 \ + libstdc++ \ + tzdata \ + userspace-rcu \ + zlib \ + icu-libs \ + curl + + apk -X https://dl-cdn.alpinelinux.org/alpine/edge/main add --no-cache \ + lttng-ust \ + openssh-client \ + + # Download the powershell '.tar.gz' archive + curl -L https://github.com/PowerShell/PowerShell/releases/download/v7.5.0/powershell-7.5.0-linux-musl-x64.tar.gz -o /tmp/powershell.tar.gz + + # Create the target folder where powershell will be placed + sudo mkdir -p /opt/microsoft/powershell/7 + + # Expand powershell to the target folder + sudo tar zxf /tmp/powershell.tar.gz -C /opt/microsoft/powershell/7 + + # Set execute permissions + sudo chmod +x /opt/microsoft/powershell/7/pwsh + + # Create the symbolic link that points to pwsh + sudo ln -s /opt/microsoft/powershell/7/pwsh /usr/bin/pwsh + + # Start PowerShell + pwsh + ``` + + - [Debian](https://learn.microsoft.com/es-es/powershell/scripting/install/install-debian?view=powershell-7.5#installation-on-debian-11-or-12-via-the-package-repository): The required version for installing PowerShell +7.4 on Debian are Debian 11 and Debian 12. The recommended way to install it is downloading the package available on PMC. You just need to follow the following steps: + + ```console + ################################### + # Prerequisites + + # Update the list of packages + sudo apt-get update + + # Install pre-requisite packages. + sudo apt-get install -y wget + + # Get the version of Debian + source /etc/os-release + + # Download the Microsoft repository GPG keys + wget -q https://packages.microsoft.com/config/debian/$VERSION_ID/packages-microsoft-prod.deb + + # Register the Microsoft repository GPG keys + sudo dpkg -i packages-microsoft-prod.deb + + # Delete the Microsoft repository GPG keys file + rm packages-microsoft-prod.deb + + # Update the list of packages after we added packages.microsoft.com + sudo apt-get update + + ################################### + # Install PowerShell + sudo apt-get install -y powershell + + # Start PowerShell + pwsh + ``` + + - [Rhel](https://learn.microsoft.com/es-es/powershell/scripting/install/install-rhel?view=powershell-7.5#installation-via-the-package-repository): The required version for installing PowerShell +7.4 on Red Hat are RHEL 8 and RHEL 9. The recommended way to install it is downloading the package available on PMC. You just need to follow the following steps: + + ```console + ################################### + # Prerequisites + + # Get version of RHEL + source /etc/os-release + if [ ${VERSION_ID%.*} -lt 8 ] + then majorver=7 + elif [ ${VERSION_ID%.*} -lt 9 ] + then majorver=8 + else majorver=9 + fi + + # Download the Microsoft RedHat repository package + curl -sSL -O https://packages.microsoft.com/config/rhel/$majorver/packages-microsoft-prod.rpm + + # Register the Microsoft RedHat repository + sudo rpm -i packages-microsoft-prod.rpm + + # Delete the downloaded package after installing + rm packages-microsoft-prod.rpm + + # Update package index files + sudo dnf update + # Install PowerShell + sudo dnf install powershell -y + ``` + +- [Docker](https://learn.microsoft.com/es-es/powershell/scripting/install/powershell-in-docker?view=powershell-7.5#use-powershell-in-a-container): The following command download the latest stable versions of PowerShell: + + ```console + docker pull mcr.microsoft.com/dotnet/sdk:9.0 + ``` + + To start an interactive shell of Pwsh you just need to run: + + ```console + docker run -it mcr.microsoft.com/dotnet/sdk:9.0 pwsh + ``` + +### Required PowerShell Modules + +Prowler relies on several PowerShell cmdlets to retrieve necessary data. +These cmdlets come from different modules that must be installed. + +#### Automatic Installation + +The required modules are automatically installed when running Prowler with the `--init-modules` flag. + +Example command: + +```console +python3 prowler-cli.py m365 --verbose --log-level ERROR --env-auth --init-modules +``` +If the modules are already installed, running this command will not cause issues—it will simply verify that the necessary modules are available. + +???+ note + Prowler installs the modules using `-Scope CurrentUser`. + If you encounter any issues with services not working after the automatic installation, try installing the modules manually using `-Scope AllUsers` (administrator permissions are required for this). + The command needed to install a module manually is: + ```powershell + Install-Module -Name "ModuleName" -Scope AllUsers -Force + ``` + +#### Modules Version + +- [ExchangeOnlineManagement](https://www.powershellgallery.com/packages/ExchangeOnlineManagement/3.6.0) (Minimum version: 3.6.0) Required for 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. +- [MSAL.PS](https://www.powershellgallery.com/packages/MSAL.PS/4.32.0): Required for Exchange module via application authentication. diff --git a/docs/tutorials/microsoft365/getting-started-m365.md b/docs/tutorials/microsoft365/getting-started-m365.md index d2c487d745..988f58a1b5 100644 --- a/docs/tutorials/microsoft365/getting-started-m365.md +++ b/docs/tutorials/microsoft365/getting-started-m365.md @@ -257,7 +257,8 @@ This method is not recommended because it requires a user with MFA enabled and M - `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) + + - `M365_USER` the user using the correct assigned domain, more info [here](../../tutorials/microsoft365/authentication.md#service-principal-and-user-credentials-authentication) - `M365_PASSWORD` the password of the user ![Prowler Cloud M365 Credentials](./img/m365-credentials.png) diff --git a/docs/tutorials/microsoft365/use-of-powershell.md b/docs/tutorials/microsoft365/use-of-powershell.md index d8fa5585bf..5861120b0d 100644 --- a/docs/tutorials/microsoft365/use-of-powershell.md +++ b/docs/tutorials/microsoft365/use-of-powershell.md @@ -4,9 +4,9 @@ PowerShell is required by this provider because it is the only way to retrieve d If you are using Prowler Cloud, you don't need to worry about PowerShell — it is already installed in our infrastructure. However, if you want to run Prowler on your own, you must have PowerShell installed to execute the full M365 provider and retrieve all findings. -To learn more about how to install PowerShell and which versions are supported, click [here](../../getting-started/requirements.md#supported-powershell-versions). +To learn more about how to install PowerShell and which versions are supported, click [here](../../tutorials/microsoft365/authentication.md#supported-powershell-versions). ## Required Modules The necessary modules will not be installed automatically by Prowler. Nevertheless, if you want Prowler to install them for you, you can execute the provider with the flag `--init-modules`, which will run the script to install and import them. -If you want to learn more about this process or you are running some issues with this, click [here](../../getting-started/requirements.md#needed-powershell-modules). +If you want to learn more about this process or you are running some issues with this, click [here](../../tutorials/microsoft365/authentication.md#required-powershell-modules). diff --git a/docs/tutorials/misc.md b/docs/tutorials/misc.md index d54ab9a0ea..0c8fd7943f 100644 --- a/docs/tutorials/misc.md +++ b/docs/tutorials/misc.md @@ -116,7 +116,7 @@ Each check must reside in a dedicated subfolder, following this structure: ???+ note The check name must start with the service name followed by an underscore (e.g., ec2\_instance\_public\_ip). -To see more information about how to write checks, refer to the [Developer Guide](../developer-guide/checks.md#create-a-new-check-for-a-provider). +To see more information about how to write checks, refer to the [Developer Guide](../developer-guide/checks.md#creating-a-check). ???+ note If you want to run ONLY your custom check(s), import it with -x (--checks-folder) and then run it with -c (--checks), e.g.: `console prowler aws -x s3://bucket/prowler/providers/aws/services/s3/s3_bucket_policy/ -c s3_bucket_policy` diff --git a/docs/tutorials/prowler-app.md b/docs/tutorials/prowler-app.md index 85e498aba5..8eca5cbf73 100644 --- a/docs/tutorials/prowler-app.md +++ b/docs/tutorials/prowler-app.md @@ -4,7 +4,7 @@ ## Accessing Prowler App and API Documentation -After [installing](../index.md#prowler-app-installation) **Prowler App**, access it at [http://localhost:3000](http://localhost:3000). To view the auto-generated **Prowler API** documentation, navigate to [http://localhost:8080/api/v1/docs](http://localhost:8080/api/v1/docs). This documentation provides details on available endpoints, parameters, and responses. +After [installing](../installation/prowler-app.md) **Prowler App**, access it at [http://localhost:3000](http://localhost:3000). To view the auto-generated **Prowler API** documentation, navigate to [http://localhost:8080/api/v1/docs](http://localhost:8080/api/v1/docs). This documentation provides details on available endpoints, parameters, and responses. ???+ note If you are a [Prowler Cloud](https://cloud.prowler.com/sign-in) user, you can access API docs at [https://api.prowler.com/api/v1/docs](https://api.prowler.com/api/v1/docs) @@ -109,7 +109,7 @@ For AWS, enter your `AWS Account ID` and choose one of the following methods to ### **Step 4.2: Azure Credentials**: -For Azure, Prowler App uses a service principal application to authenticate. For more information about the process of creating and adding permissions to a service principal refer to this [section](../getting-started/requirements.md#azure). When you finish creating and adding the [Entra](./azure/create-prowler-service-principal.md#assigning-the-proper-permissions) and [Subscription](./azure/subscriptions.md#assign-the-appropriate-permissions-to-the-identity-that-is-going-to-be-assumed-by-prowler) scope permissions to the service principal, enter the `Tenant ID`, `Client ID` and `Client Secret` of the service principal application. +For Azure, Prowler App uses a service principal application to authenticate. For more information about the process of creating and adding permissions to a service principal refer to this [section](../tutorials/azure/authentication.md). When you finish creating and adding the [Entra](./azure/create-prowler-service-principal.md#assigning-proper-permissions) and [Subscription](./azure/subscriptions.md) scope permissions to the service principal, enter the `Tenant ID`, `Client ID` and `Client Secret` of the service principal application. Azure Credentials diff --git a/mkdocs.yml b/mkdocs.yml index d40600452d..68aa34bcc6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -46,8 +46,19 @@ repo_name: prowler-cloud/prowler nav: - Getting Started: - - Overview: index.md - - Requirements: getting-started/requirements.md + - Overview: + - What is Prowler?: index.md + - Products: + - Prowler App: products/prowler-app.md + - Prowler CLI: products/prowler-cli.md + - Prowler Cloud 🔗: https://cloud.prowler.com + - Prowler Hub 🔗: https://hub.prowler.com + - Installation: + - Prowler App: installation/prowler-app.md + - Prowler CLI: installation/prowler-cli.md + - Basic Usage: + - Prowler App: basic-usage/prowler-app.md + - Prowler CLI: basic-usage/prowler-cli.md - Tutorials: - Prowler App: - Getting Started: tutorials/prowler-app.md @@ -57,6 +68,7 @@ nav: - Mute findings: tutorials/prowler-app-mute-findings.md - Amazon S3 Integration: tutorials/prowler-app-s3-integration.md - Lighthouse: tutorials/prowler-app-lighthouse.md + - Bulk Provider Provisioning: tutorials/bulk-provider-provisioning.md - CLI: - Miscellaneous: tutorials/misc.md - Reporting: tutorials/reporting.md @@ -111,12 +123,13 @@ nav: - Authentication: tutorials/microsoft365/authentication.md - Use of PowerShell: tutorials/microsoft365/use-of-powershell.md - GitHub: - - Authentication: tutorials/github/authentication.md - Getting Started: tutorials/github/getting-started-github.md + - Authentication: tutorials/github/authentication.md - IaC: - Getting Started: tutorials/iac/getting-started-iac.md + - Authentication: tutorials/iac/authentication.md - Developer Guide: - - General Concepts: + - Concepts: - Introduction: developer-guide/introduction.md - Providers: developer-guide/provider.md - Services: developer-guide/services.md @@ -125,7 +138,7 @@ nav: - Integrations: developer-guide/integrations.md - Compliance: developer-guide/security-compliance-framework.md - Lighthouse: developer-guide/lighthouse.md - - Provider Specific Details: + - Providers: - AWS: developer-guide/aws-details.md - Azure: developer-guide/azure-details.md - Google Cloud: developer-guide/gcp-details.md @@ -142,8 +155,8 @@ nav: - Security: security.md - Contact Us: contact.md - Troubleshooting: troubleshooting.md - - About: about.md - - Prowler Cloud: https://prowler.com + - About 🔗: https://prowler.com/about#team + - Release Notes 🔗: https://github.com/prowler-cloud/prowler/releases # Customization extra: diff --git a/poetry.lock b/poetry.lock index 55f31ad3da..ab1e7d5e4c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2060,14 +2060,14 @@ files = [ [[package]] name = "h2" -version = "4.2.0" +version = "4.3.0" description = "Pure-Python HTTP/2 protocol implementation" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "h2-4.2.0-py3-none-any.whl", hash = "sha256:479a53ad425bb29af087f3458a61d30780bc818e4ebcf01f0b536ba916462ed0"}, - {file = "h2-4.2.0.tar.gz", hash = "sha256:c8a52129695e88b1a0578d8d2cc6842bbd79128ac685463b887ee278126ad01f"}, + {file = "h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd"}, + {file = "h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1"}, ] [package.dependencies] @@ -2352,6 +2352,8 @@ python-versions = "*" groups = ["dev"] files = [ {file = "jsonpath-ng-1.7.0.tar.gz", hash = "sha256:f6f5f7fd4e5ff79c785f1573b394043b39849fb2bb47bcead935d12b00beab3c"}, + {file = "jsonpath_ng-1.7.0-py2-none-any.whl", hash = "sha256:898c93fc173f0c336784a3fa63d7434297544b7198124a68f9a3ef9597b0ae6e"}, + {file = "jsonpath_ng-1.7.0-py3-none-any.whl", hash = "sha256:f3d7f9e848cba1b6da28c55b1c26ff915dc9e0b1ba7e752a53d6da8d5cbd00b6"}, ] [package.dependencies] @@ -5839,4 +5841,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">3.9.1,<3.13" -content-hash = "a0635a7bb99427a5169b126b429d603079fc24c39ce6759a648fdffe74e50d6c" +content-hash = "fdd2cbdb6913d0dd8d05030ee41c0d36d3954e473783d43b730ec163e697ec15" diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index a93d5a0e8a..94cb8ccc05 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -9,14 +9,21 @@ All notable changes to the **Prowler SDK** are documented in this file. - `vm_sufficient_daily_backup_retention_period` check for Azure provider [(#8200)](https://github.com/prowler-cloud/prowler/pull/8200) - `vm_jit_access_enabled` check for Azure provider [(#8202)](https://github.com/prowler-cloud/prowler/pull/8202) - Bedrock AgentCore privilege escalation combination for AWS provider [(#8526)](https://github.com/prowler-cloud/prowler/pull/8526) +- Add User Email and APP name/installations information in GitHub provider [(#8501)](https://github.com/prowler-cloud/prowler/pull/8501) - Remove standalone iam:PassRole from privesc detection and add missing patterns [(#8530)](https://github.com/prowler-cloud/prowler/pull/8530) +- Support session/profile/role/static credentials in Security Hub integration [(#8539)](https://github.com/prowler-cloud/prowler/pull/8539) - `eks_cluster_deletion_protection_enabled` check for AWS provider [(#8536)](https://github.com/prowler-cloud/prowler/pull/8536) +- ECS privilege escalation patterns (StartTask and RunTask) for AWS provider [(#8541)](https://github.com/prowler-cloud/prowler/pull/8541) +- Resource Explorer enumeration v2 API actions in `cloudtrail_threat_detection_enumeration` check [(#8557)](https://github.com/prowler-cloud/prowler/pull/8557) + ### Changed - Refine kisa isms-p compliance mapping [(#8479)](https://github.com/prowler-cloud/prowler/pull/8479) - Update AWS Neptune service metadata to new format [(#8494)](https://github.com/prowler-cloud/prowler/pull/8494) +- Improve AWS Security Hub region check using multiple threads [(#8365)](https://github.com/prowler-cloud/prowler/pull/8365) ### Fixed +- Resource metadata error in `s3_bucket_shadow_resource_vulnerability` check [(#8572)](https://github.com/prowler-cloud/prowler/pull/8572) --- @@ -25,6 +32,9 @@ All notable changes to the **Prowler SDK** are documented in this file. ### Fixed - AWS resource-arn filtering [(#8533)](https://github.com/prowler-cloud/prowler/pull/8533) - GitHub App authentication for GitHub provider [(#8529)](https://github.com/prowler-cloud/prowler/pull/8529) +- List all accessible organizations in GitHub provider [(#8535)](https://github.com/prowler-cloud/prowler/pull/8535) +- Only evaluate enabled accounts in `entra_users_mfa_capable` check [(#8544)](https://github.com/prowler-cloud/prowler/pull/8544) +- GitHub Personal Access Token authentication fails without `user:email` scope [(#8580)](https://github.com/prowler-cloud/prowler/pull/8580) --- @@ -61,6 +71,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - GitHub repository and organization scoping support with `--repository/respositories` and `--organization/organizations` flags [(#8329)](https://github.com/prowler-cloud/prowler/pull/8329) - GCP provider retry configuration [(#8412)](https://github.com/prowler-cloud/prowler/pull/8412) - `s3_bucket_shadow_resource_vulnerability` check for AWS provider [(#8398)](https://github.com/prowler-cloud/prowler/pull/8398) +- Use `trivy` as engine for IaC provider [(#8466)](https://github.com/prowler-cloud/prowler/pull/8466) ### Changed - Handle some AWS errors as warnings instead of errors [(#8347)](https://github.com/prowler-cloud/prowler/pull/8347) diff --git a/prowler/lib/check/checks_loader.py b/prowler/lib/check/checks_loader.py index 3c7e065fc0..c03034b0e4 100644 --- a/prowler/lib/check/checks_loader.py +++ b/prowler/lib/check/checks_loader.py @@ -20,7 +20,7 @@ def load_checks_to_execute( ) -> set: """Generate the list of checks to execute based on the cloud provider and the input arguments given""" try: - # Bypass check loading for IAC provider since it uses Checkov directly + # Bypass check loading for IAC provider since it uses Trivy directly if provider == "iac": return set() diff --git a/prowler/lib/check/models.py b/prowler/lib/check/models.py index 1c2dd44533..de38162cbc 100644 --- a/prowler/lib/check/models.py +++ b/prowler/lib/check/models.py @@ -166,11 +166,11 @@ class CheckMetadata(BaseModel): return service_name @validator("CheckID", pre=True, always=True) - def valid_check_id(cls, check_id): + def valid_check_id(cls, check_id, values): if not check_id: raise ValueError("CheckID must be a non-empty string") - if check_id: + if check_id and values.get("Provider") != "iac": if "-" in check_id: raise ValueError( f"CheckID {check_id} contains a hyphen, which is not allowed" @@ -648,25 +648,34 @@ class CheckReportM365(Check_Report): @dataclass class CheckReportIAC(Check_Report): - """Contains the IAC Check's finding information using Checkov.""" + """Contains the IAC Check's finding information using Trivy.""" resource_name: str - resource_path: str resource_line_range: str - def __init__(self, metadata: dict = {}, finding: dict = {}) -> None: + def __init__( + self, metadata: dict = {}, finding: dict = {}, file_path: str = "" + ) -> None: """ - Initialize the IAC Check's finding information from a Checkov failed_check dict. + Initialize the IAC Check's finding information from a Trivy misconfiguration dict. Args: metadata (Dict): Optional check metadata (can be None). - failed_check (dict): A single failed_check result from Checkov's JSON output. + finding (dict): A single misconfiguration result from Trivy'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", "") + self.resource = finding + self.resource_name = file_path + self.resource_line_range = ( + ( + str(finding.get("CauseMetadata", {}).get("StartLine", "")) + + ":" + + str(finding.get("CauseMetadata", {}).get("EndLine", "")) + ) + if finding.get("CauseMetadata", {}).get("StartLine", "") + else "" + ) @dataclass diff --git a/prowler/lib/check/utils.py b/prowler/lib/check/utils.py index bf8854600d..cab19486d5 100644 --- a/prowler/lib/check/utils.py +++ b/prowler/lib/check/utils.py @@ -14,7 +14,7 @@ def recover_checks_from_provider( Returns a list of tuples with the following format (check_name, check_path) """ try: - # Bypass check loading for IAC provider since it uses Checkov directly + # Bypass check loading for IAC provider since it uses Trivy directly if provider == "iac": return [] @@ -63,7 +63,7 @@ def recover_checks_from_service(service_list: list, provider: str) -> set: Returns a set of checks from the given services """ try: - # Bypass check loading for IAC provider since it uses Checkov directly + # Bypass check loading for IAC provider since it uses Trivy directly if provider == "iac": return set() diff --git a/prowler/lib/outputs/finding.py b/prowler/lib/outputs/finding.py index a3ba6271dd..f686554a88 100644 --- a/prowler/lib/outputs/finding.py +++ b/prowler/lib/outputs/finding.py @@ -19,6 +19,7 @@ from prowler.lib.outputs.compliance.compliance import get_check_compliance from prowler.lib.outputs.utils import unroll_tags from prowler.lib.utils.utils import dict_to_lowercase, get_nested_attribute from prowler.providers.common.provider import Provider +from prowler.providers.github.models import GithubAppIdentityInfo, GithubIdentityInfo class Finding(BaseModel): @@ -250,15 +251,16 @@ class Finding(BaseModel): output_data["resource_name"] = check_output.resource_name output_data["resource_uid"] = check_output.resource_id - if hasattr(provider.identity, "account_name"): + if isinstance(provider.identity, GithubIdentityInfo): # GithubIdentityInfo (Personal Access Token, OAuth) output_data["account_name"] = provider.identity.account_name output_data["account_uid"] = provider.identity.account_id - elif hasattr(provider.identity, "app_id"): + output_data["account_email"] = provider.identity.account_email + elif isinstance(provider.identity, GithubAppIdentityInfo): # GithubAppIdentityInfo (GitHub App) - # TODO: Get Github App name - output_data["account_name"] = f"app-{provider.identity.app_id}" + output_data["account_name"] = provider.identity.app_name output_data["account_uid"] = provider.identity.app_id + output_data["installations"] = provider.identity.installations output_data["region"] = check_output.owner @@ -295,9 +297,9 @@ class Finding(BaseModel): output_data["auth_method"] = provider.auth_method 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["region"] = check_output.resource_path + output_data["resource_name"] = check_output.resource_name + output_data["resource_uid"] = check_output.resource_name + output_data["region"] = check_output.resource_line_range output_data["resource_line_range"] = check_output.resource_line_range output_data["framework"] = check_output.check_metadata.ServiceName diff --git a/prowler/lib/outputs/html/html.py b/prowler/lib/outputs/html/html.py index 7b4fb87601..4e05fe0e8a 100644 --- a/prowler/lib/outputs/html/html.py +++ b/prowler/lib/outputs/html/html.py @@ -41,7 +41,7 @@ class HTML(Output): {finding_status} {finding.metadata.Severity.value} {finding.metadata.ServiceName} - {":".join([finding.resource_metadata['file_path'], "-".join(map(str, finding.resource_metadata['file_line_range']))]) if finding.metadata.Provider == "iac" else finding.region.lower()} + {finding.region.lower()} {finding.metadata.CheckID.replace("_", "_")} {finding.metadata.CheckTitle} {finding.resource_uid.replace("<", "<").replace(">", ">").replace("_", "_")} @@ -204,7 +204,7 @@ class HTML(Output): Status Severity Service Name - {"File" if provider.type == "iac" else "Region"} + {"Line Range" if provider.type == "iac" else "Region"} Check ID Check Title Resource ID @@ -558,10 +558,65 @@ class HTML(Output): try: if hasattr(provider.identity, "account_name"): # GithubIdentityInfo (Personal Access Token, OAuth) - account_display = provider.identity.account_name + account_info_items = f""" +
  • + GitHub account: {provider.identity.account_name} +
  • + """ + # Add email if available + if ( + hasattr(provider.identity, "account_email") + and provider.identity.account_email + ): + account_info_items += f""" +
  • + GitHub account email: {provider.identity.account_email} +
  • """ elif hasattr(provider.identity, "app_id"): # GithubAppIdentityInfo (GitHub App) - account_display = f"app-{provider.identity.app_id}" + # Assessment items: App Name and Installations + account_info_items = f""" +
  • + GitHub App Name: {provider.identity.app_name} +
  • """ + # Add installations if available + if ( + hasattr(provider.identity, "installations") + and provider.identity.installations + ): + installations_display = ", ".join(provider.identity.installations) + account_info_items += f""" +
  • + Installations: {installations_display} +
  • """ + else: + account_info_items += """ +
  • + Installations: No installations found +
  • """ + + # Credentials items: Authentication method and App ID + credentials_items = f""" +
  • + GitHub authentication method: {provider.auth_method} +
  • +
  • + GitHub App ID: {provider.identity.app_id} +
  • """ + else: + # Fallback for other identity types + account_info_items = "" + credentials_items = f""" +
  • + GitHub authentication method: {provider.auth_method} +
  • """ + + # For PAT/OAuth, use default credentials structure + if hasattr(provider.identity, "account_name"): + credentials_items = f""" +
  • + GitHub authentication method: {provider.auth_method} +
  • """ return f"""
    @@ -569,11 +624,8 @@ class HTML(Output):
    GitHub Assessment Summary
    -
      -
    • - GitHub account: {account_display} -
    • +
        + {account_info_items}
    @@ -582,11 +634,8 @@ class HTML(Output):
    GitHub Credentials
    -
      -
    • - GitHub authentication method: {provider.auth_method} -
    • +
        + {credentials_items}
      """ diff --git a/prowler/lib/outputs/summary_table.py b/prowler/lib/outputs/summary_table.py index 2e70115256..a6e011755a 100644 --- a/prowler/lib/outputs/summary_table.py +++ b/prowler/lib/outputs/summary_table.py @@ -86,6 +86,8 @@ def display_summary_table( "Muted": [], } pass_count = fail_count = muted_count = 0 + # Sort findings by ServiceName + findings.sort(key=lambda x: x.check_metadata.ServiceName) for finding in findings: # If new service and not first, add previous row if ( diff --git a/prowler/providers/aws/aws_regions_by_service.json b/prowler/providers/aws/aws_regions_by_service.json index 7943a1078f..c06eb999ac 100644 --- a/prowler/providers/aws/aws_regions_by_service.json +++ b/prowler/providers/aws/aws_regions_by_service.json @@ -1362,6 +1362,7 @@ "ap-south-2", "ap-southeast-1", "ap-southeast-2", + "ap-southeast-3", "ap-southeast-4", "ca-central-1", "eu-central-1", @@ -1395,6 +1396,7 @@ "ap-south-2", "ap-southeast-1", "ap-southeast-2", + "ap-southeast-3", "ap-southeast-4", "ca-central-1", "eu-central-1", @@ -9666,7 +9668,10 @@ "us-west-2" ], "aws-cn": [], - "aws-us-gov": [] + "aws-us-gov": [ + "us-gov-east-1", + "us-gov-west-1" + ] } }, "s3": { diff --git a/prowler/providers/aws/lib/security_hub/security_hub.py b/prowler/providers/aws/lib/security_hub/security_hub.py index 00a1d1c044..b2809b25a4 100644 --- a/prowler/providers/aws/lib/security_hub/security_hub.py +++ b/prowler/providers/aws/lib/security_hub/security_hub.py @@ -1,18 +1,47 @@ +import os +from concurrent.futures import ThreadPoolExecutor, as_completed from dataclasses import dataclass from typing import Optional from boto3 import Session from botocore.client import ClientError +from botocore.exceptions import NoCredentialsError, ProfileNotFound from prowler.config.config import timestamp_utc from prowler.lib.logger import logger from prowler.lib.outputs.asff.asff import AWSSecurityFindingFormat 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.arn.arn import parse_iam_credentials_arn from prowler.providers.aws.lib.security_hub.exceptions.exceptions import ( SecurityHubInvalidRegionError, SecurityHubNoEnabledRegionsError, ) from prowler.providers.aws.lib.session.aws_set_up_session import AwsSetUpSession +from prowler.providers.aws.models import AWSAssumeRoleInfo from prowler.providers.common.models import Connection SECURITY_HUB_INTEGRATION_NAME = "prowler/prowler" @@ -26,10 +55,12 @@ class SecurityHubConnection(Connection): Attributes: enabled_regions (set): Set of regions where Security Hub is enabled. disabled_regions (set): Set of regions where Security Hub is disabled. + partition (str): AWS partition (e.g., aws, aws-cn, aws-us-gov) where SecurityHub is deployed. """ enabled_regions: set = None disabled_regions: set = None + partition: str = "" class SecurityHub: @@ -61,15 +92,15 @@ class SecurityHub: def __init__( self, aws_account_id: str, - aws_partition: str, + aws_partition: str = None, aws_session: Session = None, findings: list[AWSSecurityFindingFormat] = [], aws_security_hub_available_regions: list[str] = [], send_only_fails: bool = False, role_arn: str = None, - session_duration: int = None, + session_duration: int = 3600, external_id: str = None, - role_session_name: str = None, + role_session_name: str = ROLE_SESSION_NAME, mfa: bool = None, profile: str = None, aws_access_key_id: str = None, @@ -87,7 +118,7 @@ class SecurityHub: - aws_partition (str): AWS partition (e.g., aws, aws-cn, aws-us-gov) where SecurityHub is deployed. - findings (list[AWSSecurityFindingFormat]): List of findings to filter and send to Security Hub. - aws_security_hub_available_regions (list[str]): List of regions where Security Hub is available. - - send_only_fails (bool): Flag indicating whether to send only findings with status 'FAILED'. + - send_only_fails (bool): Flag indicating whether to send only findings with status 'FAIL'. - role_arn: The ARN of the IAM role to assume. - session_duration: The duration of the session in seconds, between 900 and 43200. - external_id: The external ID to use when assuming the IAM role. @@ -116,8 +147,12 @@ class SecurityHub: retries_max_attempts=retries_max_attempts, regions=regions, ) - self._session = aws_setup_session._session + self._session = aws_setup_session._session.current_session self._aws_account_id = aws_account_id + if not aws_partition: + aws_partition = AwsProvider.validate_credentials( + self._session, AWS_STS_GLOBAL_ENDPOINT_REGION + ).arn.partition self._aws_partition = aws_partition self._enabled_regions = None @@ -126,7 +161,7 @@ class SecurityHub: if aws_security_hub_available_regions: self._enabled_regions = self.verify_enabled_per_region( aws_security_hub_available_regions, - aws_session, + self._session, aws_account_id, aws_partition, ) @@ -143,7 +178,7 @@ class SecurityHub: Args: findings (list[AWSSecurityFindingFormat]): List of findings to filter. - send_only_fails (bool): Flag indicating whether to send only findings with status 'FAILED'. + send_only_fails (bool): Flag indicating whether to send only findings with status 'FAIL'. Returns: dict: A dictionary containing findings per region after applying the filtering criteria. @@ -178,6 +213,69 @@ class SecurityHub: ) return findings_per_region + @staticmethod + def _check_region_security_hub( + region: str, + session: Session, + aws_account_id: str, + aws_partition: str, + ) -> tuple[str, Session | None]: + """ + Check if Security Hub is enabled in a specific region and if Prowler integration is active. + + Args: + region (str): AWS region to check. + session (Session): AWS session object. + aws_account_id (str): AWS account ID. + aws_partition (str): AWS partition. + + Returns: + tuple: (region, client or None) - Returns client if enabled, None otherwise. + """ + try: + logger.info( + f"Checking if the {SECURITY_HUB_INTEGRATION_NAME} is enabled in the {region} region." + ) + # Check if security hub is enabled in current region + security_hub_client = session.client("securityhub", region_name=region) + security_hub_client.describe_hub() + + # Check if Prowler integration is enabled in Security Hub + security_hub_prowler_integration_arn = f"arn:{aws_partition}:securityhub:{region}:{aws_account_id}:product-subscription/{SECURITY_HUB_INTEGRATION_NAME}" + if security_hub_prowler_integration_arn not in str( + security_hub_client.list_enabled_products_for_import() + ): + logger.warning( + f"Security Hub is enabled in {region} but Prowler integration does not accept findings. More info: https://docs.prowler.cloud/en/latest/tutorials/aws/securityhub/" + ) + return region, None + else: + return region, session.client("securityhub", region_name=region) + + # Handle all the permissions / configuration errors + except ClientError as client_error: + # Check if Account is subscribed to Security Hub + error_code = client_error.response["Error"]["Code"] + error_message = client_error.response["Error"]["Message"] + if ( + error_code == "InvalidAccessException" + and f"Account {aws_account_id} is not subscribed to AWS Security Hub" + in error_message + ): + logger.warning( + f"{client_error.__class__.__name__} -- [{client_error.__traceback__.tb_lineno}]: {client_error}" + ) + else: + logger.error( + f"{client_error.__class__.__name__} -- [{client_error.__traceback__.tb_lineno}]: {client_error}" + ) + return region, None + except Exception as error: + logger.error( + f"{error.__class__.__name__} -- [{error.__traceback__.tb_lineno}]: {error}" + ) + return region, None + @staticmethod def verify_enabled_per_region( aws_security_hub_available_regions: list[str], @@ -195,49 +293,34 @@ class SecurityHub: dict: A dictionary containing enabled regions with SecurityHub clients. """ enabled_regions = {} - for region in aws_security_hub_available_regions: - try: - logger.info( - f"Checking if the {SECURITY_HUB_INTEGRATION_NAME} is enabled in the {region} region." - ) - # Check if security hub is enabled in current region - security_hub_client = session.client("securityhub", region_name=region) - security_hub_client.describe_hub() - # Check if Prowler integration is enabled in Security Hub - security_hub_prowler_integration_arn = f"arn:{aws_partition}:securityhub:{region}:{aws_account_id}:product-subscription/{SECURITY_HUB_INTEGRATION_NAME}" - if security_hub_prowler_integration_arn not in str( - security_hub_client.list_enabled_products_for_import() - ): - logger.warning( - f"Security Hub is enabled in {region} but Prowler integration does not accept findings. More info: https://docs.prowler.cloud/en/latest/tutorials/aws/securityhub/" - ) - else: - enabled_regions[region] = session.client( - "securityhub", region_name=region - ) + # Use ThreadPoolExecutor to check regions in parallel + with ThreadPoolExecutor( + max_workers=min(len(aws_security_hub_available_regions), 20) + ) as executor: + # Submit all region checks + future_to_region = { + executor.submit( + SecurityHub._check_region_security_hub, + region, + session, + aws_account_id, + aws_partition, + ): region + for region in aws_security_hub_available_regions + } - # Handle all the permissions / configuration errors - except ClientError as client_error: - # Check if Account is subscribed to Security Hub - error_code = client_error.response["Error"]["Code"] - error_message = client_error.response["Error"]["Message"] - if ( - error_code == "InvalidAccessException" - and f"Account {aws_account_id} is not subscribed to AWS Security Hub" - in error_message - ): - logger.warning( - f"{client_error.__class__.__name__} -- [{client_error.__traceback__.tb_lineno}]: {client_error}" - ) - else: + # Collect results as they complete + for future in as_completed(future_to_region): + try: + region, client = future.result() + if client is not None: + enabled_regions[region] = client + except Exception as error: logger.error( - f"{client_error.__class__.__name__} -- [{client_error.__traceback__.tb_lineno}]: {client_error}" + f"Error checking region {future_to_region[future]}: {error.__class__.__name__} -- [{error.__traceback__.tb_lineno}]: {error}" ) - except Exception as error: - logger.error( - f"{error.__class__.__name__} -- [{error.__traceback__.tb_lineno}]: {error}" - ) + return enabled_regions def batch_send_to_security_hub( @@ -363,34 +446,91 @@ class SecurityHub: @staticmethod def test_connection( - session: Session, aws_account_id: str, - aws_partition: str, + aws_partition: str = None, regions: set = None, raise_on_exception: bool = True, + 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, + aws_access_key_id: str = None, + aws_secret_access_key: str = None, + aws_session_token: Optional[str] = None, ) -> SecurityHubConnection: """ Test the connection to AWS Security Hub by checking if Security Hub is enabled in the provided region and if the Prowler integration is active. Args: - session (Session): AWS session to use for authentication. - regions (set): Set of regions to check for Security Hub integration. aws_account_id (str): AWS account ID to check for Prowler integration. aws_partition (str): AWS partition (e.g., aws, aws-cn, aws-us-gov). + regions (set): Set of regions to check for Security Hub integration. raise_on_exception (bool): Whether to raise an exception if an error occurs. + profile (str): AWS profile name to use for authentication. + aws_region (str): AWS region to use for the session. + role_arn (str): ARN of the IAM role to assume. + role_session_name (str): Name for the role session. + session_duration (int): Duration of the role session in seconds. + external_id (str): External ID to use when assuming the role. + mfa_enabled (bool): Whether MFA is enabled. + aws_access_key_id (str): AWS access key ID. + aws_secret_access_key (str): AWS secret access key. + aws_session_token (str): AWS session token. Returns: - Connection: An object that contains the result of the test connection operation. + SecurityHubConnection: An object that contains the result of the test connection operation. - is_connected (bool): Indicates whether the connection was successful. - error (Exception): An exception object if an error occurs during the connection test. - enabled_regions (set): Set of regions where Security Hub is enabled. - disabled_regions (set): Set of regions where Security Hub is disabled. + - enabled_regions (set): Set of regions where Security Hub is enabled. + - disabled_regions (set): Set of regions where Security Hub is disabled. """ try: disabled_regions = set() enabled_regions = set() + # Set up AWS session + 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 not aws_partition: + aws_partition = AwsProvider.validate_credentials( + session, aws_region + ).arn.partition + + # Handle role assumption if role_arn is provided + if role_arn: + session_duration = validate_session_duration(session_duration) + role_session_name = validate_role_session_name( + role_session_name or 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, + ) + all_regions = AwsProvider.get_available_aws_service_regions( service="securityhub", partition=aws_partition ) @@ -427,6 +567,7 @@ class SecurityHub: error=None, enabled_regions=enabled_regions, disabled_regions=disabled_regions, + partition=aws_partition, ) if len(enabled_regions) == 0: @@ -454,10 +595,206 @@ class SecurityHub: error=None, enabled_regions=enabled_regions, disabled_regions=disabled_regions, + partition=aws_partition, ) + 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 SecurityHubConnection( + is_connected=False, + error=setup_session_error, + enabled_regions=set(), + disabled_regions=set(), + ) + + 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 SecurityHubConnection( + is_connected=False, + error=validation_error, + enabled_regions=set(), + disabled_regions=set(), + ) + + 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 SecurityHubConnection( + is_connected=False, + error=arn_region_not_empty_error, + enabled_regions=set(), + disabled_regions=set(), + ) + + 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 SecurityHubConnection( + is_connected=False, + error=arn_partition_empty_error, + enabled_regions=set(), + disabled_regions=set(), + ) + + 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 SecurityHubConnection( + is_connected=False, + error=arn_service_not_iam_sts_error, + enabled_regions=set(), + disabled_regions=set(), + ) + + 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 SecurityHubConnection( + is_connected=False, + error=arn_invalid_account_id_error, + enabled_regions=set(), + disabled_regions=set(), + ) + + 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 SecurityHubConnection( + is_connected=False, + error=arn_invalid_resource_type_error, + enabled_regions=set(), + disabled_regions=set(), + ) + + 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 SecurityHubConnection( + is_connected=False, + error=arn_empty_resource_error, + enabled_regions=set(), + disabled_regions=set(), + ) + + 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 SecurityHubConnection( + is_connected=False, + error=assume_role_error, + enabled_regions=set(), + disabled_regions=set(), + ) + + 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 SecurityHubConnection( + is_connected=False, + error=profile_not_found_error, + enabled_regions=set(), + disabled_regions=set(), + ) + + 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 SecurityHubConnection( + is_connected=False, + error=no_credentials_error, + enabled_regions=set(), + disabled_regions=set(), + ) + + 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 SecurityHubConnection( + is_connected=False, + error=access_key_id_invalid_error, + enabled_regions=set(), + disabled_regions=set(), + ) + + 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 SecurityHubConnection( + is_connected=False, + error=secret_access_key_invalid_error, + enabled_regions=set(), + disabled_regions=set(), + ) + + 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 SecurityHubConnection( + is_connected=False, + error=session_token_expired, + enabled_regions=set(), + disabled_regions=set(), + ) + except Exception as error: logger.error( f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) - raise error + if raise_on_exception: + raise error + return SecurityHubConnection( + is_connected=False, + error=error, + enabled_regions=set(), + disabled_regions=set(), + ) diff --git a/prowler/providers/aws/services/cloudtrail/cloudtrail_threat_detection_enumeration/cloudtrail_threat_detection_enumeration.py b/prowler/providers/aws/services/cloudtrail/cloudtrail_threat_detection_enumeration/cloudtrail_threat_detection_enumeration.py index faef4f9772..6b6af71f02 100644 --- a/prowler/providers/aws/services/cloudtrail/cloudtrail_threat_detection_enumeration/cloudtrail_threat_detection_enumeration.py +++ b/prowler/providers/aws/services/cloudtrail/cloudtrail_threat_detection_enumeration/cloudtrail_threat_detection_enumeration.py @@ -6,6 +6,7 @@ from prowler.providers.aws.services.cloudtrail.cloudtrail_client import ( ) default_threat_detection_enumeration_actions = [ + "CreateIndex", "DescribeAccessEntry", "DescribeAccountAttributes", "DescribeAvailabilityZones", @@ -86,6 +87,7 @@ default_threat_detection_enumeration_actions = [ "ListOrganizationalUnitsForParent", "ListOriginationNumbers", "ListPolicyVersions", + "ListResources", "ListRoles", "ListRoles", "ListRules", diff --git a/prowler/providers/aws/services/iam/lib/privilege_escalation.py b/prowler/providers/aws/services/iam/lib/privilege_escalation.py index 9a4a67d3ab..b2545dfce0 100644 --- a/prowler/providers/aws/services/iam/lib/privilege_escalation.py +++ b/prowler/providers/aws/services/iam/lib/privilege_escalation.py @@ -106,6 +106,18 @@ privilege_escalation_policies_combination = { "bedrock-agentcore:CreateCodeInterpreter", "bedrock-agentcore:InvokeCodeInterpreter", }, + # ECS-based privilege escalation patterns + # Reference: https://labs.reversec.com/posts/2025/08/another-ecs-privilege-escalation-path + "PassRole+ECS+StartTask": { + "iam:PassRole", + "ecs:StartTask", + "ecs:RegisterContainerInstance", + "ecs:DeregisterContainerInstance", + }, + "PassRole+ECS+RunTask": { + "iam:PassRole", + "ecs:RunTask", + }, # TO-DO: We have to handle AssumeRole just if the resource is * and without conditions # "sts:AssumeRole": {"sts:AssumeRole"}, } diff --git a/prowler/providers/aws/services/s3/s3_bucket_shadow_resource_vulnerability/s3_bucket_shadow_resource_vulnerability.metadata.json b/prowler/providers/aws/services/s3/s3_bucket_shadow_resource_vulnerability/s3_bucket_shadow_resource_vulnerability.metadata.json index 4c9717f59e..8d071bbd34 100644 --- a/prowler/providers/aws/services/s3/s3_bucket_shadow_resource_vulnerability/s3_bucket_shadow_resource_vulnerability.metadata.json +++ b/prowler/providers/aws/services/s3/s3_bucket_shadow_resource_vulnerability/s3_bucket_shadow_resource_vulnerability.metadata.json @@ -3,7 +3,7 @@ "CheckID": "s3_bucket_shadow_resource_vulnerability", "CheckTitle": "Check for S3 buckets vulnerable to Shadow Resource Hijacking (Bucket Monopoly)", "CheckType": [ - "" + "Effects/Data Exposure" ], "ServiceName": "s3", "SubServiceName": "", diff --git a/prowler/providers/aws/services/s3/s3_bucket_shadow_resource_vulnerability/s3_bucket_shadow_resource_vulnerability.py b/prowler/providers/aws/services/s3/s3_bucket_shadow_resource_vulnerability/s3_bucket_shadow_resource_vulnerability.py index f18855da30..eb509b1c85 100644 --- a/prowler/providers/aws/services/s3/s3_bucket_shadow_resource_vulnerability/s3_bucket_shadow_resource_vulnerability.py +++ b/prowler/providers/aws/services/s3/s3_bucket_shadow_resource_vulnerability/s3_bucket_shadow_resource_vulnerability.py @@ -70,22 +70,12 @@ class s3_bucket_shadow_resource_vulnerability(Check): ) # Check if this bucket exists in another account if s3_client._head_bucket(bucket_name): - # Create a virtual bucket object for reporting - virtual_bucket = type( - "obj", - (object,), - { - "name": bucket_name, - "region": region, - "arn": f"arn:{s3_client.audited_partition}:s3:::{bucket_name}", - "tags": [], - }, - )() - - report = Check_Report_AWS(self.metadata(), resource=virtual_bucket) + report = Check_Report_AWS(self.metadata(), resource={}) report.region = region report.resource_id = bucket_name - report.resource_arn = virtual_bucket.arn + report.resource_arn = ( + f"arn:{s3_client.audited_partition}:s3:::{bucket_name}" + ) report.resource_tags = [] report.status = "FAIL" report.status_extended = f"S3 bucket {bucket_name} for service {service} is a known shadow resource that exists and is owned by another account." diff --git a/prowler/providers/common/provider.py b/prowler/providers/common/provider.py index 5f3ee07160..1b2de71148 100644 --- a/prowler/providers/common/provider.py +++ b/prowler/providers/common/provider.py @@ -252,7 +252,7 @@ class Provider(ABC): provider_class( scan_path=arguments.scan_path, scan_repository_url=arguments.scan_repository_url, - frameworks=arguments.frameworks, + scanners=arguments.scanners, exclude_path=arguments.exclude_path, config_path=arguments.config_file, fixer_config=fixer_config, diff --git a/prowler/providers/github/github_provider.py b/prowler/providers/github/github_provider.py index 5b86e9c76a..46dff4ab09 100644 --- a/prowler/providers/github/github_provider.py +++ b/prowler/providers/github/github_provider.py @@ -350,10 +350,22 @@ class GithubProvider(Provider): auth = Auth.Token(session.token) g = Github(auth=auth, retry=retry_config) try: + user = g.get_user() + # Try to get email if the token has the necessary scope + account_email = None + try: + emails = user.get_emails() + if emails: + account_email = emails[0].email + except Exception: + # Token doesn't have user:email scope or other API error + pass + identity = GithubIdentityInfo( - account_id=g.get_user().id, - account_name=g.get_user().login, - account_url=g.get_user().url, + account_id=user.id, + account_name=user.login, + account_url=user.url, + account_email=account_email, ) return identity @@ -371,8 +383,10 @@ class GithubProvider(Provider): installation.raw_data.get("account", {}).get("login") ) try: + app = gi.get_app() identity = GithubAppIdentityInfo( - app_id=gi.get_app().id, + app_id=app.id, + app_name=app.name, installations=installations, ) return identity @@ -401,11 +415,18 @@ class GithubProvider(Provider): report_lines = [ f"GitHub Account: {Fore.YELLOW}{self.identity.account_name}{Style.RESET_ALL}", f"GitHub Account ID: {Fore.YELLOW}{self.identity.account_id}{Style.RESET_ALL}", - f"Authentication Method: {Fore.YELLOW}{self.auth_method}{Style.RESET_ALL}", ] + if self.identity.account_email: + report_lines.append( + f"GitHub Account Email: {Fore.YELLOW}{self.identity.account_email}{Style.RESET_ALL}" + ) + report_lines.append( + f"Authentication Method: {Fore.YELLOW}{self.auth_method}{Style.RESET_ALL}" + ) elif isinstance(self.identity, GithubAppIdentityInfo): report_lines = [ f"GitHub App ID: {Fore.YELLOW}{self.identity.app_id}{Style.RESET_ALL}", + f"GitHub App Name: {Fore.YELLOW}{self.identity.app_name}{Style.RESET_ALL}", f"Authentication Method: {Fore.YELLOW}{self.auth_method}{Style.RESET_ALL}", ] report_title = ( diff --git a/prowler/providers/github/models.py b/prowler/providers/github/models.py index bc1c382bc2..cf92125305 100644 --- a/prowler/providers/github/models.py +++ b/prowler/providers/github/models.py @@ -1,3 +1,5 @@ +from typing import Optional + from pydantic.v1 import BaseModel from prowler.config.config import output_file_timestamp @@ -14,10 +16,12 @@ class GithubIdentityInfo(BaseModel): account_id: str account_name: str account_url: str + account_email: Optional[str] = None class GithubAppIdentityInfo(BaseModel): app_id: str + app_name: str installations: list[str] diff --git a/prowler/providers/github/services/organization/organization_service.py b/prowler/providers/github/services/organization/organization_service.py index cf57f2c799..9bbc5f2671 100644 --- a/prowler/providers/github/services/organization/organization_service.py +++ b/prowler/providers/github/services/organization/organization_service.py @@ -5,6 +5,7 @@ from pydantic.v1 import BaseModel from prowler.lib.logger import logger from prowler.providers.github.lib.service.service import GithubService +from prowler.providers.github.models import GithubAppIdentityInfo, GithubIdentityInfo class Organization(GithubService): @@ -113,8 +114,15 @@ class Organization(GithubService): elif not self.provider.repositories: # Default behavior: get all organizations the user is a member of # Only when no repositories are specified - for org in client.get_user().get_orgs(): - self._process_organization(org, organizations) + if isinstance(self.provider.identity, GithubIdentityInfo): + orgs = client.get_user().get_orgs() + for org in orgs: + self._process_organization(org, organizations) + elif isinstance(self.provider.identity, GithubAppIdentityInfo): + orgs = client.get_organizations() + if orgs.totalCount > 0: + for org in orgs: + self._process_organization(org, organizations) except github.RateLimitExceededException as error: logger.error(f"GitHub API rate limit exceeded: {error}") diff --git a/prowler/providers/iac/iac_provider.py b/prowler/providers/iac/iac_provider.py index 908f307967..cced114872 100644 --- a/prowler/providers/iac/iac_provider.py +++ b/prowler/providers/iac/iac_provider.py @@ -29,7 +29,7 @@ class IacProvider(Provider): self, scan_path: str = ".", scan_repository_url: str = None, - frameworks: list[str] = ["all"], + scanners: list[str] = ["vuln", "misconfig", "secret"], exclude_path: list[str] = [], config_path: str = None, config_content: dict = None, @@ -42,7 +42,7 @@ class IacProvider(Provider): self.scan_path = scan_path self.scan_repository_url = scan_repository_url - self.frameworks = frameworks + self.scanners = scanners self.exclude_path = exclude_path self.region = "global" self.audited_account = "local-iac" @@ -90,7 +90,7 @@ class IacProvider(Provider): # Fixer Config self._fixer_config = fixer_config - # Mutelist (not needed for IAC since Checkov has its own mutelist logic) + # Mutelist (not needed for IAC since Trivy has its own mutelist logic) self._mutelist = None self.audit_metadata = Audit_Metadata( @@ -131,41 +131,50 @@ class IacProvider(Provider): return self._fixer_config def setup_session(self): - """IAC provider doesn't need a session since it uses Checkov directly""" + """IAC provider doesn't need a session since it uses Trivy directly""" return None - def _process_check(self, finding: dict, check: dict, status: str) -> CheckReportIAC: + def _process_finding( + self, finding: dict, file_path: str, type: str + ) -> CheckReportIAC: """ Process a single check (failed or passed) and create a CheckReportIAC object. Args: - finding: The finding object from Checkov output - check: The individual check data (failed_check or passed_check) - status: The status of the check ("FAIL" or "PASS") + finding: The finding object from Trivy output + file_path: The path to the file that contains the finding + type: The type of the finding Returns: CheckReportIAC: The processed check report """ try: + if "VulnerabilityID" in finding: + finding_id = finding["VulnerabilityID"] + finding_description = finding["Description"] + finding_status = finding.get("Status", "FAIL") + elif "RuleID" in finding: + finding_id = finding["RuleID"] + finding_description = finding["Title"] + finding_status = finding.get("Status", "FAIL") + else: + finding_id = finding["ID"] + finding_description = finding["Description"] + finding_status = finding["Status"] + metadata_dict = { "Provider": "iac", - "CheckID": check.get("check_id", ""), - "CheckTitle": check.get("check_name", ""), + "CheckID": finding_id, + "CheckTitle": finding["Title"], "CheckType": ["Infrastructure as Code"], - "ServiceName": finding["check_type"], + "ServiceName": type, "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": ( - check.get("severity", "low").lower() - if check.get("severity") - else "low" - ), + "Severity": finding["Severity"], "ResourceType": "iac", - "Description": check.get("check_name", ""), + "Description": finding_description, "Risk": "", - "RelatedUrl": ( - check.get("guideline", "") if check.get("guideline") else "" - ), + "RelatedUrl": finding.get("PrimaryURL", ""), "Remediation": { "Code": { "NativeIaC": "", @@ -174,10 +183,8 @@ class IacProvider(Provider): "Other": "", }, "Recommendation": { - "Text": "", - "Url": ( - check.get("guideline", "") if check.get("guideline") else "" - ), + "Text": finding.get("Resolution", ""), + "Url": finding.get("PrimaryURL", ""), }, }, "Categories": [], @@ -189,11 +196,16 @@ class IacProvider(Provider): # 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 = CheckReportIAC( + metadata=metadata, finding=finding, file_path=file_path + ) + report.status = finding_status + report.status_extended = ( + finding.get("Message", "") + if finding.get("Message") + else finding.get("Description", "") + ) + if finding_status == "MUTED": report.muted = True return report except Exception as error: @@ -213,6 +225,8 @@ class IacProvider(Provider): Clone a git repository to a temporary directory, supporting GitHub authentication. """ try: + original_url = repository_url + if github_username and personal_access_token: repository_url = repository_url.replace( "https://github.com/", @@ -226,7 +240,7 @@ class IacProvider(Provider): temporary_directory = tempfile.mkdtemp() logger.info( - f"Cloning repository {repository_url} into {temporary_directory}..." + f"Cloning repository {original_url} into {temporary_directory}..." ) with alive_bar( ctrl_c=False, @@ -236,7 +250,7 @@ class IacProvider(Provider): enrich_print=False, ) as bar: try: - bar.title = f"-> Cloning {repository_url}..." + bar.title = f"-> Cloning {original_url}..." porcelain.clone(repository_url, temporary_directory, depth=1) bar.title = "-> Repository cloned successfully!" except Exception as clone_error: @@ -261,7 +275,7 @@ class IacProvider(Provider): scan_dir = self.scan_path try: - reports = self.run_scan(scan_dir, self.frameworks, self.exclude_path) + reports = self.run_scan(scan_dir, self.scanners, self.exclude_path) finally: if temp_dir: logger.info(f"Removing temporary directory {temp_dir}...") @@ -270,35 +284,76 @@ class IacProvider(Provider): return reports def run_scan( - self, directory: str, frameworks: list[str], exclude_path: list[str] + self, directory: str, scanners: list[str], exclude_path: list[str] ) -> List[CheckReportIAC]: try: logger.info(f"Running IaC scan on {directory} ...") - checkov_command = [ - "checkov", - "-d", + trivy_command = [ + "trivy", + "fs", directory, - "-o", + "--format", "json", - "-f", - ",".join(frameworks), + "--scanners", + ",".join(scanners), + "--parallel", + "0", + "--include-non-failures", ] if exclude_path: - checkov_command.extend(["--skip-path", ",".join(exclude_path)]) - # Run Checkov with JSON output - process = subprocess.run( - checkov_command, - capture_output=True, - text=True, - ) - # Log Checkov's error output if any + trivy_command.extend(["--skip-dirs", ",".join(exclude_path)]) + with alive_bar( + ctrl_c=False, + bar="blocks", + spinner="classic", + stats=False, + enrich_print=False, + ) as bar: + try: + bar.title = f"-> Running IaC scan on {directory} ..." + # Run Trivy with JSON output + process = subprocess.run( + trivy_command, + capture_output=True, + text=True, + ) + bar.title = "-> Scan completed!" + except Exception as error: + bar.title = "-> Scan failed!" + raise error + # Log Trivy's stderr output with preserved log levels if process.stderr: - logger.error(process.stderr) + for line in process.stderr.strip().split("\n"): + if line.strip(): + # Parse Trivy's log format to extract level and message + # Trivy format: timestamp level message + parts = line.split() + if len(parts) >= 3: + # Extract level and message + level = parts[1] + message = " ".join(parts[2:]) + + # Map Trivy log levels to Python logging levels + if level == "ERROR": + logger.error(f"{message}") + elif level == "WARN": + logger.warning(f"{message}") + elif level == "INFO": + logger.info(f"{message}") + elif level == "DEBUG": + logger.debug(f"{message}") + else: + # Default to info for unknown levels + logger.info(f"{message}") + else: + # If we can't parse the format, log as info + logger.info(f"{line}") try: - output = json.loads(process.stdout) + output = json.loads(process.stdout)["Results"] + if not output: - logger.warning("No findings returned from Checkov scan") + logger.warning("No findings returned from Trivy scan") return [] except Exception as error: logger.critical( @@ -308,37 +363,41 @@ class IacProvider(Provider): 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] - - # Process all frameworks findings + # Process all trivy findings for finding in output: - results = finding.get("results", {}) - # Process failed checks - failed_checks = results.get("failed_checks", []) - for failed_check in failed_checks: - report = self._process_check(finding, failed_check, "FAIL") + # Process Misconfigurations + for misconfiguration in finding.get("Misconfigurations", []): + report = self._process_finding( + misconfiguration, finding["Target"], finding["Type"] + ) 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") + # Process Vulnerabilities + for vulnerability in finding.get("Vulnerabilities", []): + report = self._process_finding( + vulnerability, finding["Target"], finding["Type"] + ) 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") + # Process Secrets + for secret in finding.get("Secrets", []): + report = self._process_finding( + secret, finding["Target"], finding["Class"] + ) + reports.append(report) + # Process Licenses + for license in finding.get("Licenses", []): + report = self._process_finding( + license, finding["Target"], finding["Type"] + ) reports.append(report) return reports except Exception as error: - if "No such file or directory: 'checkov'" in str(error): - logger.critical("Please, install checkov using 'pip install checkov'") + if "No such file or directory: 'trivy'" in str(error): + logger.critical( + "Trivy binary not found. Please install Trivy from https://trivy.dev/latest/getting-started/installation/ or use your system package manager (e.g., 'brew install trivy' on macOS, 'apt-get install trivy' on Ubuntu)" + ) sys.exit(1) logger.critical( f"{error.__class__.__name__}:{error.__traceback__.tb_lineno} -- {error}" @@ -367,7 +426,7 @@ class IacProvider(Provider): ) report_lines.append( - f"Frameworks: {Fore.YELLOW}{', '.join(self.frameworks)}{Style.RESET_ALL}" + f"Scanners: {Fore.YELLOW}{', '.join(self.scanners)}{Style.RESET_ALL}" ) report_lines.append( diff --git a/prowler/providers/iac/lib/arguments/arguments.py b/prowler/providers/iac/lib/arguments/arguments.py index 6adadeca86..30ebeb8db2 100644 --- a/prowler/providers/iac/lib/arguments/arguments.py +++ b/prowler/providers/iac/lib/arguments/arguments.py @@ -1,33 +1,8 @@ -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", +SCANNERS_CHOICES = [ + "vuln", + "misconfig", + "secret", + "license", ] @@ -56,14 +31,13 @@ def init_parser(self): ) iac_scan_subparser.add_argument( - "--frameworks", - "-f", - "--framework", - dest="frameworks", + "--scanners", + "--scanner", + dest="scanners", nargs="+", - default=["all"], - choices=FRAMEWORK_CHOICES, - help="Comma-separated list of frameworks to scan. Default: all", + default=["vuln", "misconfig", "secret"], + choices=SCANNERS_CHOICES, + help="Comma-separated list of scanners to scan. Default: vuln, misconfig, secret", ) iac_scan_subparser.add_argument( "--exclude-path", diff --git a/prowler/providers/m365/lib/powershell/m365_powershell.py b/prowler/providers/m365/lib/powershell/m365_powershell.py index 3cd2dda4df..26380ae2a2 100644 --- a/prowler/providers/m365/lib/powershell/m365_powershell.py +++ b/prowler/providers/m365/lib/powershell/m365_powershell.py @@ -982,6 +982,20 @@ class M365PowerShell(PowerShellSession): """ return self.execute("Get-SharingPolicy | ConvertTo-Json", json_parse=True) + def get_user_account_status(self) -> dict: + """ + Get User Account Status. + + Retrieves the current user account status settings for Exchange Online. + + Returns: + dict: User account status settings in JSON format. + """ + return self.execute( + "$dict=@{}; Get-User -ResultSize Unlimited | ForEach-Object { $dict[$_.Id] = @{ AccountDisabled = $_.AccountDisabled } }; $dict | ConvertTo-Json", + json_parse=True, + ) + # This function is used to install the required M365 PowerShell modules in Docker containers def initialize_m365_powershell_modules(): diff --git a/prowler/providers/m365/services/entra/entra_service.py b/prowler/providers/m365/services/entra/entra_service.py index e7159e1787..2cf160a940 100644 --- a/prowler/providers/m365/services/entra/entra_service.py +++ b/prowler/providers/m365/services/entra/entra_service.py @@ -14,7 +14,10 @@ from prowler.providers.m365.m365_provider import M365Provider class Entra(M365Service): def __init__(self, provider: M365Provider): super().__init__(provider) + if self.powershell: + self.powershell.connect_exchange_online() + self.user_accounts_status = self.powershell.get_user_account_status() self.powershell.close() loop = get_event_loop() @@ -36,6 +39,7 @@ class Entra(M365Service): self.groups = attributes[3] self.organizations = attributes[4] self.users = attributes[5] + self.user_accounts_status = {} async def _get_authorization_policy(self): logger.info("Entra - Getting authorization policy...") @@ -405,6 +409,9 @@ class Entra(M365Service): if registration_details.get(user.id, None) is not None else False ), + account_enabled=not self.user_accounts_status.get(user.id, {}).get( + "AccountDisabled", False + ), ) except Exception as error: logger.error( @@ -585,6 +592,7 @@ class User(BaseModel): on_premises_sync_enabled: bool directory_roles_ids: List[str] = [] is_mfa_capable: bool = False + account_enabled: bool = True class InvitationsFrom(Enum): diff --git a/prowler/providers/m365/services/entra/entra_users_mfa_capable/entra_users_mfa_capable.py b/prowler/providers/m365/services/entra/entra_users_mfa_capable/entra_users_mfa_capable.py index 8345c6963e..4b4075aa11 100644 --- a/prowler/providers/m365/services/entra/entra_users_mfa_capable/entra_users_mfa_capable.py +++ b/prowler/providers/m365/services/entra/entra_users_mfa_capable/entra_users_mfa_capable.py @@ -26,20 +26,21 @@ class entra_users_mfa_capable(Check): findings = [] for user in entra_client.users.values(): - report = CheckReportM365( - metadata=self.metadata(), - resource=user, - resource_name=user.name, - resource_id=user.id, - ) + if user.account_enabled: + report = CheckReportM365( + metadata=self.metadata(), + resource=user, + resource_name=user.name, + resource_id=user.id, + ) - if not user.is_mfa_capable: - report.status = "FAIL" - report.status_extended = f"User {user.name} is not MFA capable." - else: - report.status = "PASS" - report.status_extended = f"User {user.name} is MFA capable." + if not user.is_mfa_capable: + report.status = "FAIL" + report.status_extended = f"User {user.name} is not MFA capable." + else: + report.status = "PASS" + report.status_extended = f"User {user.name} is MFA capable." - findings.append(report) + findings.append(report) return findings diff --git a/pyproject.toml b/pyproject.toml index 60782e0d73..4308b55f91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,8 @@ dependencies = [ "slack-sdk==3.34.0", "tabulate==0.9.0", "tzlocal==5.3.1", - "py-iam-expand==0.1.0" + "py-iam-expand==0.1.0", + "h2 (==4.3.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." license = "Apache-2.0" diff --git a/tests/lib/outputs/finding_test.py b/tests/lib/outputs/finding_test.py index f9d4aaf7de..8a7b05ceae 100644 --- a/tests/lib/outputs/finding_test.py +++ b/tests/lib/outputs/finding_test.py @@ -586,7 +586,7 @@ class TestFinding: provider.type = "github" # GitHub App identity only has app_id, not account_name/account_id provider.identity = GithubAppIdentityInfo( - app_id=APP_ID, installations=["test-org"] + app_id=APP_ID, app_name="test-app", installations=["test-org"] ) provider.auth_method = "GitHub App Token" @@ -634,8 +634,8 @@ class TestFinding: # Assert account information for GitHub App - this is the core of the bug fix # Before the fix, this would fail because GithubAppIdentityInfo doesn't have account_name - # After the fix, it should use app_id with "app-" prefix - assert finding_output.account_name == f"app-{APP_ID}" + # After the fix, it should use app_name + assert finding_output.account_name == "test-app" assert finding_output.account_uid == APP_ID assert finding_output.account_email is None assert finding_output.account_organization_uid is None @@ -661,7 +661,7 @@ class TestFinding: check_output.file_path = "/path/to/iac/file.tf" check_output.resource_name = "aws_s3_bucket.example" check_output.resource_path = "/path/to/iac/file.tf" - check_output.file_line_range = [1, 5] + check_output.resource_line_range = "1:5" check_output.resource = { "resource": "aws_s3_bucket.example", "value": {}, @@ -685,7 +685,7 @@ class TestFinding: assert finding_output.auth_method == "No auth" assert finding_output.resource_name == "aws_s3_bucket.example" assert finding_output.resource_uid == "aws_s3_bucket.example" - assert finding_output.region == "/path/to/iac/file.tf" + assert finding_output.region == "1:5" assert finding_output.status == Status.PASS assert finding_output.status_extended == "mock_status_extended" assert finding_output.muted is False diff --git a/tests/lib/outputs/html/html_test.py b/tests/lib/outputs/html/html_test.py index e85567b20a..6fe352143a 100644 --- a/tests/lib/outputs/html/html_test.py +++ b/tests/lib/outputs/html/html_test.py @@ -4,6 +4,7 @@ from io import StringIO from mock import patch from prowler.config.config import prowler_version, timestamp +from prowler.lib.logger import logger from prowler.lib.outputs.html.html import HTML from prowler.providers.github.models import GithubAppIdentityInfo from tests.lib.outputs.fixtures.fixtures import generate_finding_output @@ -231,11 +232,12 @@ github_personal_access_token_html_assessment_summary = """
      GitHub Assessment Summary
      -
        +
          +
        • GitHub account: account-name
        • +
        @@ -244,8 +246,8 @@ github_personal_access_token_html_assessment_summary = """
        GitHub Credentials
        -
          +
            +
          • GitHub authentication method: Personal Access Token
          • @@ -259,10 +261,12 @@ github_app_html_assessment_summary = """
            GitHub Assessment Summary
            -
              +
              • - GitHub account: app-app-id + GitHub App Name: test-app +
              • +
              • + Installations: test-org
              @@ -272,11 +276,13 @@ github_app_html_assessment_summary = """
              GitHub Credentials
              -
                +
                • GitHub authentication method: GitHub App Token
                • +
                • + GitHub App ID: app-id +
                """ @@ -664,7 +670,12 @@ class TestHTML: summary = output.get_assessment_summary(provider) - assert summary == github_personal_access_token_html_assessment_summary + # Check for expected content in the summary + assert "GitHub Assessment Summary" in summary + assert "GitHub Credentials" in summary + assert "GitHub account: account-name" in summary + assert "GitHub authentication method: Personal Access Token" in summary + # Note: account_email is None in the default fixture, so it shouldn't appear def test_github_app_get_assessment_summary(self): """Test GitHub HTML assessment summary generation with GitHub App authentication.""" @@ -673,9 +684,18 @@ class TestHTML: provider = set_mocked_github_provider( auth_method="GitHub App Token", - identity=GithubAppIdentityInfo(app_id=APP_ID, installations=["test-org"]), + identity=GithubAppIdentityInfo( + app_id=APP_ID, app_name="test-app", installations=["test-org"] + ), ) summary = output.get_assessment_summary(provider) + logger.error(summary) - assert summary == github_app_html_assessment_summary + # Check for expected content in the summary + assert "GitHub Assessment Summary" in summary + assert "GitHub Credentials" in summary + assert "GitHub App Name: test-app" in summary + assert "Installations: test-org" in summary + assert "GitHub authentication method: GitHub App Token" in summary + assert f"GitHub App ID: {APP_ID}" in summary diff --git a/tests/providers/aws/lib/security_hub/security_hub_test.py b/tests/providers/aws/lib/security_hub/security_hub_test.py index 3cb8119316..17dec4505c 100644 --- a/tests/providers/aws/lib/security_hub/security_hub_test.py +++ b/tests/providers/aws/lib/security_hub/security_hub_test.py @@ -412,14 +412,12 @@ class TestSecurityHub: @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) def test_security_hub_test_connection_success(self): - session_mock = session.Session(region_name=AWS_REGION_EU_WEST_1) # Test successful connection connection = SecurityHub.test_connection( - session=session_mock, - regions={AWS_REGION_EU_WEST_1}, aws_account_id=AWS_ACCOUNT_NUMBER, aws_partition=AWS_COMMERCIAL_PARTITION, + regions={AWS_REGION_EU_WEST_1}, raise_on_exception=False, ) @@ -444,14 +442,11 @@ class TestSecurityHub: error_response, operation_name ) - session_mock = session.Session(region_name=AWS_REGION_EU_WEST_1) - # Test connection failure due to invalid access connection = SecurityHub.test_connection( - session=session_mock, - regions={AWS_REGION_EU_WEST_1}, aws_account_id=AWS_ACCOUNT_NUMBER, aws_partition=AWS_COMMERCIAL_PARTITION, + regions={AWS_REGION_EU_WEST_1}, raise_on_exception=False, ) @@ -468,14 +463,11 @@ class TestSecurityHub: "ProductSubscriptions": [] } - session_mock = session.Session(region_name=AWS_REGION_EU_WEST_1) - # Test connection failure due to missing Prowler subscription connection = SecurityHub.test_connection( - session=session_mock, - regions={AWS_REGION_EU_WEST_1}, aws_account_id=AWS_ACCOUNT_NUMBER, aws_partition=AWS_COMMERCIAL_PARTITION, + regions={AWS_REGION_EU_WEST_1}, raise_on_exception=False, ) @@ -489,14 +481,11 @@ class TestSecurityHub: # Mock unexpected exception mock_security_hub_client.side_effect = Exception("Unexpected error") - session_mock = session.Session(region_name=AWS_REGION_EU_WEST_1) - # Test connection failure due to an unexpected exception connection = SecurityHub.test_connection( - session=session_mock, - regions={AWS_REGION_EU_WEST_1}, aws_account_id=AWS_ACCOUNT_NUMBER, aws_partition=AWS_COMMERCIAL_PARTITION, + regions={AWS_REGION_EU_WEST_1}, raise_on_exception=False, ) @@ -510,11 +499,8 @@ class TestSecurityHub: # Mock unexpected exception mock_security_hub_client.side_effect = Exception("Unexpected error") - session_mock = session.Session(region_name=AWS_REGION_EU_WEST_1) - # Test connection failure due to an unexpected exception connection = SecurityHub.test_connection( - session=session_mock, aws_account_id=AWS_ACCOUNT_NUMBER, aws_partition=AWS_COMMERCIAL_PARTITION, raise_on_exception=False, @@ -566,3 +552,734 @@ class TestSecurityHub: str(e.value) == "If no role ARN is provided, a profile, an AWS access key ID, or an AWS secret access key is required." ) + + # Tests for new test_connection functionality - AWS Credential Management + @patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session") + @patch( + "prowler.providers.aws.aws_provider.AwsProvider.get_available_aws_service_regions" + ) + @patch( + "prowler.providers.aws.lib.security_hub.security_hub.SecurityHub.verify_enabled_per_region" + ) + def test_security_hub_test_connection_with_profile( + self, mock_verify_enabled, mock_get_regions, mock_setup_session + ): + # Mock session setup + mock_session = session.Session(region_name=AWS_REGION_EU_WEST_1) + mock_setup_session.return_value = mock_session + + # Mock available regions + mock_get_regions.return_value = [AWS_REGION_EU_WEST_1] + + # Mock enabled regions + mock_verify_enabled.return_value = {AWS_REGION_EU_WEST_1: mock_session} + + # Test connection with profile + connection = SecurityHub.test_connection( + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + profile="test-profile", + raise_on_exception=False, + ) + + assert connection.is_connected is True + assert connection.error is None + mock_setup_session.assert_called_once_with( + mfa=False, + profile="test-profile", + aws_access_key_id=None, + aws_secret_access_key=None, + aws_session_token=None, + ) + + @patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session") + @patch( + "prowler.providers.aws.aws_provider.AwsProvider.get_available_aws_service_regions" + ) + @patch( + "prowler.providers.aws.lib.security_hub.security_hub.SecurityHub.verify_enabled_per_region" + ) + def test_security_hub_test_connection_with_access_keys( + self, mock_verify_enabled, mock_get_regions, mock_setup_session + ): + # Mock session setup + mock_session = session.Session(region_name=AWS_REGION_EU_WEST_1) + mock_setup_session.return_value = mock_session + + # Mock available regions + mock_get_regions.return_value = [AWS_REGION_EU_WEST_1] + + # Mock enabled regions + mock_verify_enabled.return_value = {AWS_REGION_EU_WEST_1: mock_session} + + # Test connection with access keys + connection = SecurityHub.test_connection( + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + aws_access_key_id="test-key", + aws_secret_access_key="test-secret", + raise_on_exception=False, + ) + + assert connection.is_connected is True + assert connection.error is None + mock_setup_session.assert_called_once_with( + mfa=False, + profile=None, + aws_access_key_id="test-key", + aws_secret_access_key="test-secret", + aws_session_token=None, + ) + + @patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session") + @patch( + "prowler.providers.aws.aws_provider.AwsProvider.get_available_aws_service_regions" + ) + @patch( + "prowler.providers.aws.lib.security_hub.security_hub.SecurityHub.verify_enabled_per_region" + ) + def test_security_hub_test_connection_with_session_token( + self, mock_verify_enabled, mock_get_regions, mock_setup_session + ): + # Mock session setup + mock_session = session.Session(region_name=AWS_REGION_EU_WEST_1) + mock_setup_session.return_value = mock_session + + # Mock available regions + mock_get_regions.return_value = [AWS_REGION_EU_WEST_1] + + # Mock enabled regions + mock_verify_enabled.return_value = {AWS_REGION_EU_WEST_1: mock_session} + + # Test connection with session token + connection = SecurityHub.test_connection( + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + aws_session_token="test-token", + raise_on_exception=False, + ) + + assert connection.is_connected is True + assert connection.error is None + mock_setup_session.assert_called_once_with( + mfa=False, + profile=None, + aws_access_key_id=None, + aws_secret_access_key=None, + aws_session_token="test-token", + ) + + @patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session") + @patch( + "prowler.providers.aws.aws_provider.AwsProvider.get_available_aws_service_regions" + ) + @patch( + "prowler.providers.aws.lib.security_hub.security_hub.SecurityHub.verify_enabled_per_region" + ) + def test_security_hub_test_connection_with_mfa( + self, mock_verify_enabled, mock_get_regions, mock_setup_session + ): + # Mock session setup + mock_session = session.Session(region_name=AWS_REGION_EU_WEST_1) + mock_setup_session.return_value = mock_session + + # Mock available regions + mock_get_regions.return_value = [AWS_REGION_EU_WEST_1] + + # Mock enabled regions + mock_verify_enabled.return_value = {AWS_REGION_EU_WEST_1: mock_session} + + # Test connection with MFA enabled + connection = SecurityHub.test_connection( + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + mfa_enabled=True, + raise_on_exception=False, + ) + + assert connection.is_connected is True + assert connection.error is None + mock_setup_session.assert_called_once_with( + mfa=True, + profile=None, + aws_access_key_id=None, + aws_secret_access_key=None, + aws_session_token=None, + ) + + # Tests for Role Assumption functionality + @patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session") + @patch("prowler.providers.aws.aws_provider.AwsProvider.assume_role") + @patch( + "prowler.providers.aws.aws_provider.AwsProvider.get_available_aws_service_regions" + ) + @patch( + "prowler.providers.aws.lib.security_hub.security_hub.SecurityHub.verify_enabled_per_region" + ) + def test_security_hub_test_connection_with_role_arn( + self, + mock_verify_enabled, + mock_get_regions, + mock_assume_role, + mock_setup_session, + ): + # Mock initial session setup + mock_session = session.Session(region_name=AWS_REGION_EU_WEST_1) + mock_setup_session.return_value = mock_session + + # Mock assumed role credentials + from datetime import datetime, timezone + + from prowler.providers.aws.models import AWSCredentials + + mock_credentials = AWSCredentials( + aws_access_key_id="assumed-key", + aws_secret_access_key="assumed-secret", + aws_session_token="assumed-token", + expiration=datetime.now(timezone.utc), + ) + mock_assume_role.return_value = mock_credentials + + # Mock available regions + mock_get_regions.return_value = [AWS_REGION_EU_WEST_1] + + # Mock enabled regions + mock_verify_enabled.return_value = {AWS_REGION_EU_WEST_1: mock_session} + + # Test connection with role ARN + connection = SecurityHub.test_connection( + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + role_arn="arn:aws:iam::123456789012:role/test-role", + external_id="test-external-id", + session_duration=7200, + role_session_name="test-session", + raise_on_exception=False, + ) + + assert connection.is_connected is True + assert connection.error is None + mock_assume_role.assert_called_once() + + @patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session") + @patch("prowler.providers.aws.aws_provider.AwsProvider.assume_role") + @patch( + "prowler.providers.aws.aws_provider.AwsProvider.get_available_aws_service_regions" + ) + @patch( + "prowler.providers.aws.lib.security_hub.security_hub.SecurityHub.verify_enabled_per_region" + ) + def test_security_hub_test_connection_with_role_arn_default_values( + self, + mock_verify_enabled, + mock_get_regions, + mock_assume_role, + mock_setup_session, + ): + # Mock initial session setup + mock_session = session.Session(region_name=AWS_REGION_EU_WEST_1) + mock_setup_session.return_value = mock_session + + # Mock assumed role credentials + from datetime import datetime, timezone + + from prowler.providers.aws.models import AWSCredentials + + mock_credentials = AWSCredentials( + aws_access_key_id="assumed-key", + aws_secret_access_key="assumed-secret", + aws_session_token="assumed-token", + expiration=datetime.now(timezone.utc), + ) + mock_assume_role.return_value = mock_credentials + + # Mock available regions + mock_get_regions.return_value = [AWS_REGION_EU_WEST_1] + + # Mock enabled regions + mock_verify_enabled.return_value = {AWS_REGION_EU_WEST_1: mock_session} + + # Test connection with role ARN using default values + connection = SecurityHub.test_connection( + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + role_arn="arn:aws:iam::123456789012:role/test-role", + external_id="test-external-id", + raise_on_exception=False, + ) + + assert connection.is_connected is True + assert connection.error is None + mock_assume_role.assert_called_once() + + # Tests for Error Handling - Session Setup Errors + @patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session") + def test_security_hub_test_connection_setup_session_error(self, mock_setup_session): + from prowler.providers.aws.exceptions.exceptions import AWSSetUpSessionError + + # Mock session setup error + mock_setup_session.side_effect = AWSSetUpSessionError( + file="test_file.py", original_exception=Exception("Session setup failed") + ) + + # Test connection failure due to session setup error + connection = SecurityHub.test_connection( + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + raise_on_exception=False, + ) + + assert connection.is_connected is False + assert isinstance(connection.error, AWSSetUpSessionError) + + @patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session") + def test_security_hub_test_connection_setup_session_error_raise( + self, mock_setup_session + ): + from prowler.providers.aws.exceptions.exceptions import AWSSetUpSessionError + + # Mock session setup error + mock_setup_session.side_effect = AWSSetUpSessionError( + file="test_file.py", original_exception=Exception("Session setup failed") + ) + + # Test that error is raised when raise_on_exception=True + with pytest.raises(AWSSetUpSessionError): + SecurityHub.test_connection( + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + raise_on_exception=True, + ) + + # Tests for Error Handling - Argument Validation Errors + @patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session") + def test_security_hub_test_connection_argument_validation_error( + self, mock_setup_session + ): + from prowler.providers.aws.exceptions.exceptions import ( + AWSArgumentTypeValidationError, + ) + + # Mock session setup error + mock_setup_session.side_effect = AWSArgumentTypeValidationError( + file="test_file.py", original_exception=ValueError("Invalid argument") + ) + + # Test connection failure due to argument validation error + connection = SecurityHub.test_connection( + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + raise_on_exception=False, + ) + + assert connection.is_connected is False + assert isinstance(connection.error, AWSArgumentTypeValidationError) + + # Tests for Error Handling - Role ARN Validation Errors + @patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session") + def test_security_hub_test_connection_role_arn_region_not_empty_error( + self, mock_setup_session + ): + from prowler.providers.aws.exceptions.exceptions import ( + AWSIAMRoleARNRegionNotEmtpyError, + ) + + # Mock session setup error + mock_setup_session.side_effect = AWSIAMRoleARNRegionNotEmtpyError( + file="test_file.py" + ) + + # Test connection failure due to role ARN region validation error + connection = SecurityHub.test_connection( + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + raise_on_exception=False, + ) + + assert connection.is_connected is False + assert isinstance(connection.error, AWSIAMRoleARNRegionNotEmtpyError) + + @patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session") + def test_security_hub_test_connection_role_arn_partition_empty_error( + self, mock_setup_session + ): + from prowler.providers.aws.exceptions.exceptions import ( + AWSIAMRoleARNPartitionEmptyError, + ) + + # Mock session setup error + mock_setup_session.side_effect = AWSIAMRoleARNPartitionEmptyError( + file="test_file.py" + ) + + # Test connection failure due to role ARN partition validation error + connection = SecurityHub.test_connection( + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + raise_on_exception=False, + ) + + assert connection.is_connected is False + assert isinstance(connection.error, AWSIAMRoleARNPartitionEmptyError) + + @patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session") + def test_security_hub_test_connection_role_arn_service_not_iam_sts_error( + self, mock_setup_session + ): + from prowler.providers.aws.exceptions.exceptions import ( + AWSIAMRoleARNServiceNotIAMnorSTSError, + ) + + # Mock session setup error + mock_setup_session.side_effect = AWSIAMRoleARNServiceNotIAMnorSTSError( + file="test_file.py" + ) + + # Test connection failure due to role ARN service validation error + connection = SecurityHub.test_connection( + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + raise_on_exception=False, + ) + + assert connection.is_connected is False + assert isinstance(connection.error, AWSIAMRoleARNServiceNotIAMnorSTSError) + + @patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session") + def test_security_hub_test_connection_role_arn_invalid_account_id_error( + self, mock_setup_session + ): + from prowler.providers.aws.exceptions.exceptions import ( + AWSIAMRoleARNInvalidAccountIDError, + ) + + # Mock session setup error + mock_setup_session.side_effect = AWSIAMRoleARNInvalidAccountIDError( + file="test_file.py" + ) + + # Test connection failure due to role ARN account ID validation error + connection = SecurityHub.test_connection( + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + raise_on_exception=False, + ) + + assert connection.is_connected is False + assert isinstance(connection.error, AWSIAMRoleARNInvalidAccountIDError) + + @patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session") + def test_security_hub_test_connection_role_arn_invalid_resource_type_error( + self, mock_setup_session + ): + from prowler.providers.aws.exceptions.exceptions import ( + AWSIAMRoleARNInvalidResourceTypeError, + ) + + # Mock session setup error + mock_setup_session.side_effect = AWSIAMRoleARNInvalidResourceTypeError( + file="test_file.py" + ) + + # Test connection failure due to role ARN resource type validation error + connection = SecurityHub.test_connection( + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + raise_on_exception=False, + ) + + assert connection.is_connected is False + assert isinstance(connection.error, AWSIAMRoleARNInvalidResourceTypeError) + + @patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session") + def test_security_hub_test_connection_role_arn_empty_resource_error( + self, mock_setup_session + ): + from prowler.providers.aws.exceptions.exceptions import ( + AWSIAMRoleARNEmptyResourceError, + ) + + # Mock session setup error + mock_setup_session.side_effect = AWSIAMRoleARNEmptyResourceError( + file="test_file.py" + ) + + # Test connection failure due to role ARN empty resource validation error + connection = SecurityHub.test_connection( + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + raise_on_exception=False, + ) + + assert connection.is_connected is False + assert isinstance(connection.error, AWSIAMRoleARNEmptyResourceError) + + # Tests for Error Handling - Role Assumption Errors + @patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session") + def test_security_hub_test_connection_assume_role_error(self, mock_setup_session): + from prowler.providers.aws.exceptions.exceptions import AWSAssumeRoleError + + # Mock session setup error + mock_setup_session.side_effect = AWSAssumeRoleError( + file="test_file.py", original_exception=Exception("Role assumption failed") + ) + + # Test connection failure due to role assumption error + connection = SecurityHub.test_connection( + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + raise_on_exception=False, + ) + + assert connection.is_connected is False + assert isinstance(connection.error, AWSAssumeRoleError) + + # Tests for Error Handling - Profile and Credential Errors + @patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session") + def test_security_hub_test_connection_profile_not_found_error( + self, mock_setup_session + ): + from botocore.exceptions import ProfileNotFound + + # Mock session setup error + mock_setup_session.side_effect = ProfileNotFound(profile="test-profile") + + # Test connection failure due to profile not found error + connection = SecurityHub.test_connection( + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + profile="test-profile", + raise_on_exception=False, + ) + + assert connection.is_connected is False + assert isinstance(connection.error, ProfileNotFound) + + @patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session") + def test_security_hub_test_connection_profile_not_found_error_raise( + self, mock_setup_session + ): + from botocore.exceptions import ProfileNotFound + + from prowler.providers.aws.exceptions.exceptions import AWSProfileNotFoundError + + # Mock session setup error + mock_setup_session.side_effect = ProfileNotFound(profile="test-profile") + + # Test that error is raised when raise_on_exception=True + with pytest.raises(AWSProfileNotFoundError): + SecurityHub.test_connection( + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + profile="test-profile", + raise_on_exception=True, + ) + + @patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session") + def test_security_hub_test_connection_no_credentials_error( + self, mock_setup_session + ): + from botocore.exceptions import NoCredentialsError + + # Mock session setup error + mock_setup_session.side_effect = NoCredentialsError() + + # Test connection failure due to no credentials error + connection = SecurityHub.test_connection( + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + raise_on_exception=False, + ) + + assert connection.is_connected is False + assert isinstance(connection.error, NoCredentialsError) + + @patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session") + def test_security_hub_test_connection_no_credentials_error_raise( + self, mock_setup_session + ): + from botocore.exceptions import NoCredentialsError + + from prowler.providers.aws.exceptions.exceptions import AWSNoCredentialsError + + # Mock session setup error + mock_setup_session.side_effect = NoCredentialsError() + + # Test that error is raised when raise_on_exception=True + with pytest.raises(AWSNoCredentialsError): + SecurityHub.test_connection( + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + raise_on_exception=True, + ) + + # Tests for Error Handling - Access Key and Secret Key Errors + @patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session") + def test_security_hub_test_connection_access_key_id_invalid_error( + self, mock_setup_session + ): + from prowler.providers.aws.exceptions.exceptions import ( + AWSAccessKeyIDInvalidError, + ) + + # Mock session setup error + mock_setup_session.side_effect = AWSAccessKeyIDInvalidError( + file="test_file.py", original_exception=ValueError("Invalid access key ID") + ) + + # Test connection failure due to invalid access key ID error + connection = SecurityHub.test_connection( + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + raise_on_exception=False, + ) + + assert connection.is_connected is False + assert isinstance(connection.error, AWSAccessKeyIDInvalidError) + + @patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session") + def test_security_hub_test_connection_secret_access_key_invalid_error( + self, mock_setup_session + ): + from prowler.providers.aws.exceptions.exceptions import ( + AWSSecretAccessKeyInvalidError, + ) + + # Mock session setup error + mock_setup_session.side_effect = AWSSecretAccessKeyInvalidError( + file="test_file.py", + original_exception=ValueError("Invalid secret access key"), + ) + + # Test connection failure due to invalid secret access key error + connection = SecurityHub.test_connection( + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + raise_on_exception=False, + ) + + assert connection.is_connected is False + assert isinstance(connection.error, AWSSecretAccessKeyInvalidError) + + @patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session") + def test_security_hub_test_connection_session_token_expired_error( + self, mock_setup_session + ): + from prowler.providers.aws.exceptions.exceptions import ( + AWSSessionTokenExpiredError, + ) + + # Mock session setup error + mock_setup_session.side_effect = AWSSessionTokenExpiredError( + file="test_file.py", original_exception=ValueError("Session token expired") + ) + + # Test connection failure due to session token expired error + connection = SecurityHub.test_connection( + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + raise_on_exception=False, + ) + + assert connection.is_connected is False + assert isinstance(connection.error, AWSSessionTokenExpiredError) + + # Tests for Error Handling - Generic Exception + @patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session") + def test_security_hub_test_connection_generic_exception(self, mock_setup_session): + # Mock session setup error + mock_setup_session.side_effect = Exception("Generic error") + + # Test connection failure due to generic exception + connection = SecurityHub.test_connection( + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + raise_on_exception=False, + ) + + assert connection.is_connected is False + assert isinstance(connection.error, Exception) + assert str(connection.error) == "Generic error" + + @patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session") + def test_security_hub_test_connection_generic_exception_raise( + self, mock_setup_session + ): + # Mock session setup error + mock_setup_session.side_effect = Exception("Generic error") + + # Test that error is raised when raise_on_exception=True + with pytest.raises(Exception) as exc_info: + SecurityHub.test_connection( + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + raise_on_exception=True, + ) + + assert str(exc_info.value) == "Generic error" + + # Tests for Edge Cases and Parameter Validation + @patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session") + @patch( + "prowler.providers.aws.aws_provider.AwsProvider.get_available_aws_service_regions" + ) + @patch( + "prowler.providers.aws.lib.security_hub.security_hub.SecurityHub.verify_enabled_per_region" + ) + def test_security_hub_test_connection_with_aws_region( + self, mock_verify_enabled, mock_get_regions, mock_setup_session + ): + # Mock session setup + mock_session = session.Session(region_name=AWS_REGION_EU_WEST_1) + mock_setup_session.return_value = mock_session + + # Mock available regions + mock_get_regions.return_value = [AWS_REGION_EU_WEST_1, AWS_REGION_EU_WEST_2] + + # Mock enabled regions + mock_verify_enabled.return_value = {AWS_REGION_EU_WEST_1: mock_session} + + # Test connection with specific AWS region + connection = SecurityHub.test_connection( + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + aws_region=AWS_REGION_EU_WEST_1, + raise_on_exception=False, + ) + + assert connection.is_connected is True + assert connection.error is None + assert AWS_REGION_EU_WEST_1 in connection.enabled_regions + assert AWS_REGION_EU_WEST_2 in connection.disabled_regions + + @patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session") + @patch( + "prowler.providers.aws.aws_provider.AwsProvider.get_available_aws_service_regions" + ) + @patch( + "prowler.providers.aws.lib.security_hub.security_hub.SecurityHub.verify_enabled_per_region" + ) + def test_security_hub_test_connection_no_regions_specified( + self, mock_verify_enabled, mock_get_regions, mock_setup_session + ): + # Mock session setup + mock_session = session.Session(region_name=AWS_REGION_EU_WEST_1) + mock_setup_session.return_value = mock_session + + # Mock available regions + mock_get_regions.return_value = [AWS_REGION_EU_WEST_1, AWS_REGION_EU_WEST_2] + + # Mock enabled regions + mock_verify_enabled.return_value = {AWS_REGION_EU_WEST_1: mock_session} + + # Test connection without specifying regions + connection = SecurityHub.test_connection( + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + raise_on_exception=False, + ) + + assert connection.is_connected is True + assert connection.error is None + assert len(connection.enabled_regions) == 1 + assert len(connection.disabled_regions) == 1 diff --git a/tests/providers/aws/services/s3/s3_bucket_shadow_resource_vulnerability/s3_bucket_shadow_resource_vulnerability_test.py b/tests/providers/aws/services/s3/s3_bucket_shadow_resource_vulnerability/s3_bucket_shadow_resource_vulnerability_test.py index 5013ff1cdd..9b80d6db35 100644 --- a/tests/providers/aws/services/s3/s3_bucket_shadow_resource_vulnerability/s3_bucket_shadow_resource_vulnerability_test.py +++ b/tests/providers/aws/services/s3/s3_bucket_shadow_resource_vulnerability/s3_bucket_shadow_resource_vulnerability_test.py @@ -13,13 +13,14 @@ from tests.providers.aws.utils import ( class Test_s3_bucket_shadow_resource_vulnerability: @mock_aws def test_no_buckets(self): - s3_client = mock.MagicMock - s3_client.buckets = {} aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) aws_provider.identity.identity_arn = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root" - s3_client = mock.MagicMock + + s3_client = mock.MagicMock() + s3_client.buckets = {} s3_client.provider = aws_provider s3_client._head_bucket = mock.MagicMock(return_value=False) + with ( mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -36,28 +37,31 @@ class Test_s3_bucket_shadow_resource_vulnerability: check = s3_bucket_shadow_resource_vulnerability() result = check.execute() + assert len(result) == 0 @mock_aws def test_bucket_owned_by_account(self): - s3_client = mock.MagicMock + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + aws_provider.identity.identity_arn = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root" + bucket_name = f"sagemaker-{AWS_REGION_US_EAST_1}-{AWS_ACCOUNT_NUMBER}" - s3_client.audited_account_id = AWS_ACCOUNT_NUMBER - s3_client.audited_identity_arn = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root" + + s3_client = mock.MagicMock() s3_client.audited_canonical_id = AWS_ACCOUNT_NUMBER + s3_client.audited_partition = "aws" s3_client.buckets = { bucket_name: Bucket( name=bucket_name, arn=f"arn:aws:s3:::{bucket_name}", region=AWS_REGION_US_EAST_1, owner_id=AWS_ACCOUNT_NUMBER, + tags=[{"Key": "Environment", "Value": "test"}], ) } - aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) - aws_provider.identity.identity_arn = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root" - s3_client = mock.MagicMock s3_client.provider = aws_provider s3_client._head_bucket = mock.MagicMock(return_value=False) + with ( mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -74,32 +78,41 @@ class Test_s3_bucket_shadow_resource_vulnerability: check = s3_bucket_shadow_resource_vulnerability() result = check.execute() + assert len(result) == 1 - assert result[0].status == "PASS" - assert ( - "is correctly owned by the audited account" in result[0].status_extended - ) + report = result[0] + + # Test all report attributes + assert report.status == "PASS" + assert report.region == AWS_REGION_US_EAST_1 + assert report.resource_id == bucket_name + assert report.resource_arn == f"arn:aws:s3:::{bucket_name}" + assert report.resource_tags == [{"Key": "Environment", "Value": "test"}] + assert "is correctly owned by the audited account" in report.status_extended + assert "SageMaker" in report.status_extended @mock_aws def test_bucket_not_predictable(self): - s3_client = mock.MagicMock + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + aws_provider.identity.identity_arn = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root" + bucket_name = "my-non-predictable-bucket" - s3_client.audited_account_id = AWS_ACCOUNT_NUMBER - s3_client.audited_identity_arn = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root" + + s3_client = mock.MagicMock() s3_client.audited_canonical_id = AWS_ACCOUNT_NUMBER + s3_client.audited_partition = "aws" s3_client.buckets = { bucket_name: Bucket( name=bucket_name, arn=f"arn:aws:s3:::{bucket_name}", region=AWS_REGION_US_EAST_1, owner_id=AWS_ACCOUNT_NUMBER, + tags=[{"Key": "Project", "Value": "test-project"}], ) } - aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) - aws_provider.identity.identity_arn = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root" - s3_client = mock.MagicMock s3_client.provider = aws_provider s3_client._head_bucket = mock.MagicMock(return_value=False) + with ( mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -116,18 +129,25 @@ class Test_s3_bucket_shadow_resource_vulnerability: check = s3_bucket_shadow_resource_vulnerability() result = check.execute() + assert len(result) == 1 - assert result[0].status == "PASS" - assert "is not a known shadow resource" in result[0].status_extended + report = result[0] + + # Test all report attributes + assert report.status == "PASS" + assert report.region == AWS_REGION_US_EAST_1 + assert report.resource_id == bucket_name + assert report.resource_arn == f"arn:aws:s3:::{bucket_name}" + assert report.resource_tags == [{"Key": "Project", "Value": "test-project"}] + assert "is not a known shadow resource" in report.status_extended @mock_aws def test_shadow_resource_in_other_account(self): # Mock S3 client with no buckets in current account s3_client = mock.MagicMock() s3_client.buckets = {} - s3_client.audited_account_id = AWS_ACCOUNT_NUMBER - s3_client.audited_identity_arn = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root" s3_client.audited_canonical_id = AWS_ACCOUNT_NUMBER + s3_client.audited_identity_arn = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root" s3_client.audited_partition = "aws" # Mock regional clients - this is what the check uses to determine regions to test @@ -183,27 +203,31 @@ class Test_s3_bucket_shadow_resource_vulnerability: finding.status == "FAIL" and "shadow resource" in finding.status_extended ): - if ( - "aws-glue-assets" in finding.status_extended - and "Glue" in finding.status_extended - ): - found_services.add("Glue") - assert "us-west-2" in finding.status_extended - elif ( - "sagemaker" in finding.status_extended - and "SageMaker" in finding.status_extended - ): - found_services.add("SageMaker") - assert "us-east-1" in finding.status_extended - elif ( - "aws-emr-studio" in finding.status_extended - and "EMR" in finding.status_extended - ): - found_services.add("EMR") - assert "eu-west-1" in finding.status_extended + # Test all report attributes for cross-account findings + assert finding.status == "FAIL" + assert finding.resource_id in shadow_resources + assert finding.resource_arn == f"arn:aws:s3:::{finding.resource_id}" + assert finding.resource_tags == [] - # Verify common attributes - assert "owned by another account" in finding.status_extended + # Determine service from resource_id and test exact status_extended + if "aws-glue-assets" in finding.resource_id: + service = "Glue" + expected_status = f"S3 bucket {finding.resource_id} for service {service} is a known shadow resource that exists and is owned by another account." + assert finding.status_extended == expected_status + found_services.add("Glue") + assert finding.region == "us-west-2" + elif "sagemaker" in finding.resource_id: + service = "SageMaker" + expected_status = f"S3 bucket {finding.resource_id} for service {service} is a known shadow resource that exists and is owned by another account." + assert finding.status_extended == expected_status + found_services.add("SageMaker") + assert finding.region == "us-east-1" + elif "aws-emr-studio" in finding.resource_id: + service = "EMR" + expected_status = f"S3 bucket {finding.resource_id} for service {service} is a known shadow resource that exists and is owned by another account." + assert finding.status_extended == expected_status + found_services.add("EMR") + assert finding.region == "eu-west-1" # Verify we found all expected services expected_services = {"Glue", "SageMaker", "EMR"} diff --git a/tests/providers/github/github_fixtures.py b/tests/providers/github/github_fixtures.py index a848c9ed17..37cbd744be 100644 --- a/tests/providers/github/github_fixtures.py +++ b/tests/providers/github/github_fixtures.py @@ -12,6 +12,7 @@ ACCOUNT_URL = "/user" PAT_TOKEN = "github-token" OAUTH_TOKEN = "oauth-token" APP_ID = "app-id" +APP_NAME = "app-name" APP_KEY = "app-key" diff --git a/tests/providers/github/github_provider_test.py b/tests/providers/github/github_provider_test.py index 199964a743..a87a35e038 100644 --- a/tests/providers/github/github_provider_test.py +++ b/tests/providers/github/github_provider_test.py @@ -27,6 +27,7 @@ from tests.providers.github.github_fixtures import ( ACCOUNT_URL, APP_ID, APP_KEY, + APP_NAME, OAUTH_TOKEN, PAT_TOKEN, ) @@ -135,6 +136,7 @@ class TestGitHubProvider: "prowler.providers.github.github_provider.GithubProvider.setup_identity", return_value=GithubAppIdentityInfo( app_id=APP_ID, + app_name=APP_NAME, installations=["test-org"], ), ), @@ -149,7 +151,7 @@ class TestGitHubProvider: assert provider._type == "github" assert provider.session == GithubSession(token="", id=APP_ID, key=APP_KEY) assert provider.identity == GithubAppIdentityInfo( - app_id=APP_ID, installations=["test-org"] + app_id=APP_ID, app_name=APP_NAME, installations=["test-org"] ) assert provider._audit_config == { "inactive_not_archived_days_threshold": 180, @@ -210,7 +212,7 @@ class TestGitHubProvider: patch( "prowler.providers.github.github_provider.GithubProvider.setup_identity", return_value=GithubAppIdentityInfo( - app_id=APP_ID, installations=["test-org"] + app_id=APP_ID, app_name=APP_NAME, installations=["test-org"] ), ), ): diff --git a/tests/providers/github/services/organization/organization_service_test.py b/tests/providers/github/services/organization/organization_service_test.py index a51373b8c7..45b85198de 100644 --- a/tests/providers/github/services/organization/organization_service_test.py +++ b/tests/providers/github/services/organization/organization_service_test.py @@ -63,7 +63,12 @@ class Test_Organization_Scoping: mock_client = MagicMock() mock_user = MagicMock() - mock_user.get_orgs.return_value = [self.mock_org1, self.mock_org2] + mock_orgs = MagicMock() + mock_orgs.totalCount = 2 + mock_orgs.__iter__ = MagicMock( + return_value=iter([self.mock_org1, self.mock_org2]) + ) + mock_user.get_orgs.return_value = mock_orgs mock_client.get_user.return_value = mock_user with patch( @@ -81,6 +86,95 @@ class Test_Organization_Scoping: assert orgs[1].name == "test-org1" assert orgs[2].name == "test-org2" + def test_no_organizations_found_for_user(self): + """Test that when user has no organizations, empty dict is returned""" + provider = set_mocked_github_provider() + provider.repositories = [] + provider.organizations = [] + + mock_client = MagicMock() + mock_user = MagicMock() + mock_orgs = MagicMock() + mock_orgs.totalCount = 0 + mock_user.get_orgs.return_value = mock_orgs + mock_client.get_user.return_value = mock_user + + with patch( + "prowler.providers.github.services.organization.organization_service.GithubService.__init__" + ): + organization_service = Organization(provider) + organization_service.clients = [mock_client] + organization_service.provider = provider + + orgs = organization_service._list_organizations() + + assert len(orgs) == 0 + + def test_github_app_organization_scoping(self): + """Test GitHub App organization listing""" + from prowler.providers.github.models import GithubAppIdentityInfo + + provider = set_mocked_github_provider() + provider.repositories = [] + provider.organizations = [] + # Set GitHub App identity + provider.identity = GithubAppIdentityInfo( + app_id="test-app-id", + app_name="test-app", + installations=["test-org1", "test-org2"], + ) + + mock_client = MagicMock() + mock_orgs = MagicMock() + mock_orgs.totalCount = 2 + mock_orgs.__iter__ = MagicMock( + return_value=iter([self.mock_org1, self.mock_org2]) + ) + mock_client.get_organizations.return_value = mock_orgs + + with patch( + "prowler.providers.github.services.organization.organization_service.GithubService.__init__" + ): + organization_service = Organization(provider) + organization_service.clients = [mock_client] + organization_service.provider = provider + + orgs = organization_service._list_organizations() + + assert len(orgs) == 2 + assert 1 in orgs + assert 2 in orgs + assert orgs[1].name == "test-org1" + assert orgs[2].name == "test-org2" + + def test_github_app_no_organizations_found(self): + """Test GitHub App when no organizations are accessible""" + from prowler.providers.github.models import GithubAppIdentityInfo + + provider = set_mocked_github_provider() + provider.repositories = [] + provider.organizations = [] + # Set GitHub App identity + provider.identity = GithubAppIdentityInfo( + app_id="test-app-id", app_name="test-app", installations=[] + ) + + mock_client = MagicMock() + mock_orgs = MagicMock() + mock_orgs.totalCount = 0 + mock_client.get_organizations.return_value = mock_orgs + + with patch( + "prowler.providers.github.services.organization.organization_service.GithubService.__init__" + ): + organization_service = Organization(provider) + organization_service.clients = [mock_client] + organization_service.provider = provider + + orgs = organization_service._list_organizations() + + assert len(orgs) == 0 + def test_specific_organization_scoping(self): """Test that only specified organizations are returned""" provider = set_mocked_github_provider() diff --git a/tests/providers/iac/iac_fixtures.py b/tests/providers/iac/iac_fixtures.py index 933aa4a2fd..9e4c1105f4 100644 --- a/tests/providers/iac/iac_fixtures.py +++ b/tests/providers/iac/iac_fixtures.py @@ -1,167 +1,290 @@ # IAC Provider Constants DEFAULT_SCAN_PATH = "." -# Sample Checkov Output -SAMPLE_CHECKOV_OUTPUT = [ - { - "check_type": "terraform", - "results": { - "failed_checks": [ +# Sample Trivy Output +SAMPLE_TRIVY_OUTPUT = { + "Results": [ + { + "Target": "main.tf", + "Type": "terraform", + "Misconfigurations": [ { - "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", + "ID": "AVD-AWS-0001", + "Title": "S3 bucket should have encryption enabled", + "Description": "S3 bucket should have encryption enabled", + "Message": "S3 bucket should have encryption enabled", + "Resolution": "Enable encryption on the S3 bucket", + "Severity": "LOW", + "PrimaryURL": "https://avd.aquasec.com/misconfig/aws/s3/avd-aws-0001", + "RuleID": "AVD-AWS-0001", }, { - "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", + "ID": "AVD-AWS-0002", + "Title": "S3 bucket should have public access blocked", + "Description": "S3 bucket should have public access blocked", + "Message": "S3 bucket should have public access blocked", + "Resolution": "Block public access on the S3 bucket", + "Severity": "LOW", + "PrimaryURL": "https://avd.aquasec.com/misconfig/aws/s3/avd-aws-0002", + "RuleID": "AVD-AWS-0002", }, ], - "passed_checks": [ + "Vulnerabilities": [], + "Secrets": [], + "Licenses": [], + }, + { + "Target": "main.tf", + "Type": "terraform", + "Misconfigurations": [ { - "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", + "ID": "AVD-AWS-0003", + "Title": "S3 bucket should have versioning enabled", + "Description": "S3 bucket should have versioning enabled", + "Message": "S3 bucket should have versioning enabled", + "Resolution": "Enable versioning on the S3 bucket", + "Severity": "LOW", + "PrimaryURL": "https://avd.aquasec.com/misconfig/aws/s3/avd-aws-0003", + "RuleID": "AVD-AWS-0003", } ], + "Vulnerabilities": [], + "Secrets": [], + "Licenses": [], }, - } -] + ] +} # Sample Finding Data -SAMPLE_FINDING = SAMPLE_CHECKOV_OUTPUT[0] +SAMPLE_FINDING = SAMPLE_TRIVY_OUTPUT["Results"][0] 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", + "ID": "AVD-AWS-0001", + "Title": "S3 bucket should have encryption enabled", + "Description": "S3 bucket should have encryption enabled", + "Message": "S3 bucket should have encryption enabled", + "Resolution": "Enable encryption on the S3 bucket", + "Severity": "low", + "PrimaryURL": "https://avd.aquasec.com/misconfig/aws/s3/avd-aws-0001", + "RuleID": "AVD-AWS-0001", } 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", + "ID": "AVD-AWS-0003", + "Title": "S3 bucket should have versioning enabled", + "Description": "S3 bucket should have versioning enabled", + "Message": "S3 bucket should have versioning enabled", + "Resolution": "Enable versioning on the S3 bucket", + "Severity": "low", + "PrimaryURL": "https://avd.aquasec.com/misconfig/aws/s3/avd-aws-0003", + "RuleID": "AVD-AWS-0003", } # Additional sample checks SAMPLE_ANOTHER_FAILED_CHECK = { - "check_id": "CKV_AWS_4", - "check_name": "Ensure S3 bucket has logging enabled", - "guideline": "https://docs.bridgecrew.io/docs/s3_4-s3-bucket-has-logging-enabled", - "severity": "medium", + "ID": "AVD-AWS-0004", + "Title": "S3 bucket should have logging enabled", + "Description": "S3 bucket should have logging enabled", + "Message": "S3 bucket should have logging enabled", + "Resolution": "Enable logging on the S3 bucket", + "Severity": "medium", + "PrimaryURL": "https://avd.aquasec.com/misconfig/aws/s3/avd-aws-0004", + "RuleID": "AVD-AWS-0004", } SAMPLE_ANOTHER_PASSED_CHECK = { - "check_id": "CKV_AWS_5", - "check_name": "Ensure S3 bucket has lifecycle policy", - "guideline": "https://docs.bridgecrew.io/docs/s3_5-s3-bucket-has-lifecycle-policy", - "severity": "low", + "ID": "AVD-AWS-0005", + "Title": "S3 bucket should have lifecycle policy", + "Description": "S3 bucket should have lifecycle policy", + "Message": "S3 bucket should have lifecycle policy", + "Resolution": "Configure lifecycle policy on the S3 bucket", + "Severity": "low", + "PrimaryURL": "https://avd.aquasec.com/misconfig/aws/s3/avd-aws-0005", + "RuleID": "AVD-AWS-0005", } SAMPLE_ANOTHER_SKIPPED_CHECK = { - "check_id": "CKV_AWS_6", - "check_name": "Ensure S3 bucket has object lock enabled", - "guideline": "https://docs.bridgecrew.io/docs/s3_6-s3-bucket-has-object-lock-enabled", - "severity": "high", - "suppress_comment": "Not applicable for this use case", + "ID": "AVD-AWS-0006", + "Title": "S3 bucket should have object lock enabled", + "Description": "S3 bucket should have object lock enabled", + "Message": "S3 bucket should have object lock enabled", + "Resolution": "Enable object lock on the S3 bucket", + "Severity": "high", + "PrimaryURL": "https://avd.aquasec.com/misconfig/aws/s3/avd-aws-0006", + "RuleID": "AVD-AWS-0006", + "Status": "MUTED", } SAMPLE_SKIPPED_CHECK = { - "check_id": "CKV_AWS_7", - "check_name": "Ensure S3 bucket has server-side encryption", - "guideline": "https://docs.bridgecrew.io/docs/s3_7-s3-bucket-has-server-side-encryption", - "severity": "medium", - "suppress_comment": "Legacy bucket, will be migrated", + "ID": "AVD-AWS-0007", + "Title": "S3 bucket should have server-side encryption", + "Description": "S3 bucket should have server-side encryption", + "Message": "S3 bucket should have server-side encryption", + "Resolution": "Enable server-side encryption on the S3 bucket", + "Severity": "medium", + "PrimaryURL": "https://avd.aquasec.com/misconfig/aws/s3/avd-aws-0007", + "RuleID": "AVD-AWS-0007", + "Status": "MUTED", } SAMPLE_HIGH_SEVERITY_CHECK = { - "check_id": "CKV_AWS_8", - "check_name": "Ensure S3 bucket has public access blocked", - "guideline": "https://docs.bridgecrew.io/docs/s3_8-s3-bucket-has-public-access-blocked", - "severity": "high", + "ID": "AVD-AWS-0008", + "Title": "S3 bucket should have public access blocked", + "Description": "S3 bucket should have public access blocked", + "Message": "S3 bucket should have public access blocked", + "Resolution": "Block public access on the S3 bucket", + "Severity": "high", + "PrimaryURL": "https://avd.aquasec.com/misconfig/aws/s3/avd-aws-0008", + "RuleID": "AVD-AWS-0008", } # Dockerfile samples SAMPLE_DOCKERFILE_REPORT = { - "check_type": "dockerfile", - "results": { - "failed_checks": [ - { - "check_id": "CKV_DOCKER_1", - "check_name": "Ensure base image is not using latest tag", - "guideline": "https://docs.bridgecrew.io/docs/docker_1-base-image-not-using-latest-tag", - "severity": "medium", - } - ], - "passed_checks": [], - }, + "Target": "Dockerfile", + "Type": "dockerfile", + "Misconfigurations": [ + { + "ID": "AVD-DOCKER-0001", + "Title": "Base image should not use latest tag", + "Description": "Base image should not use latest tag", + "Message": "Base image should not use latest tag", + "Resolution": "Use a specific version tag instead of latest", + "Severity": "medium", + "PrimaryURL": "https://avd.aquasec.com/misconfig/docker/dockerfile/avd-docker-0001", + "RuleID": "AVD-DOCKER-0001", + } + ], + "Vulnerabilities": [], + "Secrets": [], + "Licenses": [], } SAMPLE_DOCKERFILE_CHECK = { - "check_id": "CKV_DOCKER_1", - "check_name": "Ensure base image is not using latest tag", - "guideline": "https://docs.bridgecrew.io/docs/docker_1-base-image-not-using-latest-tag", - "severity": "medium", + "ID": "AVD-DOCKER-0001", + "Title": "Base image should not use latest tag", + "Description": "Base image should not use latest tag", + "Message": "Base image should not use latest tag", + "Resolution": "Use a specific version tag instead of latest", + "Severity": "medium", + "PrimaryURL": "https://avd.aquasec.com/misconfig/docker/dockerfile/avd-docker-0001", + "RuleID": "AVD-DOCKER-0001", } # YAML samples SAMPLE_YAML_REPORT = { - "check_type": "yaml", - "results": { - "failed_checks": [ - { - "check_id": "CKV_K8S_1", - "check_name": "Ensure API server is not exposed", - "guideline": "https://docs.bridgecrew.io/docs/k8s_1-api-server-not-exposed", - "severity": "high", - } - ], - "passed_checks": [], - }, + "Target": "deployment.yaml", + "Type": "kubernetes", + "Misconfigurations": [ + { + "ID": "AVD-K8S-0001", + "Title": "API server should not be exposed", + "Description": "API server should not be exposed", + "Message": "API server should not be exposed", + "Resolution": "Do not expose the API server", + "Severity": "high", + "PrimaryURL": "https://avd.aquasec.com/misconfig/kubernetes/avd-k8s-0001", + "RuleID": "AVD-K8S-0001", + } + ], + "Vulnerabilities": [], + "Secrets": [], + "Licenses": [], } SAMPLE_YAML_CHECK = { - "check_id": "CKV_K8S_1", - "check_name": "Ensure API server is not exposed", - "guideline": "https://docs.bridgecrew.io/docs/k8s_1-api-server-not-exposed", - "severity": "high", + "ID": "AVD-K8S-0001", + "Title": "API server should not be exposed", + "Description": "API server should not be exposed", + "Message": "API server should not be exposed", + "Resolution": "Do not expose the API server", + "Severity": "high", + "PrimaryURL": "https://avd.aquasec.com/misconfig/kubernetes/avd-k8s-0001", + "RuleID": "AVD-K8S-0001", } # CloudFormation samples SAMPLE_CLOUDFORMATION_CHECK = { - "check_id": "CKV_AWS_9", - "check_name": "Ensure CloudFormation stack has drift detection enabled", - "guideline": "https://docs.bridgecrew.io/docs/aws_9-cloudformation-stack-has-drift-detection-enabled", - "severity": "low", + "ID": "AVD-AWS-0009", + "Title": "CloudFormation stack should have drift detection enabled", + "Description": "CloudFormation stack should have drift detection enabled", + "Message": "CloudFormation stack should have drift detection enabled", + "Resolution": "Enable drift detection on the CloudFormation stack", + "Severity": "low", + "PrimaryURL": "https://avd.aquasec.com/misconfig/aws/cloudformation/avd-aws-0009", + "RuleID": "AVD-AWS-0009", } # Kubernetes samples SAMPLE_KUBERNETES_CHECK = { - "check_id": "CKV_K8S_2", - "check_name": "Ensure RBAC is enabled", - "guideline": "https://docs.bridgecrew.io/docs/k8s_2-rbac-enabled", - "severity": "medium", + "ID": "AVD-K8S-0002", + "Title": "RBAC should be enabled", + "Description": "RBAC should be enabled", + "Message": "RBAC should be enabled", + "Resolution": "Enable RBAC on the cluster", + "Severity": "medium", + "PrimaryURL": "https://avd.aquasec.com/misconfig/kubernetes/avd-k8s-0002", + "RuleID": "AVD-K8S-0002", +} + +# Sample Trivy output with vulnerabilities +SAMPLE_TRIVY_VULNERABILITY_OUTPUT = { + "Results": [ + { + "Target": "package.json", + "Type": "nodejs", + "Misconfigurations": [], + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2023-1234", + "Title": "Example vulnerability", + "Description": "This is an example vulnerability", + "Severity": "high", + "PrimaryURL": "https://example.com/cve-2023-1234", + } + ], + "Secrets": [], + "Licenses": [], + } + ] +} + +# Sample Trivy output with secrets +SAMPLE_TRIVY_SECRET_OUTPUT = { + "Results": [ + { + "Target": "config.yaml", + "Class": "secret", + "Misconfigurations": [], + "Vulnerabilities": [], + "Secrets": [ + { + "ID": "aws-access-key-id", + "Title": "AWS Access Key ID", + "Description": "AWS Access Key ID found in configuration", + "Severity": "critical", + "PrimaryURL": "https://example.com/secret-aws-access-key-id", + } + ], + "Licenses": [], + } + ] } -def get_sample_checkov_json_output(): - """Return sample Checkov JSON output as string""" +def get_sample_trivy_json_output(): + """Return sample Trivy JSON output as string""" import json - return json.dumps(SAMPLE_CHECKOV_OUTPUT) + return json.dumps(SAMPLE_TRIVY_OUTPUT) -def get_empty_checkov_output(): - """Return empty Checkov output as string""" - return "[]" +def get_empty_trivy_output(): + """Return empty Trivy output as string""" + import json + + return json.dumps({"Results": []}) -def get_invalid_checkov_output(): +def get_invalid_trivy_output(): """Return invalid JSON output as string""" return "invalid json output" diff --git a/tests/providers/iac/iac_provider_test.py b/tests/providers/iac/iac_provider_test.py index d6803289e2..7f89ff7a49 100644 --- a/tests/providers/iac/iac_provider_test.py +++ b/tests/providers/iac/iac_provider_test.py @@ -15,18 +15,15 @@ from tests.providers.iac.iac_fixtures import ( SAMPLE_ANOTHER_SKIPPED_CHECK, SAMPLE_CLOUDFORMATION_CHECK, SAMPLE_DOCKERFILE_CHECK, - SAMPLE_DOCKERFILE_REPORT, SAMPLE_FAILED_CHECK, - SAMPLE_FINDING, SAMPLE_HIGH_SEVERITY_CHECK, SAMPLE_KUBERNETES_CHECK, SAMPLE_PASSED_CHECK, SAMPLE_SKIPPED_CHECK, SAMPLE_YAML_CHECK, - SAMPLE_YAML_REPORT, - get_empty_checkov_output, - get_invalid_checkov_output, - get_sample_checkov_json_output, + get_empty_trivy_output, + get_invalid_trivy_output, + get_sample_trivy_json_output, ) @@ -51,73 +48,85 @@ class TestIacProvider: assert provider._type == "iac" assert provider.scan_path == custom_path - def test_iac_provider_process_check_failed(self): - """Test processing a failed check""" + def test_iac_provider_process_finding_failed(self): + """Test processing a failed finding""" provider = IacProvider() - report = provider._process_check(SAMPLE_FINDING, SAMPLE_FAILED_CHECK, "FAIL") + report = provider._process_finding(SAMPLE_FAILED_CHECK, "main.tf", "terraform") assert isinstance(report, CheckReportIAC) 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["ID"] + assert report.check_metadata.CheckTitle == SAMPLE_FAILED_CHECK["Title"] assert report.check_metadata.Severity == "low" - assert report.check_metadata.RelatedUrl == SAMPLE_FAILED_CHECK["guideline"] + assert report.check_metadata.RelatedUrl == SAMPLE_FAILED_CHECK["PrimaryURL"] - def test_iac_provider_process_check_passed(self): - """Test processing a passed check""" + def test_iac_provider_process_finding_passed(self): + """Test processing a passed finding""" provider = IacProvider() - report = provider._process_check(SAMPLE_FINDING, SAMPLE_PASSED_CHECK, "PASS") + report = provider._process_finding(SAMPLE_PASSED_CHECK, "main.tf", "terraform") assert isinstance(report, CheckReportIAC) - assert report.status == "PASS" + assert report.status == "FAIL" # Trivy findings are always FAIL by default 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["ID"] + assert report.check_metadata.CheckTitle == SAMPLE_PASSED_CHECK["Title"] assert report.check_metadata.Severity == "low" @patch("subprocess.run") def test_iac_provider_run_scan_success(self, mock_subprocess): - """Test successful IAC scan with Checkov""" + """Test successful IAC scan with Trivy""" provider = IacProvider() mock_subprocess.return_value = MagicMock( - stdout=get_sample_checkov_json_output(), stderr="" + stdout=get_sample_trivy_json_output(), stderr="" ) - reports = provider.run_scan("/test/directory", ["all"], []) + reports = provider.run_scan( + "/test/directory", ["vuln", "misconfig", "secret"], [] + ) - # Should have 2 failed checks + 1 passed check = 3 total reports + # Should have 3 misconfigurations from the sample output assert len(reports) == 3 - # Check that we have both failed and passed reports + # Check that we have failed reports (Trivy findings are always FAIL by default) 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 + assert len(failed_reports) == 3 # Verify subprocess was called correctly mock_subprocess.assert_called_once_with( - ["checkov", "-d", "/test/directory", "-o", "json", "-f", "all"], + [ + "trivy", + "fs", + "/test/directory", + "--format", + "json", + "--scanners", + "vuln,misconfig,secret", + "--parallel", + "0", + "--include-non-failures", + ], 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""" + """Test IAC scan with empty Trivy output""" provider = IacProvider() mock_subprocess.return_value = MagicMock( - stdout=get_empty_checkov_output(), stderr="" + stdout=get_empty_trivy_output(), stderr="" ) - reports = provider.run_scan("/test/directory", ["all"], []) + reports = provider.run_scan( + "/test/directory", ["vuln", "misconfig", "secret"], [] + ) assert len(reports) == 0 def test_provider_run_local_scan(self): @@ -127,7 +136,9 @@ class TestIacProvider: "prowler.providers.iac.iac_provider.IacProvider.run_scan", ) as mock_run_scan: provider.run() - mock_run_scan.assert_called_with(scan_path, ["all"], []) + mock_run_scan.assert_called_with( + scan_path, ["vuln", "misconfig", "secret"], [] + ) @mock.patch.dict(os.environ, {}, clear=True) def test_provider_run_remote_scan(self): @@ -145,7 +156,9 @@ class TestIacProvider: ): provider.run() mock_clone.assert_called_with(scan_repository_url, None, None, None) - mock_run_scan.assert_called_with(temp_dir, ["all"], []) + mock_run_scan.assert_called_with( + temp_dir, ["vuln", "misconfig", "secret"], [] + ) @mock.patch.dict(os.environ, {}, clear=True) def test_print_credentials_local(self): @@ -183,7 +196,7 @@ class TestIacProvider: provider = IacProvider() mock_subprocess.return_value = MagicMock( - stdout=get_invalid_checkov_output(), stderr="" + stdout=get_invalid_trivy_output(), stderr="" ) with pytest.raises(SystemExit) as excinfo: @@ -193,92 +206,102 @@ class TestIacProvider: @patch("subprocess.run") def test_iac_provider_run_scan_null_output(self, mock_subprocess): - """Test IAC scan with null Checkov output""" + """Test IAC scan with null Trivy output""" provider = IacProvider() mock_subprocess.return_value = MagicMock(stdout="null", stderr="") - reports = provider.run_scan("/test/directory", ["all"], []) - assert len(reports) == 0 + with pytest.raises(SystemExit) as exc_info: + provider.run_scan("/test/directory", ["vuln", "misconfig", "secret"], []) + assert exc_info.value.code == 1 - def test_iac_provider_process_check_dockerfile(self): - """Test processing a Dockerfile check""" + def test_iac_provider_process_finding_dockerfile(self): + """Test processing a Dockerfile finding""" provider = IacProvider() - report = provider._process_check( - SAMPLE_DOCKERFILE_REPORT, SAMPLE_DOCKERFILE_CHECK, "FAIL" + report = provider._process_finding( + SAMPLE_DOCKERFILE_CHECK, "Dockerfile", "dockerfile" ) assert isinstance(report, CheckReportIAC) assert report.status == "FAIL" assert report.check_metadata.ServiceName == "dockerfile" - assert report.check_metadata.CheckID == SAMPLE_DOCKERFILE_CHECK["check_id"] + assert report.check_metadata.CheckID == SAMPLE_DOCKERFILE_CHECK["ID"] - def test_iac_provider_process_check_yaml(self): - """Test processing a YAML check""" + def test_iac_provider_process_finding_yaml(self): + """Test processing a YAML finding""" provider = IacProvider() - report = provider._process_check(SAMPLE_YAML_REPORT, SAMPLE_YAML_CHECK, "PASS") + report = provider._process_finding( + SAMPLE_YAML_CHECK, "deployment.yaml", "kubernetes" + ) assert isinstance(report, CheckReportIAC) - assert report.status == "PASS" - assert report.check_metadata.ServiceName == "yaml" - assert report.check_metadata.CheckID == SAMPLE_YAML_CHECK["check_id"] + assert report.status == "FAIL" # Trivy findings are always FAIL by default + assert report.check_metadata.ServiceName == "kubernetes" + assert report.check_metadata.CheckID == SAMPLE_YAML_CHECK["ID"] @patch("subprocess.run") def test_run_scan_success_with_failed_and_passed_checks(self, mock_subprocess): """Test successful run_scan with both failed and passed checks""" provider = IacProvider() - # Create sample output with both failed and passed checks - sample_output = [ - { - "check_type": "terraform", - "results": { - "failed_checks": [SAMPLE_FAILED_CHECK], - "passed_checks": [SAMPLE_PASSED_CHECK], - "skipped_checks": [], - }, - } - ] + # Create sample Trivy output with both failed and passed checks + sample_output = { + "Results": [ + { + "Target": "main.tf", + "Type": "terraform", + "Misconfigurations": [SAMPLE_FAILED_CHECK, SAMPLE_PASSED_CHECK], + "Vulnerabilities": [], + "Secrets": [], + "Licenses": [], + } + ] + } mock_subprocess.return_value = MagicMock( stdout=json.dumps(sample_output), stderr="" ) - result = provider.run_scan("/test/directory", ["terraform"], []) + result = provider.run_scan( + "/test/directory", ["vuln", "misconfig", "secret"], [] + ) # 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 + # Check that we have FAIL reports (Trivy findings are always FAIL by default) statuses = [report.status for report in result] - assert "FAIL" in statuses - assert "PASS" in statuses + assert all(status == "FAIL" for status in statuses) @patch("subprocess.run") def test_run_scan_with_skipped_checks(self, mock_subprocess): """Test run_scan with skipped checks (muted)""" provider = IacProvider() - # Create sample output with skipped checks - sample_output = [ - { - "check_type": "terraform", - "results": { - "failed_checks": [], - "passed_checks": [], - "skipped_checks": [SAMPLE_SKIPPED_CHECK], - }, - } - ] + # Create sample Trivy output with skipped checks + sample_output = { + "Results": [ + { + "Target": "main.tf", + "Type": "terraform", + "Misconfigurations": [SAMPLE_SKIPPED_CHECK], + "Vulnerabilities": [], + "Secrets": [], + "Licenses": [], + } + ] + } mock_subprocess.return_value = MagicMock( stdout=json.dumps(sample_output), stderr="" ) - result = provider.run_scan("/test/directory", ["all"], ["exclude/path"]) + result = provider.run_scan( + "/test/directory", ["vuln", "misconfig", "secret"], ["exclude/path"] + ) # Verify results assert len(result) == 1 @@ -291,9 +314,13 @@ class TestIacProvider: """Test run_scan with no findings""" provider = IacProvider() - mock_subprocess.return_value = MagicMock(stdout="[]", stderr="") + mock_subprocess.return_value = MagicMock( + stdout=json.dumps({"Results": []}), stderr="" + ) - result = provider.run_scan("/test/directory", ["kubernetes"], []) + result = provider.run_scan( + "/test/directory", ["vuln", "misconfig", "secret"], [] + ) # Verify results assert len(result) == 0 @@ -303,40 +330,43 @@ class TestIacProvider: """Test run_scan with multiple reports from different frameworks""" provider = IacProvider() - # Create sample output with multiple frameworks - sample_output = [ - { - "check_type": "terraform", - "results": { - "failed_checks": [SAMPLE_FAILED_CHECK], - "passed_checks": [], - "skipped_checks": [], + # Create sample Trivy output with multiple frameworks + sample_output = { + "Results": [ + { + "Target": "main.tf", + "Type": "terraform", + "Misconfigurations": [SAMPLE_FAILED_CHECK], + "Vulnerabilities": [], + "Secrets": [], + "Licenses": [], }, - }, - { - "check_type": "kubernetes", - "results": { - "failed_checks": [], - "passed_checks": [SAMPLE_PASSED_CHECK], - "skipped_checks": [], + { + "Target": "deployment.yaml", + "Type": "kubernetes", + "Misconfigurations": [SAMPLE_PASSED_CHECK], + "Vulnerabilities": [], + "Secrets": [], + "Licenses": [], }, - }, - ] + ] + } mock_subprocess.return_value = MagicMock( stdout=json.dumps(sample_output), stderr="" ) - result = provider.run_scan("/test/directory", ["terraform", "kubernetes"], []) + result = provider.run_scan( + "/test/directory", ["vuln", "misconfig", "secret"], [] + ) # 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 + # Check that we have FAIL reports (Trivy findings are always FAIL by default) statuses = [report.status for report in result] - assert "FAIL" in statuses - assert "PASS" in statuses + assert all(status == "FAIL" for status in statuses) @patch("subprocess.run") def test_run_scan_exception_handling(self, mock_subprocess): @@ -347,44 +377,49 @@ class TestIacProvider: mock_subprocess.side_effect = Exception("Test exception") with pytest.raises(SystemExit) as exc_info: - provider.run_scan("/test/directory", ["terraform"], []) + provider.run_scan("/test/directory", ["vuln", "misconfig", "secret"], []) assert exc_info.value.code == 1 @patch("subprocess.run") def test_run_scan_with_different_frameworks(self, mock_subprocess): - """Test run_scan with different framework configurations""" + """Test run_scan with different scanner configurations""" provider = IacProvider() - sample_output = [ - { - "check_type": "terraform", - "results": { - "failed_checks": [], - "passed_checks": [SAMPLE_PASSED_CHECK], - "skipped_checks": [], - }, - } - ] + sample_output = { + "Results": [ + { + "Target": "main.tf", + "Type": "terraform", + "Misconfigurations": [SAMPLE_PASSED_CHECK], + "Vulnerabilities": [], + "Secrets": [], + "Licenses": [], + } + ] + } mock_subprocess.return_value = MagicMock( stdout=json.dumps(sample_output), stderr="" ) - # Test with specific frameworks - frameworks = ["terraform", "kubernetes", "cloudformation"] - result = provider.run_scan("/test/directory", frameworks, []) + # Test with specific scanners + scanners = ["vuln", "misconfig", "secret"] + result = provider.run_scan("/test/directory", scanners, []) - # Verify subprocess was called with correct frameworks + # Verify subprocess was called with correct scanners mock_subprocess.assert_called_once_with( [ - "checkov", - "-d", + "trivy", + "fs", "/test/directory", - "-o", + "--format", "json", - "-f", - ",".join(frameworks), + "--scanners", + ",".join(scanners), + "--parallel", + "0", + "--include-non-failures", ], capture_output=True, text=True, @@ -392,23 +427,25 @@ class TestIacProvider: # Verify results assert len(result) == 1 - assert result[0].status == "PASS" + assert result[0].status == "FAIL" # Trivy findings are always FAIL by default @patch("subprocess.run") def test_run_scan_with_exclude_paths(self, mock_subprocess): """Test run_scan with exclude paths""" provider = IacProvider() - sample_output = [ - { - "check_type": "terraform", - "results": { - "failed_checks": [], - "passed_checks": [SAMPLE_PASSED_CHECK], - "skipped_checks": [], - }, - } - ] + sample_output = { + "Results": [ + { + "Target": "main.tf", + "Type": "terraform", + "Misconfigurations": [SAMPLE_PASSED_CHECK], + "Vulnerabilities": [], + "Secrets": [], + "Licenses": [], + } + ] + } mock_subprocess.return_value = MagicMock( stdout=json.dumps(sample_output), stderr="" @@ -416,18 +453,23 @@ class TestIacProvider: # Test with exclude paths exclude_paths = ["node_modules", ".git", "vendor"] - result = provider.run_scan("/test/directory", ["all"], exclude_paths) + result = provider.run_scan( + "/test/directory", ["vuln", "misconfig", "secret"], exclude_paths + ) # Verify subprocess was called with correct exclude paths expected_command = [ - "checkov", - "-d", + "trivy", + "fs", "/test/directory", - "-o", + "--format", "json", - "-f", - "all", - "--skip-path", + "--scanners", + "vuln,misconfig,secret", + "--parallel", + "0", + "--include-non-failures", + "--skip-dirs", ",".join(exclude_paths), ] mock_subprocess.assert_called_once_with( @@ -438,38 +480,47 @@ class TestIacProvider: # Verify results assert len(result) == 1 - assert result[0].status == "PASS" + assert result[0].status == "FAIL" # Trivy findings are always FAIL by default @patch("subprocess.run") def test_run_scan_all_check_types(self, mock_subprocess): """Test run_scan with all types of checks (failed, passed, skipped)""" provider = IacProvider() - sample_output = [ - { - "check_type": "terraform", - "results": { - "failed_checks": [SAMPLE_FAILED_CHECK, SAMPLE_HIGH_SEVERITY_CHECK], - "passed_checks": [SAMPLE_PASSED_CHECK, SAMPLE_CLOUDFORMATION_CHECK], - "skipped_checks": [SAMPLE_SKIPPED_CHECK], - }, - } - ] + sample_output = { + "Results": [ + { + "Target": "main.tf", + "Type": "terraform", + "Misconfigurations": [ + SAMPLE_FAILED_CHECK, + SAMPLE_HIGH_SEVERITY_CHECK, + SAMPLE_PASSED_CHECK, + SAMPLE_CLOUDFORMATION_CHECK, + SAMPLE_SKIPPED_CHECK, + ], + "Vulnerabilities": [], + "Secrets": [], + "Licenses": [], + } + ] + } mock_subprocess.return_value = MagicMock( stdout=json.dumps(sample_output), stderr="" ) - result = provider.run_scan("/test/directory", ["all"], []) + result = provider.run_scan( + "/test/directory", ["vuln", "misconfig", "secret"], [] + ) # Verify results - assert len(result) == 5 # 2 failed + 2 passed + 1 skipped + assert len(result) == 5 # 5 misconfigurations # 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 + assert statuses.count("FAIL") == 4 # 4 regular findings + assert statuses.count("MUTED") == 1 # 1 skipped finding # Check that muted reports have muted=True muted_reports = [report for report in result if report.status == "MUTED"] @@ -481,9 +532,13 @@ class TestIacProvider: provider = IacProvider() # Return empty list of reports - mock_subprocess.return_value = MagicMock(stdout="[]", stderr="") + mock_subprocess.return_value = MagicMock( + stdout=json.dumps({"Results": []}), stderr="" + ) - result = provider.run_scan("/test/directory", ["terraform"], []) + result = provider.run_scan( + "/test/directory", ["vuln", "misconfig", "secret"], [] + ) # Verify results assert len(result) == 0 @@ -493,60 +548,70 @@ class TestIacProvider: """Test run_scan with multiple frameworks and different types of checks""" provider = IacProvider() - # Create sample output with multiple frameworks and different check types - sample_output = [ - { - "check_type": "terraform", - "results": { - "failed_checks": [SAMPLE_FAILED_CHECK, SAMPLE_ANOTHER_FAILED_CHECK], - "passed_checks": [SAMPLE_PASSED_CHECK], - "skipped_checks": [], + # Create sample Trivy output with multiple frameworks and different check types + sample_output = { + "Results": [ + { + "Target": "main.tf", + "Type": "terraform", + "Misconfigurations": [ + SAMPLE_FAILED_CHECK, + SAMPLE_ANOTHER_FAILED_CHECK, + SAMPLE_PASSED_CHECK, + ], + "Vulnerabilities": [], + "Secrets": [], + "Licenses": [], }, - }, - { - "check_type": "kubernetes", - "results": { - "failed_checks": [SAMPLE_KUBERNETES_CHECK], - "passed_checks": [], - "skipped_checks": [SAMPLE_ANOTHER_SKIPPED_CHECK], + { + "Target": "deployment.yaml", + "Type": "kubernetes", + "Misconfigurations": [ + SAMPLE_KUBERNETES_CHECK, + SAMPLE_ANOTHER_SKIPPED_CHECK, + ], + "Vulnerabilities": [], + "Secrets": [], + "Licenses": [], }, - }, - { - "check_type": "cloudformation", - "results": { - "failed_checks": [], - "passed_checks": [ + { + "Target": "template.yaml", + "Type": "cloudformation", + "Misconfigurations": [ SAMPLE_CLOUDFORMATION_CHECK, SAMPLE_ANOTHER_PASSED_CHECK, ], - "skipped_checks": [], + "Vulnerabilities": [], + "Secrets": [], + "Licenses": [], }, - }, - ] + ] + } mock_subprocess.return_value = MagicMock( stdout=json.dumps(sample_output), stderr="" ) result = provider.run_scan( - "/test/directory", ["terraform", "kubernetes", "cloudformation"], [] + "/test/directory", ["vuln", "misconfig", "secret"], [] ) # Verify results assert ( len(result) == 7 - ) # 2 failed + 1 passed (terraform) + 1 failed + 1 skipped (kubernetes) + 2 passed (cloudformation) + ) # 3 terraform + 2 kubernetes + 2 cloudformation = 7 total # 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 + assert statuses.count("FAIL") == 6 # 6 regular findings + assert statuses.count("MUTED") == 1 # 1 skipped finding 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"] + scan_path="/custom/path", + scanners=["vuln", "misconfig"], + exclude_path=["exclude"], ) with patch.object(provider, "run_scan") as mock_run_scan: @@ -554,7 +619,7 @@ class TestIacProvider: provider.run() mock_run_scan.assert_called_once_with( - "/custom/path", ["terraform"], ["exclude"] + "/custom/path", ["vuln", "misconfig"], ["exclude"] ) @mock.patch("prowler.providers.iac.iac_provider.porcelain.clone") diff --git a/tests/providers/m365/services/entra/entra_users_mfa_capable/entra_users_mfa_capable_test.py b/tests/providers/m365/services/entra/entra_users_mfa_capable/entra_users_mfa_capable_test.py index 4415628b2c..b84b8976ae 100644 --- a/tests/providers/m365/services/entra/entra_users_mfa_capable/entra_users_mfa_capable_test.py +++ b/tests/providers/m365/services/entra/entra_users_mfa_capable/entra_users_mfa_capable_test.py @@ -34,6 +34,7 @@ class Test_entra_users_mfa_capable: on_premises_sync_enabled=False, directory_roles_ids=[], is_mfa_capable=False, + account_enabled=True, ) } @@ -75,6 +76,7 @@ class Test_entra_users_mfa_capable: on_premises_sync_enabled=False, directory_roles_ids=[], is_mfa_capable=True, + account_enabled=True, ) } @@ -117,6 +119,7 @@ class Test_entra_users_mfa_capable: on_premises_sync_enabled=False, directory_roles_ids=[], is_mfa_capable=True, + account_enabled=True, ), user2_id: User( id=user2_id, @@ -124,6 +127,7 @@ class Test_entra_users_mfa_capable: on_premises_sync_enabled=False, directory_roles_ids=[], is_mfa_capable=False, + account_enabled=True, ), } @@ -143,3 +147,93 @@ class Test_entra_users_mfa_capable: assert result[1].resource == entra_client.users[user2_id] assert result[1].resource_name == "Test User 2" assert result[1].resource_id == user2_id + + def test_disabled_user_not_checked(self): + """Disabled user should not be checked: expected no results.""" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_users_mfa_capable.entra_users_mfa_capable.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_users_mfa_capable.entra_users_mfa_capable import ( + entra_users_mfa_capable, + ) + + user_id = str(uuid4()) + entra_client.users = { + user_id: User( + id=user_id, + name="Disabled User", + on_premises_sync_enabled=False, + directory_roles_ids=[], + is_mfa_capable=False, + account_enabled=False, # Disabled user + ) + } + + check = entra_users_mfa_capable() + result = check.execute() + + # No results should be returned for disabled users + assert len(result) == 0 + + def test_mixed_enabled_disabled_users(self): + """Mix of enabled and disabled users: only enabled users should be checked.""" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_users_mfa_capable.entra_users_mfa_capable.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_users_mfa_capable.entra_users_mfa_capable import ( + entra_users_mfa_capable, + ) + + enabled_user_id = str(uuid4()) + disabled_user_id = str(uuid4()) + entra_client.users = { + enabled_user_id: User( + id=enabled_user_id, + name="Enabled User", + on_premises_sync_enabled=False, + directory_roles_ids=[], + is_mfa_capable=True, + account_enabled=True, # Enabled user + ), + disabled_user_id: User( + id=disabled_user_id, + name="Disabled User", + on_premises_sync_enabled=False, + directory_roles_ids=[], + is_mfa_capable=False, + account_enabled=False, # Disabled user + ), + } + + check = entra_users_mfa_capable() + result = check.execute() + + # Only the enabled user should be checked + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].status_extended == "User Enabled User is MFA capable." + assert result[0].resource == entra_client.users[enabled_user_id] + assert result[0].resource_name == "Enabled User" + assert result[0].resource_id == enabled_user_id diff --git a/ui/.gitignore b/ui/.gitignore index 45c1abce86..555ceeb8d3 100644 --- a/ui/.gitignore +++ b/ui/.gitignore @@ -33,4 +33,4 @@ yarn-error.log* # typescript *.tsbuildinfo -next-env.d.ts +next-env.d.ts \ No newline at end of file diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index ee939ada4e..9badc5216a 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to the **Prowler UI** are documented in this file. ### 🚀 Added +- Security Hub integration [(#8552)](https://github.com/prowler-cloud/prowler/pull/8552) - `Cloud Provider` type filter to providers page [(#8473)](https://github.com/prowler-cloud/prowler/pull/8473) - New menu item under Configuration section for quick access to the Mutelist [(#8444)](https://github.com/prowler-cloud/prowler/pull/8444) - Resource agent to Lighthouse for querying resource information [(#8509)](https://github.com/prowler-cloud/prowler/pull/8509) @@ -15,15 +16,26 @@ All notable changes to the **Prowler UI** are documented in this file. - Disable `See Compliance` button until scan completes [(#8487)](https://github.com/prowler-cloud/prowler/pull/8487) - Provider connection filter now shows "Connected/Disconnected" instead of "true/false" for better UX [(#8520)](https://github.com/prowler-cloud/prowler/pull/8520) +- Provider Uid filter on scan page to list all UIDs regardless of connection status [(#8375)] (https://github.com/prowler-cloud/prowler/pull/8375) ### 🐞 Fixed +- Default value inside credentials form in AWS Provider add workflow properly set [(#8553)](https://github.com/prowler-cloud/prowler/pull/8553) +- Auth callback route checking working as expected [(#8556)](https://github.com/prowler-cloud/prowler/pull/8556) - DataTable column headers set to single-line [(#8480)](https://github.com/prowler-cloud/prowler/pull/8480) ### ❌ Removed --- +## [1.10.2] (Prowler v5.10.3) + +### 🐞 Fixed + +- Lighthouse using default config instead of backend config [(#8546)](https://github.com/prowler-cloud/prowler/pull/8546) + +--- + ## [1.10.1] (Prowler v5.10.1) ### 🐞 Fixed diff --git a/ui/actions/integrations/index.ts b/ui/actions/integrations/index.ts index faf38d3ec7..ab4160c009 100644 --- a/ui/actions/integrations/index.ts +++ b/ui/actions/integrations/index.ts @@ -3,6 +3,7 @@ export { deleteIntegration, getIntegration, getIntegrations, + pollConnectionTestStatus, testIntegrationConnection, updateIntegration, } from "./integrations"; diff --git a/ui/actions/integrations/integrations.ts b/ui/actions/integrations/integrations.ts index 132dff1b17..2121810a50 100644 --- a/ui/actions/integrations/integrations.ts +++ b/ui/actions/integrations/integrations.ts @@ -9,7 +9,8 @@ import { parseStringify, } from "@/lib"; -import { getTask } from "../task"; +import { getTask } from "@/actions/task"; +import { IntegrationType } from "@/types/integrations"; export const getIntegrations = async (searchParams?: URLSearchParams) => { const headers = await getAuthHeaders({ contentType: false }); @@ -59,7 +60,7 @@ export const getIntegration = async (id: string) => { export const createIntegration = async ( formData: FormData, -): Promise<{ success: string; testConnection?: any } | { error: string }> => { +): Promise<{ success: string; integrationId?: string } | { error: string }> => { const headers = await getAuthHeaders({ contentType: true }); const url = new URL(`${apiBaseUrl}/integrations`); @@ -97,25 +98,33 @@ export const createIntegration = async ( const responseData = await response.json(); const integrationId = responseData.data.id; - const testResult = await testIntegrationConnection(integrationId); + // Revalidate the appropriate page based on integration type + if (integration_type === "amazon_s3") { + revalidatePath("/integrations/amazon-s3"); + } else if (integration_type === "aws_security_hub") { + revalidatePath("/integrations/aws-security-hub"); + } return { success: "Integration created successfully!", - testConnection: testResult, + integrationId, }; } const errorData = await response.json().catch(() => ({})); const errorMessage = errorData.errors?.[0]?.detail || - `Unable to create S3 integration: ${response.statusText}`; + `Unable to create integration: ${response.statusText}`; return { error: errorMessage }; } catch (error) { return handleApiError(error); } }; -export const updateIntegration = async (id: string, formData: FormData) => { +export const updateIntegration = async ( + id: string, + formData: FormData, +): Promise<{ success: string; integrationId?: string } | { error: string }> => { const headers = await getAuthHeaders({ contentType: true }); const url = new URL(`${apiBaseUrl}/integrations/${id}`); @@ -172,33 +181,33 @@ export const updateIntegration = async (id: string, formData: FormData) => { }); if (response.ok) { - revalidatePath("/integrations/s3"); - - // Only test connection if credentials or configuration were updated - if (credentials || configuration) { - const testResult = await testIntegrationConnection(id); - return { - success: "Integration updated successfully!", - testConnection: testResult, - }; - } else { - return { - success: "Integration updated successfully!", - }; + // Revalidate the appropriate page based on integration type + if (integration_type === "amazon_s3") { + revalidatePath("/integrations/amazon-s3"); + } else if (integration_type === "aws_security_hub") { + revalidatePath("/integrations/aws-security-hub"); } + + return { + success: "Integration updated successfully!", + integrationId: id, + }; } const errorData = await response.json().catch(() => ({})); const errorMessage = errorData.errors?.[0]?.detail || - `Unable to update S3 integration: ${response.statusText}`; + `Unable to update integration: ${response.statusText}`; return { error: errorMessage }; } catch (error) { return handleApiError(error); } }; -export const deleteIntegration = async (id: string) => { +export const deleteIntegration = async ( + id: string, + integration_type: IntegrationType, +) => { const headers = await getAuthHeaders({ contentType: true }); const url = new URL(`${apiBaseUrl}/integrations/${id}`); @@ -206,14 +215,20 @@ export const deleteIntegration = async (id: string) => { const response = await fetch(url.toString(), { method: "DELETE", headers }); if (response.ok) { - revalidatePath("/integrations/s3"); + // Revalidate the appropriate page based on integration type + if (integration_type === "amazon_s3") { + revalidatePath("/integrations/amazon-s3"); + } else if (integration_type === "aws_security_hub") { + revalidatePath("/integrations/aws-security-hub"); + } + return { success: "Integration deleted successfully!" }; } const errorData = await response.json().catch(() => ({})); const errorMessage = errorData.errors?.[0]?.detail || - `Unable to delete S3 integration: ${response.statusText}`; + `Unable to delete integration: ${response.statusText}`; return { error: errorMessage }; } catch (error) { return handleApiError(error); @@ -269,7 +284,10 @@ const pollTaskUntilComplete = async (taskId: string): Promise => { return { error: "Connection test timeout. Test took too long to complete." }; }; -export const testIntegrationConnection = async (id: string) => { +export const testIntegrationConnection = async ( + id: string, + waitForCompletion = true, +) => { const headers = await getAuthHeaders({ contentType: true }); const url = new URL(`${apiBaseUrl}/integrations/${id}/connection`); @@ -281,10 +299,22 @@ export const testIntegrationConnection = async (id: string) => { const taskId = data?.data?.id; if (taskId) { + // If waitForCompletion is false, return immediately with task started status + if (!waitForCompletion) { + return { + success: true, + message: + "Connection test started. It may take some time to complete.", + taskId, + data: parseStringify(data), + }; + } + // Poll the task until completion const pollResult = await pollTaskUntilComplete(taskId); - revalidatePath("/integrations/s3"); + revalidatePath("/integrations/amazon-s3"); + revalidatePath("/integrations/aws-security-hub"); if (pollResult.error) { return { error: pollResult.error }; @@ -292,17 +322,20 @@ export const testIntegrationConnection = async (id: string) => { if (pollResult.success) { return { - success: "Connection test completed successfully!", - message: pollResult.message, + success: true, + message: + pollResult.message || "Connection test completed successfully!", data: parseStringify(data), }; } else { return { + success: false, error: pollResult.message || "Connection test failed.", }; } } else { return { + success: false, error: "Failed to start connection test. No task ID received.", }; } @@ -311,9 +344,37 @@ export const testIntegrationConnection = async (id: string) => { const errorData = await response.json().catch(() => ({})); const errorMessage = errorData.errors?.[0]?.detail || - `Unable to test S3 integration connection: ${response.statusText}`; - return { error: errorMessage }; + `Unable to test integration connection: ${response.statusText}`; + return { success: false, error: errorMessage }; } catch (error) { return handleApiError(error); } }; + +export const pollConnectionTestStatus = async (taskId: string) => { + try { + const pollResult = await pollTaskUntilComplete(taskId); + + revalidatePath("/integrations/amazon-s3"); + revalidatePath("/integrations/aws-security-hub"); + + if (pollResult.error) { + return { success: false, error: pollResult.error }; + } + + if (pollResult.success) { + return { + success: true, + message: + pollResult.message || "Connection test completed successfully!", + }; + } else { + return { + success: false, + error: pollResult.message || "Connection test failed.", + }; + } + } catch (error) { + return { success: false, error: "Failed to check connection test status." }; + } +}; diff --git a/ui/actions/lighthouse/lighthouse.ts b/ui/actions/lighthouse/lighthouse.ts index 181890fd77..980404c234 100644 --- a/ui/actions/lighthouse/lighthouse.ts +++ b/ui/actions/lighthouse/lighthouse.ts @@ -104,7 +104,7 @@ export const getLighthouseConfig = async () => { // Check if data array exists and has at least one item if (data?.data && data.data.length > 0) { - return data.data[0]; + return data.data[0].attributes; } return undefined; diff --git a/ui/app/(prowler)/integrations/s3/page.tsx b/ui/app/(prowler)/integrations/amazon-s3/page.tsx similarity index 100% rename from ui/app/(prowler)/integrations/s3/page.tsx rename to ui/app/(prowler)/integrations/amazon-s3/page.tsx diff --git a/ui/app/(prowler)/integrations/aws-security-hub/page.tsx b/ui/app/(prowler)/integrations/aws-security-hub/page.tsx new file mode 100644 index 0000000000..bbb6975840 --- /dev/null +++ b/ui/app/(prowler)/integrations/aws-security-hub/page.tsx @@ -0,0 +1,90 @@ +import React from "react"; + +import { getIntegrations } from "@/actions/integrations"; +import { getProviders } from "@/actions/providers"; +import { SecurityHubIntegrationsManager } from "@/components/integrations/security-hub/security-hub-integrations-manager"; +import { ContentLayout } from "@/components/ui"; + +interface SecurityHubIntegrationsProps { + searchParams: { [key: string]: string | string[] | undefined }; +} + +export default async function SecurityHubIntegrations({ + searchParams, +}: SecurityHubIntegrationsProps) { + const page = parseInt(searchParams.page?.toString() || "1", 10); + const pageSize = parseInt(searchParams.pageSize?.toString() || "10", 10); + const sort = searchParams.sort?.toString(); + + const filters = Object.fromEntries( + Object.entries(searchParams).filter(([key]) => key.startsWith("filter[")), + ); + + const urlSearchParams = new URLSearchParams(); + urlSearchParams.set("filter[integration_type]", "aws_security_hub"); + urlSearchParams.set("page[number]", page.toString()); + urlSearchParams.set("page[size]", pageSize.toString()); + + if (sort) { + urlSearchParams.set("sort", sort); + } + + Object.entries(filters).forEach(([key, value]) => { + if (value !== undefined && key !== "filter[integration_type]") { + const stringValue = Array.isArray(value) ? value[0] : String(value); + urlSearchParams.set(key, stringValue); + } + }); + + const [integrations, providers] = await Promise.all([ + getIntegrations(urlSearchParams), + getProviders({ pageSize: 100 }), + ]); + + const securityHubIntegrations = integrations?.data || []; + const availableProviders = providers?.data || []; + const metadata = integrations?.meta; + + return ( + +
                +
                +

                + Configure AWS Security Hub integration to automatically send your + security findings for centralized monitoring and compliance. +

                + +
                +

                + Features: +

                +
                  +
                • + + Automated findings export +
                • +
                • + + Multi-region support +
                • +
                • + + Send failed findings only +
                • +
                • + + Archive previous findings +
                • +
                +
                +
                + + +
                +
                + ); +} diff --git a/ui/app/(prowler)/integrations/page.tsx b/ui/app/(prowler)/integrations/page.tsx index 265540c2be..f4626f5abe 100644 --- a/ui/app/(prowler)/integrations/page.tsx +++ b/ui/app/(prowler)/integrations/page.tsx @@ -1,6 +1,9 @@ import React from "react"; -import { S3IntegrationCard } from "@/components/integrations"; +import { + S3IntegrationCard, + SecurityHubIntegrationCard, +} from "@/components/integrations"; import { ContentLayout } from "@/components/ui"; export default async function Integrations() { @@ -17,6 +20,9 @@ export default async function Integrations() {
                {/* Amazon S3 Integration */} + + {/* AWS Security Hub Integration */} +
                diff --git a/ui/app/(prowler)/lighthouse/config/page.tsx b/ui/app/(prowler)/lighthouse/config/page.tsx index e8e223783a..0d5632ac76 100644 --- a/ui/app/(prowler)/lighthouse/config/page.tsx +++ b/ui/app/(prowler)/lighthouse/config/page.tsx @@ -5,12 +5,12 @@ import { ContentLayout } from "@/components/ui"; export const dynamic = "force-dynamic"; export default async function ChatbotConfigPage() { - const response = await getLighthouseConfig(); - const initialValues = response?.attributes + const lighthouseConfig = await getLighthouseConfig(); + const initialValues = lighthouseConfig ? { - model: response.attributes.model, - apiKey: response.attributes.api_key || "", - businessContext: response.attributes.business_context || "", + model: lighthouseConfig.model, + apiKey: lighthouseConfig.api_key || "", + businessContext: lighthouseConfig.business_context || "", } : { model: "gpt-4o", @@ -18,7 +18,7 @@ export default async function ChatbotConfigPage() { businessContext: "", }; - const configExists = !!response; + const configExists = !!lighthouseConfig; return ( diff --git a/ui/app/(prowler)/lighthouse/page.tsx b/ui/app/(prowler)/lighthouse/page.tsx index b59cf79346..96e5ecbfb7 100644 --- a/ui/app/(prowler)/lighthouse/page.tsx +++ b/ui/app/(prowler)/lighthouse/page.tsx @@ -4,10 +4,10 @@ import { Chat } from "@/components/lighthouse"; import { ContentLayout } from "@/components/ui"; export default async function AIChatbot() { - const config = await getLighthouseConfig(); + const lighthouseConfig = await getLighthouseConfig(); - const hasConfig = !!config; - const isActive = config?.attributes?.is_active ?? false; + const hasConfig = !!lighthouseConfig; + const isActive = lighthouseConfig.is_active ?? false; return ( }> diff --git a/ui/app/(prowler)/scans/page.tsx b/ui/app/(prowler)/scans/page.tsx index 6a56ba5a71..68ce8b8e29 100644 --- a/ui/app/(prowler)/scans/page.tsx +++ b/ui/app/(prowler)/scans/page.tsx @@ -31,20 +31,22 @@ export default async function Scans({ const searchParamsKey = JSON.stringify(filteredParams); const providersData = await getProviders({ - filters: { - "filter[connected]": true, - }, pageSize: 50, }); const providerInfo = - providersData?.data?.map((provider: ProviderProps) => ({ - providerId: provider.id, - alias: provider.attributes.alias, - providerType: provider.attributes.provider, - uid: provider.attributes.uid, - connected: provider.attributes.connection.connected, - })) || []; + providersData?.data + ?.filter( + (provider: ProviderProps) => + provider.attributes.connection.connected === true, + ) + .map((provider: ProviderProps) => ({ + providerId: provider.id, + alias: provider.attributes.alias, + providerType: provider.attributes.provider, + uid: provider.attributes.uid, + connected: provider.attributes.connection.connected, + })) || []; const thereIsNoProviders = !providersData?.data; diff --git a/ui/app/api/lighthouse/analyst/route.ts b/ui/app/api/lighthouse/analyst/route.ts index 51cf2bec4f..45dd19d180 100644 --- a/ui/app/api/lighthouse/analyst/route.ts +++ b/ui/app/api/lighthouse/analyst/route.ts @@ -25,8 +25,8 @@ export async function POST(req: Request) { const processedMessages = [...messages]; // Get AI configuration to access business context - const aiConfig = await getLighthouseConfig(); - const businessContext = aiConfig?.attributes?.business_context; + const lighthouseConfig = await getLighthouseConfig(); + const businessContext = lighthouseConfig.business_context; // Get current user data const currentData = await getCurrentDataSection(); diff --git a/ui/auth.config.ts b/ui/auth.config.ts index a317a1a800..6990f8cc26 100644 --- a/ui/auth.config.ts +++ b/ui/auth.config.ts @@ -142,18 +142,17 @@ export const authConfig = { callbacks: { authorized({ auth, request: { nextUrl } }) { const isLoggedIn = !!auth?.user; - const isOnDashboard = nextUrl.pathname.startsWith("/"); const isSignUpPage = nextUrl.pathname === "/sign-up"; + const isSignInPage = nextUrl.pathname === "/sign-in"; - // Allow access to sign-up page - if (isSignUpPage) return true; + // Allow access to sign-up and sign-in pages + if (isSignUpPage || isSignInPage) return true; - if (isOnDashboard) { - if (isLoggedIn) return true; - return false; // Redirect users who are not logged in to the login page - } else if (isLoggedIn) { - return Response.redirect(new URL("/", nextUrl)); + // For all other routes, require authentication + if (!isLoggedIn) { + return false; // Will redirect to signIn page defined in pages config } + return true; }, diff --git a/ui/components/integrations/index.ts b/ui/components/integrations/index.ts index 5abff3e316..aa53bd26db 100644 --- a/ui/components/integrations/index.ts +++ b/ui/components/integrations/index.ts @@ -1,7 +1,10 @@ -export * from "../providers/provider-selector"; +export * from "../providers/enhanced-provider-selector"; export * from "./s3/s3-integration-card"; export * from "./s3/s3-integration-form"; export * from "./s3/s3-integrations-manager"; -export * from "./s3/skeleton-s3-integration-card"; export * from "./saml/saml-config-form"; export * from "./saml/saml-integration-card"; +export * from "./security-hub/security-hub-integration-card"; +export * from "./security-hub/security-hub-integration-form"; +export * from "./security-hub/security-hub-integrations-manager"; +export * from "./shared"; diff --git a/ui/components/integrations/s3/s3-integration-card.tsx b/ui/components/integrations/s3/s3-integration-card.tsx index 7f1b659c68..3c547f65fc 100644 --- a/ui/components/integrations/s3/s3-integration-card.tsx +++ b/ui/components/integrations/s3/s3-integration-card.tsx @@ -37,7 +37,7 @@ export const S3IntegrationCard = () => { size="sm" variant="bordered" startContent={} - asLink="/integrations/s3" + asLink="/integrations/amazon-s3" ariaLabel="Manage S3 integrations" > Manage diff --git a/ui/components/integrations/s3/s3-integration-form.tsx b/ui/components/integrations/s3/s3-integration-form.tsx index adc2fa1d1e..6c473b5908 100644 --- a/ui/components/integrations/s3/s3-integration-form.tsx +++ b/ui/components/integrations/s3/s3-integration-form.tsx @@ -8,7 +8,7 @@ import { useState } from "react"; import { Control, useForm } from "react-hook-form"; import { createIntegration, updateIntegration } from "@/actions/integrations"; -import { ProviderSelector } from "@/components/providers/provider-selector"; +import { EnhancedProviderSelector } from "@/components/providers/enhanced-provider-selector"; import { AWSRoleCredentialsForm } from "@/components/providers/workflow/forms/select-credentials-type/aws/credentials-type/aws-role-credentials-form"; import { useToast } from "@/components/ui"; import { CustomInput } from "@/components/ui/custom"; @@ -27,7 +27,7 @@ import { ProviderProps } from "@/types/providers"; interface S3IntegrationFormProps { integration?: IntegrationProps | null; providers: ProviderProps[]; - onSuccess: () => void; + onSuccess: (integrationId?: string, shouldTestConnection?: boolean) => void; onCancel: () => void; editMode?: "configuration" | "credentials" | null; // null means creating new } @@ -49,6 +49,11 @@ export const S3IntegrationForm = ({ const isEditingConfig = editMode === "configuration"; const isEditingCredentials = editMode === "credentials"; + const defaultCredentialsType = + process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true" + ? "aws-sdk-default" + : "access-secret-key"; + const form = useForm({ resolver: zodResolver( // For credentials editing, use creation schema (all fields required) @@ -66,21 +71,16 @@ export const S3IntegrationForm = ({ providers: integration?.relationships?.providers?.data?.map((p) => p.id) || [], enabled: integration?.attributes.enabled ?? true, - credentials_type: "access-secret-key" as const, + credentials_type: defaultCredentialsType, aws_access_key_id: "", aws_secret_access_key: "", aws_session_token: "", - // For credentials editing, show current values as placeholders but require new input - role_arn: isEditingCredentials - ? "" - : integration?.attributes.configuration.credentials?.role_arn || "", + role_arn: "", // External ID always defaults to tenantId, even when editing credentials - external_id: - integration?.attributes.configuration.credentials?.external_id || - session?.tenantId || - "", + external_id: session?.tenantId || "", role_session_name: "", session_duration: "", + show_role_section: false, }, }); @@ -116,6 +116,9 @@ export const S3IntegrationForm = ({ const buildCredentials = (values: any) => { const credentials: any = {}; + // Don't include credentials_type in the API payload - it's a UI-only field + // The backend determines credential type based on which fields are present + // Only include role-related fields if role_arn is provided if (values.role_arn && values.role_arn.trim() !== "") { credentials.role_arn = values.role_arn; @@ -202,10 +205,16 @@ export const S3IntegrationForm = ({ try { let result; + let shouldTestConnection = false; + if (isEditing && integration) { result = await updateIntegration(integration.id, formData); + // Test connection if we're editing credentials or configuration (S3 needs both) + shouldTestConnection = isEditingCredentials || isEditingConfig; } else { result = await createIntegration(formData); + // Always test connection for new integrations + shouldTestConnection = true; } if ("success" in result) { @@ -214,23 +223,8 @@ export const S3IntegrationForm = ({ description: `S3 integration ${isEditing ? "updated" : "created"} successfully.`, }); - if ("testConnection" in result) { - if (result.testConnection.success) { - toast({ - title: "Connection test started!", - description: - "Connection test started. It may take some time to complete.", - }); - } else if (result.testConnection.error) { - toast({ - variant: "destructive", - title: "Connection test failed", - description: result.testConnection.error, - }); - } - } - - onSuccess(); + // Pass the integration ID and whether to test connection to the success callback + onSuccess(result.integrationId, shouldTestConnection); } else if ("error" in result) { const errorMessage = result.error; @@ -261,6 +255,7 @@ export const S3IntegrationForm = ({ const templateLinks = getAWSCredentialsTemplateLinks( externalId, bucketName, + "amazon_s3", ); return ( @@ -269,7 +264,7 @@ export const S3IntegrationForm = ({ setValue={form.setValue as any} externalId={externalId} templateLinks={templateLinks} - type="s3-integration" + type="integrations" /> ); } @@ -280,13 +275,15 @@ export const S3IntegrationForm = ({ <> {/* Provider Selection */}
                - 10} />
                diff --git a/ui/components/integrations/s3/s3-integrations-manager.tsx b/ui/components/integrations/s3/s3-integrations-manager.tsx index 3b4f417414..1b3061dbc3 100644 --- a/ui/components/integrations/s3/s3-integrations-manager.tsx +++ b/ui/components/integrations/s3/s3-integrations-manager.tsx @@ -1,14 +1,8 @@ "use client"; -import { Card, CardBody, CardHeader, Chip } from "@nextui-org/react"; +import { Card, CardBody, CardHeader } from "@nextui-org/react"; import { format } from "date-fns"; -import { - PlusIcon, - Power, - SettingsIcon, - TestTube, - Trash2Icon, -} from "lucide-react"; +import { PlusIcon, Trash2Icon } from "lucide-react"; import { useState } from "react"; import { @@ -17,15 +11,20 @@ import { updateIntegration, } from "@/actions/integrations"; import { AmazonS3Icon } from "@/components/icons/services/IconServices"; +import { + IntegrationActionButtons, + IntegrationCardHeader, + IntegrationSkeleton, +} from "@/components/integrations/shared"; import { useToast } from "@/components/ui"; import { CustomAlertModal, CustomButton } from "@/components/ui/custom"; import { DataTablePagination } from "@/components/ui/table/data-table-pagination"; +import { triggerTestConnectionWithDelay } from "@/lib/integrations/test-connection-helper"; import { MetaDataProps } from "@/types"; import { IntegrationProps } from "@/types/integrations"; import { ProviderProps } from "@/types/providers"; import { S3IntegrationForm } from "./s3-integration-form"; -import { S3IntegrationCardSkeleton } from "./skeleton-s3-integration-card"; interface S3IntegrationsManagerProps { integrations: IntegrationProps[]; @@ -78,7 +77,7 @@ export const S3IntegrationsManager = ({ const handleDeleteIntegration = async (id: string) => { setIsDeleting(id); try { - const result = await deleteIntegration(id); + const result = await deleteIntegration(id, "amazon_s3"); if (result.success) { toast({ @@ -173,11 +172,34 @@ export const S3IntegrationsManager = ({ setEditMode(null); }; - const handleFormSuccess = () => { + const handleFormSuccess = async ( + integrationId?: string, + shouldTestConnection?: boolean, + ) => { + // Close the modal immediately setIsModalOpen(false); setEditingIntegration(null); setEditMode(null); setIsOperationLoading(true); + + // Set testing state for server-triggered test connections + if (integrationId && shouldTestConnection) { + setIsTesting(integrationId); + } + + // Trigger test connection if needed + triggerTestConnectionWithDelay( + integrationId, + shouldTestConnection, + "s3", + toast, + 200, + () => { + // Clear testing state when server-triggered test completes + setIsTesting(null); + }, + ); + // Reset loading state after a short delay to show the skeleton briefly setTimeout(() => { setIsOperationLoading(false); @@ -274,44 +296,33 @@ export const S3IntegrationsManager = ({ {/* Integrations List */} {isOperationLoading ? ( - } + title="Amazon S3" + subtitle="Export security findings to Amazon S3 buckets." /> ) : integrations.length > 0 ? (
                {integrations.map((integration) => ( -
                -
                - -
                -

                - {integration.attributes.configuration.bucket_name || - "Unknown Bucket"} -

                -

                - Output directory:{" "} - {integration.attributes.configuration - .output_directory || - integration.attributes.configuration.path || - "/"} -

                -
                -
                - - {integration.attributes.connected - ? "Connected" - : "Disconnected"} - -
                + } + title={ + integration.attributes.configuration.bucket_name || + "Unknown Bucket" + } + subtitle={`Output directory: ${ + integration.attributes.configuration.output_directory || + integration.attributes.configuration.path || + "/" + }`} + connectionStatus={{ + connected: integration.attributes.connected, + }} + />
                @@ -328,68 +339,15 @@ export const S3IntegrationsManager = ({

                )}
                -
                - } - onPress={() => handleTestConnection(integration.id)} - isLoading={isTesting === integration.id} - isDisabled={!integration.attributes.enabled} - ariaLabel="Test connection" - className="w-full sm:w-auto" - > - Test - - } - onPress={() => handleEditConfiguration(integration)} - ariaLabel="Edit configuration" - className="w-full sm:w-auto" - > - Config - - } - onPress={() => handleEditCredentials(integration)} - ariaLabel="Edit credentials" - className="w-full sm:w-auto" - > - Credentials - - } - onPress={() => handleToggleEnabled(integration)} - ariaLabel={ - integration.attributes.enabled - ? "Disable integration" - : "Enable integration" - } - className="w-full sm:w-auto" - > - {integration.attributes.enabled ? "Disable" : "Enable"} - - } - onPress={() => handleOpenDeleteModal(integration)} - ariaLabel="Delete integration" - className="w-full sm:w-auto" - > - Delete - -
                +
                diff --git a/ui/components/integrations/s3/skeleton-s3-integration-card.tsx b/ui/components/integrations/s3/skeleton-s3-integration-card.tsx deleted file mode 100644 index 645b47db78..0000000000 --- a/ui/components/integrations/s3/skeleton-s3-integration-card.tsx +++ /dev/null @@ -1,95 +0,0 @@ -"use client"; - -import { Card, CardBody, CardHeader, Skeleton } from "@nextui-org/react"; - -import { AmazonS3Icon } from "@/components/icons/services/IconServices"; - -interface S3IntegrationCardSkeletonProps { - variant?: "main" | "manager"; - count?: number; -} - -export const S3IntegrationCardSkeleton = ({ - variant = "main", - count = 1, -}: S3IntegrationCardSkeletonProps) => { - if (variant === "main") { - return ( - - -
                -
                - -
                -

                Amazon S3

                -
                -

                - Export security findings to Amazon S3 buckets. -

                - -
                -
                -
                -
                - - -
                -
                -
                - -
                -
                - {Array.from({ length: count }).map((_, index) => ( -
                -
                - - -
                - -
                - ))} -
                -
                -
                -
                - ); - } - - // Manager variant - for individual cards in S3IntegrationsManager - return ( -
                - {Array.from({ length: count }).map((_, index) => ( - - -
                -
                - -
                - - -
                -
                - -
                -
                - -
                -
                - - -
                -
                - - - -
                -
                -
                -
                - ))} -
                - ); -}; diff --git a/ui/components/integrations/security-hub/security-hub-integration-card.tsx b/ui/components/integrations/security-hub/security-hub-integration-card.tsx new file mode 100644 index 0000000000..79da80b13a --- /dev/null +++ b/ui/components/integrations/security-hub/security-hub-integration-card.tsx @@ -0,0 +1,58 @@ +"use client"; + +import { Card, CardBody, CardHeader } from "@nextui-org/react"; +import { SettingsIcon } from "lucide-react"; + +import { AWSSecurityHubIcon } from "@/components/icons/services/IconServices"; +import { CustomButton } from "@/components/ui/custom"; +import { CustomLink } from "@/components/ui/custom/custom-link"; + +export const SecurityHubIntegrationCard = () => { + return ( + + +
                +
                + +
                +

                + AWS Security Hub +

                +
                +

                + Send security findings to AWS Security Hub. +

                + + Learn more + +
                +
                +
                +
                + } + asLink="/integrations/aws-security-hub" + ariaLabel="Manage Security Hub integrations" + > + Manage + +
                +
                +
                + +
                +

                + Configure and manage your AWS Security Hub integrations to + automatically send security findings for centralized monitoring. +

                +
                +
                +
                + ); +}; diff --git a/ui/components/integrations/security-hub/security-hub-integration-form.tsx b/ui/components/integrations/security-hub/security-hub-integration-form.tsx new file mode 100644 index 0000000000..1d5a0c4399 --- /dev/null +++ b/ui/components/integrations/security-hub/security-hub-integration-form.tsx @@ -0,0 +1,490 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { Checkbox, Divider, Radio, RadioGroup } from "@nextui-org/react"; +import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react"; +import { useSession } from "next-auth/react"; +import { useEffect, useMemo, useState } from "react"; +import { Control, useForm } from "react-hook-form"; + +import { createIntegration, updateIntegration } from "@/actions/integrations"; +import { EnhancedProviderSelector } from "@/components/providers/enhanced-provider-selector"; +import { AWSRoleCredentialsForm } from "@/components/providers/workflow/forms/select-credentials-type/aws/credentials-type/aws-role-credentials-form"; +import { useToast } from "@/components/ui"; +import { CustomLink } from "@/components/ui/custom/custom-link"; +import { Form, FormControl, FormField } from "@/components/ui/form"; +import { FormButtons } from "@/components/ui/form/form-buttons"; +import { getAWSCredentialsTemplateLinks } from "@/lib"; +import { AWSCredentialsRole } from "@/types"; +import { + editSecurityHubIntegrationFormSchema, + IntegrationProps, + securityHubIntegrationFormSchema, +} from "@/types/integrations"; +import { ProviderProps } from "@/types/providers"; + +interface SecurityHubIntegrationFormProps { + integration?: IntegrationProps | null; + providers: ProviderProps[]; + existingIntegrations?: IntegrationProps[]; + onSuccess: (integrationId?: string, shouldTestConnection?: boolean) => void; + onCancel: () => void; + editMode?: "configuration" | "credentials" | null; +} + +export const SecurityHubIntegrationForm = ({ + integration, + providers, + existingIntegrations = [], + onSuccess, + onCancel, + editMode = null, +}: SecurityHubIntegrationFormProps) => { + const { data: session } = useSession(); + const { toast } = useToast(); + const [currentStep, setCurrentStep] = useState( + editMode === "credentials" ? 1 : 0, + ); + const isEditing = !!integration; + const isCreating = !isEditing; + const isEditingConfig = editMode === "configuration"; + const isEditingCredentials = editMode === "credentials"; + + const disabledProviderIds = useMemo(() => { + // When editing, no providers should be disabled since we're not changing it + if (isEditing) { + return []; + } + + // When creating, disable providers that are already used by other Security Hub integrations + const usedProviderIds: string[] = []; + existingIntegrations.forEach((existingIntegration) => { + const providerRelationships = + existingIntegration.relationships?.providers?.data; + if (providerRelationships && providerRelationships.length > 0) { + usedProviderIds.push(providerRelationships[0].id); + } + }); + + return usedProviderIds; + }, [isEditing, existingIntegrations]); + + const form = useForm({ + resolver: zodResolver( + isEditingCredentials || isCreating + ? securityHubIntegrationFormSchema + : editSecurityHubIntegrationFormSchema, + ), + defaultValues: { + integration_type: "aws_security_hub" as const, + provider_id: integration?.relationships?.providers?.data?.[0]?.id || "", + send_only_fails: + integration?.attributes.configuration.send_only_fails ?? true, + archive_previous_findings: + integration?.attributes.configuration.archive_previous_findings ?? + false, + use_custom_credentials: false, + enabled: integration?.attributes.enabled ?? true, + credentials_type: "access-secret-key" as const, + aws_access_key_id: "", + aws_secret_access_key: "", + aws_session_token: "", + role_arn: "", + external_id: session?.tenantId || "", + role_session_name: "", + session_duration: "", + show_role_section: false, + }, + }); + + const isLoading = form.formState.isSubmitting; + const useCustomCredentials = form.watch("use_custom_credentials"); + const providerIdValue = form.watch("provider_id"); + const hasErrors = !!form.formState.errors.provider_id || !providerIdValue; + + useEffect(() => { + if (!useCustomCredentials && isCreating) { + setCurrentStep(0); + } + }, [useCustomCredentials, isCreating]); + + const handleNext = async (e: React.FormEvent) => { + e.preventDefault(); + + if (isEditingConfig || isEditingCredentials) { + return; + } + + const stepFields = currentStep === 0 ? (["provider_id"] as const) : []; + const isValid = stepFields.length === 0 || (await form.trigger(stepFields)); + + if (isValid) { + setCurrentStep(1); + } + }; + + const handleBack = () => { + setCurrentStep(0); + }; + + const buildCredentials = (values: any) => { + const credentials: any = {}; + + if (values.role_arn && values.role_arn.trim() !== "") { + credentials.role_arn = values.role_arn; + credentials.external_id = values.external_id; + + if (values.role_session_name) + credentials.role_session_name = values.role_session_name; + if (values.session_duration) + credentials.session_duration = + parseInt(values.session_duration, 10) || 3600; + } + + if (values.credentials_type === "access-secret-key") { + credentials.aws_access_key_id = values.aws_access_key_id; + credentials.aws_secret_access_key = values.aws_secret_access_key; + if (values.aws_session_token) + credentials.aws_session_token = values.aws_session_token; + } + + return credentials; + }; + + const buildConfiguration = (values: any) => { + const configuration: any = {}; + + configuration.send_only_fails = values.send_only_fails ?? true; + configuration.archive_previous_findings = + values.archive_previous_findings ?? false; + + return configuration; + }; + + const buildFormData = (values: any) => { + const formData = new FormData(); + formData.append("integration_type", values.integration_type); + + if (isEditingConfig) { + const configuration = buildConfiguration(values); + if (Object.keys(configuration).length > 0) { + formData.append("configuration", JSON.stringify(configuration)); + } + } else if (isEditingCredentials) { + // When editing credentials, check if using custom credentials + if (!values.use_custom_credentials) { + // Use provider credentials - send empty object + formData.append("credentials", JSON.stringify({})); + } else { + // Use custom credentials + const credentials = buildCredentials(values); + formData.append("credentials", JSON.stringify(credentials)); + } + } else { + const configuration = buildConfiguration(values); + formData.append("configuration", JSON.stringify(configuration)); + + if (values.use_custom_credentials) { + const credentials = buildCredentials(values); + formData.append("credentials", JSON.stringify(credentials)); + } else { + formData.append("credentials", JSON.stringify({})); + } + + formData.append("enabled", JSON.stringify(values.enabled ?? true)); + + // Send provider_id as an array for consistency with the action + formData.append("providers", JSON.stringify([values.provider_id])); + } + + return formData; + }; + + const onSubmit = async (values: any) => { + const formData = buildFormData(values); + + try { + let result; + let shouldTestConnection = false; + + if (isEditing && integration) { + result = await updateIntegration(integration.id, formData); + // Test connection ONLY if we're editing credentials (Security Hub doesn't need test for config changes) + shouldTestConnection = isEditingCredentials; + } else { + result = await createIntegration(formData); + // Always test connection for new integrations + shouldTestConnection = true; + } + + if ("success" in result) { + toast({ + title: "Success!", + description: `Security Hub integration ${isEditing ? "updated" : "created"} successfully.`, + }); + + // Pass the integration ID and whether to test connection to the success callback + onSuccess(result.integrationId, shouldTestConnection); + } else if ("error" in result) { + const errorMessage = result.error; + + toast({ + variant: "destructive", + title: "Security Hub Integration Error", + description: errorMessage, + }); + } + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "An unexpected error occurred"; + + toast({ + variant: "destructive", + title: "Connection Error", + description: `${errorMessage}. Please check your network connection and try again.`, + }); + } + }; + + const renderStepContent = () => { + if (isEditingCredentials) { + // When editing credentials, show the credential type selector first + return ( +
                + { + form.setValue("use_custom_credentials", value === "custom", { + shouldValidate: true, + shouldDirty: true, + }); + }} + > + + Use provider credentials + + + Use custom credentials + + + + {useCustomCredentials && ( + <> + + } + setValue={form.setValue as any} + externalId={ + form.getValues("external_id") || session?.tenantId || "" + } + templateLinks={getAWSCredentialsTemplateLinks( + form.getValues("external_id") || session?.tenantId || "", + undefined, + "aws_security_hub", + )} + type="integrations" + /> + + )} +
                + ); + } + + if (currentStep === 1 && useCustomCredentials) { + const externalId = + form.getValues("external_id") || session?.tenantId || ""; + const templateLinks = getAWSCredentialsTemplateLinks( + externalId, + undefined, + "aws_security_hub", + ); + + return ( + } + setValue={form.setValue as any} + externalId={externalId} + templateLinks={templateLinks} + type="integrations" + /> + ); + } + + if (isEditingConfig || currentStep === 0) { + return ( + <> + {!isEditingConfig && ( + <> +
                + +
                + + + )} + +
                + ( + + + Send only Failed Findings + + + )} + /> + + ( + + + Archive previous findings + + + )} + /> + + {isCreating && ( + ( + + + Use custom credentials + + + )} + /> + )} +
                + + ); + } + + return null; + }; + + const renderStepButtons = () => { + if (isEditingConfig || isEditingCredentials) { + const updateText = isEditingConfig + ? "Update Configuration" + : "Update Credentials"; + const loadingText = isEditingConfig + ? "Updating Configuration..." + : "Updating Credentials..."; + + return ( + {}} + onCancel={onCancel} + submitText={updateText} + cancelText="Cancel" + loadingText={loadingText} + isDisabled={isLoading} + /> + ); + } + + if (currentStep === 0 && !useCustomCredentials) { + return ( + {}} + onCancel={onCancel} + submitText="Create Integration" + cancelText="Cancel" + loadingText="Creating..." + isDisabled={isLoading || hasErrors} + /> + ); + } + + if (currentStep === 0 && useCustomCredentials) { + return ( + {}} + onCancel={onCancel} + submitText="Next" + cancelText="Cancel" + loadingText="Processing..." + isDisabled={isLoading || hasErrors} + rightIcon={} + /> + ); + } + + return ( + {}} + onCancel={handleBack} + submitText="Create Integration" + cancelText="Back" + loadingText="Creating..." + leftIcon={} + isDisabled={isLoading} + /> + ); + }; + + return ( +
                + +
                +
                +

                + Need help configuring your AWS Security Hub integration? +

                + + Read the docs + +
                + {renderStepContent()} +
                + {renderStepButtons()} +
                + + ); +}; diff --git a/ui/components/integrations/security-hub/security-hub-integrations-manager.tsx b/ui/components/integrations/security-hub/security-hub-integrations-manager.tsx new file mode 100644 index 0000000000..6e16ab5497 --- /dev/null +++ b/ui/components/integrations/security-hub/security-hub-integrations-manager.tsx @@ -0,0 +1,445 @@ +"use client"; + +import { Card, CardBody, CardHeader, Chip } from "@nextui-org/react"; +import { format } from "date-fns"; +import { PlusIcon, Trash2Icon } from "lucide-react"; +import { useState } from "react"; + +import { + deleteIntegration, + testIntegrationConnection, + updateIntegration, +} from "@/actions/integrations"; +import { AWSSecurityHubIcon } from "@/components/icons/services/IconServices"; +import { + IntegrationActionButtons, + IntegrationCardHeader, + IntegrationSkeleton, +} from "@/components/integrations/shared"; +import { useToast } from "@/components/ui"; +import { CustomAlertModal, CustomButton } from "@/components/ui/custom"; +import { DataTablePagination } from "@/components/ui/table/data-table-pagination"; +import { triggerTestConnectionWithDelay } from "@/lib/integrations/test-connection-helper"; +import { MetaDataProps } from "@/types"; +import { IntegrationProps } from "@/types/integrations"; +import { ProviderProps } from "@/types/providers"; + +import { SecurityHubIntegrationForm } from "./security-hub-integration-form"; + +interface SecurityHubIntegrationsManagerProps { + integrations: IntegrationProps[]; + providers: ProviderProps[]; + metadata?: MetaDataProps; +} + +export const SecurityHubIntegrationsManager = ({ + integrations, + providers, + metadata, +}: SecurityHubIntegrationsManagerProps) => { + const [isModalOpen, setIsModalOpen] = useState(false); + const [editingIntegration, setEditingIntegration] = + useState(null); + const [editMode, setEditMode] = useState< + "configuration" | "credentials" | null + >(null); + const [isDeleting, setIsDeleting] = useState(null); + const [isTesting, setIsTesting] = useState(null); + const [isOperationLoading, setIsOperationLoading] = useState(false); + const [isDeleteOpen, setIsDeleteOpen] = useState(false); + const [integrationToDelete, setIntegrationToDelete] = + useState(null); + const { toast } = useToast(); + + const handleAddIntegration = () => { + setEditingIntegration(null); + setEditMode(null); + setIsModalOpen(true); + }; + + const handleEditConfiguration = (integration: IntegrationProps) => { + setEditingIntegration(integration); + setEditMode("configuration"); + setIsModalOpen(true); + }; + + const handleEditCredentials = (integration: IntegrationProps) => { + setEditingIntegration(integration); + setEditMode("credentials"); + setIsModalOpen(true); + }; + + const handleOpenDeleteModal = (integration: IntegrationProps) => { + setIntegrationToDelete(integration); + setIsDeleteOpen(true); + }; + + const handleDeleteIntegration = async (id: string) => { + setIsDeleting(id); + try { + const result = await deleteIntegration(id, "aws_security_hub"); + + if (result.success) { + toast({ + title: "Success!", + description: "Security Hub integration deleted successfully.", + }); + } else if (result.error) { + toast({ + variant: "destructive", + title: "Delete Failed", + description: result.error, + }); + } + } catch (error) { + toast({ + variant: "destructive", + title: "Error", + description: + "Failed to delete Security Hub integration. Please try again.", + }); + } finally { + setIsDeleting(null); + setIsDeleteOpen(false); + setIntegrationToDelete(null); + } + }; + + const handleTestConnection = async (id: string) => { + setIsTesting(id); + try { + const result = await testIntegrationConnection(id); + + if (result.success) { + toast({ + title: "Connection test successful!", + description: + result.message || "Connection test completed successfully.", + }); + } else if (result.error) { + toast({ + variant: "destructive", + title: "Connection test failed", + description: result.error, + }); + } + } catch (error) { + toast({ + variant: "destructive", + title: "Error", + description: "Failed to test connection. Please try again.", + }); + } finally { + setIsTesting(null); + } + }; + + const handleToggleEnabled = async (integration: IntegrationProps) => { + try { + const newEnabledState = !integration.attributes.enabled; + const formData = new FormData(); + formData.append( + "integration_type", + integration.attributes.integration_type, + ); + formData.append("enabled", JSON.stringify(newEnabledState)); + + const result = await updateIntegration(integration.id, formData); + + if (result && "success" in result) { + toast({ + title: "Success!", + description: `Integration ${newEnabledState ? "enabled" : "disabled"} successfully.`, + }); + + // If enabling, trigger test connection automatically + if (newEnabledState) { + setIsTesting(integration.id); + + triggerTestConnectionWithDelay( + integration.id, + true, + "security_hub", + toast, + 500, + () => { + setIsTesting(null); + }, + ); + } + } else if (result && "error" in result) { + toast({ + variant: "destructive", + title: "Toggle Failed", + description: result.error, + }); + } + } catch (error) { + toast({ + variant: "destructive", + title: "Error", + description: "Failed to toggle integration. Please try again.", + }); + } + }; + + const handleModalClose = () => { + setIsModalOpen(false); + setEditingIntegration(null); + setEditMode(null); + }; + + const handleFormSuccess = async ( + integrationId?: string, + shouldTestConnection?: boolean, + ) => { + // Close the modal immediately + setIsModalOpen(false); + setEditingIntegration(null); + setEditMode(null); + setIsOperationLoading(true); + + // Set testing state for server-triggered test connections + if (integrationId && shouldTestConnection) { + setIsTesting(integrationId); + } + + // Trigger test connection if needed + triggerTestConnectionWithDelay( + integrationId, + shouldTestConnection, + "security_hub", + toast, + 200, + () => { + // Clear testing state when server-triggered test completes + setIsTesting(null); + }, + ); + + // Reset loading state after a short delay to show the skeleton briefly + setTimeout(() => { + setIsOperationLoading(false); + }, 1500); + }; + + const getProviderDetails = (integration: IntegrationProps) => { + const providerRelationships = integration.relationships?.providers?.data; + + if (!providerRelationships || providerRelationships.length === 0) { + return { displayName: "Unknown Account", accountId: null }; + } + + // Security Hub should only have one provider + const providerId = providerRelationships[0].id; + const provider = providers.find((p) => p.id === providerId); + + if (!provider) { + return { displayName: "Unknown Account", accountId: null }; + } + + return { + displayName: provider.attributes.alias || provider.attributes.uid, + accountId: provider.attributes.uid, + alias: provider.attributes.alias, + }; + }; + + const getEnabledRegions = (integration: IntegrationProps) => { + const regions = integration.attributes.configuration.regions; + if (!regions || typeof regions !== "object") return []; + + return Object.entries(regions) + .filter(([_, enabled]) => enabled === true) + .map(([region]) => region) + .sort(); + }; + + return ( + <> + +
                + { + setIsDeleteOpen(false); + setIntegrationToDelete(null); + }} + isDisabled={isDeleting !== null} + > + Cancel + + + } + onPress={() => + integrationToDelete && + handleDeleteIntegration(integrationToDelete.id) + } + > + {isDeleting ? "Deleting..." : "Delete"} + +
                +
                + + + + + +
                +
                +
                +

                + Configured Security Hub Integrations +

                +

                + {integrations.length === 0 + ? "Not configured yet" + : `${integrations.length} integration${integrations.length !== 1 ? "s" : ""} configured`} +

                +
                + } + onPress={handleAddIntegration} + ariaLabel="Add integration" + > + Add Integration + +
                + + {isOperationLoading ? ( + } + title="AWS Security Hub" + subtitle="Send security findings to AWS Security Hub." + /> + ) : integrations.length > 0 ? ( +
                + {integrations.map((integration) => { + const enabledRegions = getEnabledRegions(integration); + const providerDetails = getProviderDetails(integration); + + return ( + + + } + title={providerDetails.displayName} + subtitle={ + providerDetails.accountId && providerDetails.alias + ? `Account ID: ${providerDetails.accountId}` + : "AWS Security Hub Integration" + } + chips={[ + { + label: integration.attributes.configuration + .send_only_fails + ? "Failed Only" + : "All Findings", + }, + { + label: integration.attributes.configuration + .archive_previous_findings + ? "Archive Previous" + : "Keep Previous", + }, + ]} + connectionStatus={{ + connected: integration.attributes.connected, + }} + /> + + +
                + {enabledRegions.length > 0 && ( +
                + {enabledRegions.map((region) => ( + + {region} + + ))} +
                + )} + +
                +
                + {integration.attributes.updated_at && ( +

                + Last updated:{" "} + {format( + new Date(integration.attributes.updated_at), + "yyyy/MM/dd", + )} +

                + )} +
                + +
                +
                +
                +
                + ); + })} +
                + ) : null} + + {metadata && integrations.length > 0 && ( +
                + +
                + )} +
                + + ); +}; diff --git a/ui/components/integrations/shared/index.ts b/ui/components/integrations/shared/index.ts new file mode 100644 index 0000000000..499b38677a --- /dev/null +++ b/ui/components/integrations/shared/index.ts @@ -0,0 +1,3 @@ +export { IntegrationActionButtons } from "./integration-action-buttons"; +export { IntegrationCardHeader } from "./integration-card-header"; +export { IntegrationSkeleton } from "./integration-skeleton"; diff --git a/ui/components/integrations/shared/integration-action-buttons.tsx b/ui/components/integrations/shared/integration-action-buttons.tsx new file mode 100644 index 0000000000..0a56480dc5 --- /dev/null +++ b/ui/components/integrations/shared/integration-action-buttons.tsx @@ -0,0 +1,100 @@ +"use client"; + +import { + LockIcon, + Power, + SettingsIcon, + TestTube, + Trash2Icon, +} from "lucide-react"; + +import { CustomButton } from "@/components/ui/custom"; +import { IntegrationProps } from "@/types/integrations"; + +interface IntegrationActionButtonsProps { + integration: IntegrationProps; + onTestConnection: (id: string) => void; + onEditConfiguration: (integration: IntegrationProps) => void; + onEditCredentials: (integration: IntegrationProps) => void; + onToggleEnabled: (integration: IntegrationProps) => void; + onDelete: (integration: IntegrationProps) => void; + isTesting?: boolean; + showCredentialsButton?: boolean; +} + +export const IntegrationActionButtons = ({ + integration, + onTestConnection, + onEditConfiguration, + onEditCredentials, + onToggleEnabled, + onDelete, + isTesting = false, + showCredentialsButton = true, +}: IntegrationActionButtonsProps) => { + return ( +
                + } + onPress={() => onTestConnection(integration.id)} + isLoading={isTesting} + isDisabled={!integration.attributes.enabled || isTesting} + ariaLabel="Test connection" + className="w-full sm:w-auto" + > + Test + + } + onPress={() => onEditConfiguration(integration)} + ariaLabel="Edit configuration" + className="w-full sm:w-auto" + > + Config + + {showCredentialsButton && ( + } + onPress={() => onEditCredentials(integration)} + ariaLabel="Edit credentials" + className="w-full sm:w-auto" + > + Credentials + + )} + } + onPress={() => onToggleEnabled(integration)} + isDisabled={isTesting} + ariaLabel={ + integration.attributes.enabled + ? "Disable integration" + : "Enable integration" + } + className="w-full sm:w-auto" + > + {integration.attributes.enabled ? "Disable" : "Enable"} + + } + onPress={() => onDelete(integration)} + ariaLabel="Delete integration" + className="w-full sm:w-auto" + > + Delete + +
                + ); +}; diff --git a/ui/components/integrations/shared/integration-card-header.tsx b/ui/components/integrations/shared/integration-card-header.tsx new file mode 100644 index 0000000000..a56473f410 --- /dev/null +++ b/ui/components/integrations/shared/integration-card-header.tsx @@ -0,0 +1,74 @@ +"use client"; + +import { Chip } from "@nextui-org/react"; +import { ReactNode } from "react"; + +interface IntegrationCardHeaderProps { + icon: ReactNode; + title: string; + subtitle?: string; + chips?: Array<{ + label: string; + color?: + | "default" + | "primary" + | "secondary" + | "success" + | "warning" + | "danger"; + variant?: "solid" | "bordered" | "light" | "flat" | "faded" | "shadow"; + }>; + connectionStatus?: { + connected: boolean; + label?: string; + }; +} + +export const IntegrationCardHeader = ({ + icon, + title, + subtitle, + chips = [], + connectionStatus, +}: IntegrationCardHeaderProps) => { + return ( +
                +
                + {icon} +
                +

                {title}

                + {subtitle && ( +

                + {subtitle} +

                + )} +
                +
                + {(chips.length > 0 || connectionStatus) && ( +
                + {chips.map((chip, index) => ( + + {chip.label} + + ))} + {connectionStatus && ( + + {connectionStatus.label || + (connectionStatus.connected ? "Connected" : "Disconnected")} + + )} +
                + )} +
                + ); +}; diff --git a/ui/components/integrations/shared/integration-skeleton.tsx b/ui/components/integrations/shared/integration-skeleton.tsx new file mode 100644 index 0000000000..73ad4e5017 --- /dev/null +++ b/ui/components/integrations/shared/integration-skeleton.tsx @@ -0,0 +1,96 @@ +"use client"; + +import { Card, CardBody, CardHeader, Skeleton } from "@nextui-org/react"; +import { ReactNode } from "react"; + +interface IntegrationSkeletonProps { + variant?: "main" | "manager"; + count?: number; + icon: ReactNode; + title?: string; + subtitle?: string; +} + +export const IntegrationSkeleton = ({ + variant = "main", + count = 1, + icon, + title = "Integration", + subtitle = "Loading integration details...", +}: IntegrationSkeletonProps) => { + if (variant === "main") { + return ( + + +
                +
                + {icon} +
                +

                {title}

                +
                +

                {subtitle}

                + +
                +
                +
                +
                + +
                +
                +
                + +
                + + +
                +
                +
                + ); + } + + // Manager variant - for individual cards in integration managers + return ( +
                + {Array.from({ length: count }).map((_, index) => ( + + +
                +
                + {icon} +
                + + +
                +
                +
                + + + +
                +
                +
                + +
                + {/* Region chips skeleton */} +
                + + + +
                +
                + +
                + + + + + +
                +
                +
                +
                +
                + ))} +
                + ); +}; diff --git a/ui/components/providers/enhanced-provider-selector.tsx b/ui/components/providers/enhanced-provider-selector.tsx new file mode 100644 index 0000000000..39779382be --- /dev/null +++ b/ui/components/providers/enhanced-provider-selector.tsx @@ -0,0 +1,294 @@ +"use client"; + +import { Button, Input, Select, SelectItem } from "@nextui-org/react"; +import { CheckSquare, Search, Square } from "lucide-react"; +import { useMemo, useState } from "react"; +import { Control } from "react-hook-form"; + +import { FormControl, FormField, FormMessage } from "@/components/ui/form"; +import { ProviderProps, ProviderType } from "@/types/providers"; + +const providerTypeLabels: Record = { + aws: "Amazon Web Services", + gcp: "Google Cloud Platform", + azure: "Microsoft Azure", + m365: "Microsoft 365", + kubernetes: "Kubernetes", + github: "GitHub", +}; + +interface EnhancedProviderSelectorProps { + control: Control; + name: string; + providers: ProviderProps[]; + label?: string; + placeholder?: string; + isInvalid?: boolean; + showFormMessage?: boolean; + selectionMode?: "single" | "multiple"; + providerType?: ProviderType; + enableSearch?: boolean; + disabledProviderIds?: string[]; +} + +export const EnhancedProviderSelector = ({ + control, + name, + providers, + label = "Provider", + placeholder = "Select provider", + isInvalid = false, + showFormMessage = true, + selectionMode = "single", + providerType, + enableSearch = false, + disabledProviderIds = [], +}: EnhancedProviderSelectorProps) => { + const [searchValue, setSearchValue] = useState(""); + + const filteredProviders = useMemo(() => { + let filtered = providers; + + // Filter by provider type if specified + if (providerType) { + filtered = filtered.filter((p) => p.attributes.provider === providerType); + } + + // Filter by search value + if (searchValue && enableSearch) { + const lowerSearch = searchValue.toLowerCase(); + filtered = filtered.filter((p) => { + const displayName = p.attributes.alias || p.attributes.uid; + const typeLabel = providerTypeLabels[p.attributes.provider]; + return ( + displayName.toLowerCase().includes(lowerSearch) || + typeLabel.toLowerCase().includes(lowerSearch) + ); + }); + } + + // Sort providers + return filtered.sort((a, b) => { + const typeComparison = a.attributes.provider.localeCompare( + b.attributes.provider, + ); + if (typeComparison !== 0) return typeComparison; + + const nameA = a.attributes.alias || a.attributes.uid; + const nameB = b.attributes.alias || b.attributes.uid; + return nameA.localeCompare(nameB); + }); + }, [providers, providerType, searchValue, enableSearch]); + + return ( + { + const isMultiple = selectionMode === "multiple"; + const selectedIds = isMultiple ? value || [] : value ? [value] : []; + const allProviderIds = filteredProviders + .filter((p) => !disabledProviderIds.includes(p.id)) + .map((p) => p.id); + const isAllSelected = + isMultiple && + allProviderIds.length > 0 && + allProviderIds.every((id) => selectedIds.includes(id)); + + const handleSelectAll = () => { + if (isAllSelected) { + onChange([]); + } else { + onChange(allProviderIds); + } + }; + + const handleSelectionChange = (keys: any) => { + if (isMultiple) { + const selectedArray = Array.from(keys); + onChange(selectedArray); + } else { + const selectedValue = Array.from(keys)[0]; + onChange(selectedValue || ""); + } + }; + + return ( + <> + +
                + {isMultiple && filteredProviders.length > 1 && ( +
                + + {label} + + +
                + )} + } + value={searchValue} + onValueChange={setSearchValue} + onClear={() => setSearchValue("")} + classNames={{ + inputWrapper: + "border-default-200 bg-transparent hover:bg-default-100/50 dark:bg-transparent dark:hover:bg-default-100/20", + input: "text-small", + clearButton: "text-default-400", + }} + /> +
                + ) : null, + }} + > + {filteredProviders.map((provider) => { + const providerType = provider.attributes.provider; + const displayName = + provider.attributes.alias || provider.attributes.uid; + const typeLabel = providerTypeLabels[providerType]; + const isDisabled = disabledProviderIds.includes( + provider.id, + ); + + return ( + +
                +
                +
                +
                + {displayName} +
                +
                + {typeLabel} + {isDisabled && ( + + (Already used) + + )} +
                +
                +
                +
                +
                +
                +
                + + ); + })} + +
                +
                + {showFormMessage && ( + + )} + + ); + }} + /> + ); +}; diff --git a/ui/components/providers/provider-selector.tsx b/ui/components/providers/provider-selector.tsx deleted file mode 100644 index a39a924bea..0000000000 --- a/ui/components/providers/provider-selector.tsx +++ /dev/null @@ -1,201 +0,0 @@ -"use client"; - -import { Button, Select, SelectItem } from "@nextui-org/react"; -import { CheckSquare, Square } from "lucide-react"; -import { Control } from "react-hook-form"; - -import { FormControl, FormField, FormMessage } from "@/components/ui/form"; -import { ProviderProps, ProviderType } from "@/types/providers"; - -const providerTypeLabels: Record = { - aws: "Amazon Web Services", - gcp: "Google Cloud Platform", - azure: "Microsoft Azure", - m365: "Microsoft 365", - kubernetes: "Kubernetes", - github: "GitHub", -}; - -interface ProviderSelectorProps { - control: Control; - name: string; - providers: ProviderProps[]; - label?: string; - placeholder?: string; - isInvalid?: boolean; - showFormMessage?: boolean; -} - -export const ProviderSelector = ({ - control, - name, - providers, - label = "Providers", - placeholder = "Select providers", - isInvalid = false, - showFormMessage = true, -}: ProviderSelectorProps) => { - // Sort providers by type and then by name for better organization - const sortedProviders = [...providers].sort((a, b) => { - const typeComparison = a.attributes.provider.localeCompare( - b.attributes.provider, - ); - if (typeComparison !== 0) return typeComparison; - - const nameA = a.attributes.alias || a.attributes.uid; - const nameB = b.attributes.alias || b.attributes.uid; - return nameA.localeCompare(nameB); - }); - - return ( - { - const selectedIds = value || []; - const allProviderIds = sortedProviders.map((p) => p.id); - const isAllSelected = - allProviderIds.length > 0 && - allProviderIds.every((id) => selectedIds.includes(id)); - - const handleSelectAll = () => { - if (isAllSelected) { - onChange([]); - } else { - onChange(allProviderIds); - } - }; - - return ( - <> - -
                -
                - - {label} - - {sortedProviders.length > 1 && ( - - )} -
                - -
                -
                - {showFormMessage && ( - - )} - - ); - }} - /> - ); -}; diff --git a/ui/components/providers/workflow/credentials-role-helper.tsx b/ui/components/providers/workflow/credentials-role-helper.tsx index 209b62e6ee..52d5952530 100644 --- a/ui/components/providers/workflow/credentials-role-helper.tsx +++ b/ui/components/providers/workflow/credentials-role-helper.tsx @@ -11,7 +11,7 @@ interface CredentialsRoleHelperProps { cloudformationQuickLink: string; terraform: string; }; - type?: "providers" | "s3-integration"; + type?: "providers" | "integrations"; } export const CredentialsRoleHelper = ({ @@ -24,7 +24,7 @@ export const CredentialsRoleHelper = ({

                A read-only IAM role must be manually created - {type === "s3-integration" ? " or updated" : ""}. + {type === "integrations" ? " or updated" : ""}.

                { const isCloudEnv = process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true"; const defaultCredentialsType = isCloudEnv ? "aws-sdk-default" : "access-secret-key"; + const credentialsType = useWatch({ control, name: ProviderCredentialFields.CREDENTIALS_TYPE, defaultValue: defaultCredentialsType, }); + const [showOptionalRole, setShowOptionalRole] = useState(false); + const showRoleSection = type === "providers" || (isCloudEnv && credentialsType === "aws-sdk-default") || showOptionalRole; + // Track role section visibility and ensure external_id is set + useEffect(() => { + // Set show_role_section for validation + setValue("show_role_section" as any, showRoleSection); + + // When role section is shown, ensure external_id is set + // This handles both initial mount and when the section becomes visible + if (showRoleSection && externalId) { + setValue(ProviderCredentialFields.EXTERNAL_ID, externalId, { + shouldValidate: false, + shouldDirty: false, + }); + } + }, [showRoleSection, setValue, externalId]); + return ( <>
                @@ -57,7 +75,7 @@ export const AWSRoleCredentialsForm = ({ name={ProviderCredentialFields.CREDENTIALS_TYPE} label="Authentication Method" placeholder="Select credentials type" - defaultSelectedKeys={[defaultCredentialsType]} + selectedKeys={[credentialsType || defaultCredentialsType]} className="mb-4" variant="bordered" onSelectionChange={(keys) => @@ -182,7 +200,7 @@ export const AWSRoleCredentialsForm = ({ labelPlacement="inside" placeholder="Enter the Role ARN" variant="bordered" - isRequired={type === "providers"} + isRequired={showRoleSection} isInvalid={ !!control._formState.errors[ProviderCredentialFields.ROLE_ARN] } diff --git a/ui/hooks/use-credentials-form.ts b/ui/hooks/use-credentials-form.ts index 0180c06431..5067a8c6c0 100644 --- a/ui/hooks/use-credentials-form.ts +++ b/ui/hooks/use-credentials-form.ts @@ -64,9 +64,13 @@ export const useCredentialsForm = ({ // AWS Role credentials if (providerType === "aws" && via === "role") { + const isCloudEnv = process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true"; + const defaultCredentialsType = isCloudEnv + ? "aws-sdk-default" + : "access-secret-key"; return { ...baseDefaults, - [ProviderCredentialFields.CREDENTIALS_TYPE]: "access-secret-key", + [ProviderCredentialFields.CREDENTIALS_TYPE]: defaultCredentialsType, [ProviderCredentialFields.ROLE_ARN]: "", [ProviderCredentialFields.EXTERNAL_ID]: session?.tenantId || "", [ProviderCredentialFields.AWS_ACCESS_KEY_ID]: "", diff --git a/ui/lib/external-urls.ts b/ui/lib/external-urls.ts index c38f1a3574..125a227b87 100644 --- a/ui/lib/external-urls.ts +++ b/ui/lib/external-urls.ts @@ -1,3 +1,5 @@ +import { IntegrationType } from "../types/integrations"; + export const getProviderHelpText = (provider: string) => { switch (provider) { case "aws": @@ -41,12 +43,46 @@ export const getProviderHelpText = (provider: string) => { export const getAWSCredentialsTemplateLinks = ( externalId: string, bucketName?: string, -) => { + integrationType?: IntegrationType, +): { + cloudformation: string; + terraform: string; + cloudformationQuickLink: string; +} => { + let links = {}; + + if (integrationType === undefined) { + links = { + cloudformation: + "https://github.com/prowler-cloud/prowler/blob/master/permissions/templates/cloudformation/prowler-scan-role.yml", + terraform: + "https://github.com/prowler-cloud/prowler/tree/master/permissions/templates/terraform", + }; + } + + if (integrationType === "aws_security_hub") { + links = { + cloudformation: + "https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/prowler-app-s3-integration/", + terraform: + "https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/prowler-app-s3-integration/#terraform", + }; + } + + if (integrationType === "amazon_s3") { + links = { + cloudformation: + "https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/prowler-app-s3-integration/", + terraform: + "https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/prowler-app-s3-integration/#terraform", + }; + } + return { - cloudformation: - "https://github.com/prowler-cloud/prowler/blob/master/permissions/templates/cloudformation/prowler-scan-role.yml", + ...(links as { + cloudformation: string; + terraform: string; + }), cloudformationQuickLink: `https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/quickcreate?templateURL=https%3A%2F%2Fprowler-cloud-public.s3.eu-west-1.amazonaws.com%2Fpermissions%2Ftemplates%2Faws%2Fcloudformation%2Fprowler-scan-role.yml&stackName=Prowler¶m_ExternalId=${externalId}${bucketName ? `¶m_EnableS3Integration=true¶m_S3IntegrationBucketName=${bucketName}` : ""}`, - terraform: - "https://github.com/prowler-cloud/prowler/tree/master/permissions/templates/terraform", }; }; diff --git a/ui/lib/integrations/test-connection-helper.ts b/ui/lib/integrations/test-connection-helper.ts new file mode 100644 index 0000000000..6c92adbf39 --- /dev/null +++ b/ui/lib/integrations/test-connection-helper.ts @@ -0,0 +1,161 @@ +import { + pollConnectionTestStatus, + testIntegrationConnection, +} from "@/actions/integrations"; + +// Integration configuration type +export interface IntegrationMessages { + testingMessage: string; + successMessage: string; + errorMessage: string; +} + +// Configuration map for integration-specific messages +const INTEGRATION_CONFIG: Record = { + "amazon-s3": { + testingMessage: "Testing connection to Amazon S3 bucket...", + successMessage: "Successfully connected to Amazon S3 bucket.", + errorMessage: "Failed to connect to Amazon S3 bucket.", + }, + "aws-security-hub": { + testingMessage: "Testing connection to AWS Security Hub...", + successMessage: "Successfully connected to AWS Security Hub.", + errorMessage: "Failed to connect to AWS Security Hub.", + }, + // Legacy mappings for backward compatibility + s3: { + testingMessage: "Testing connection to Amazon S3 bucket...", + successMessage: "Successfully connected to Amazon S3 bucket.", + errorMessage: "Failed to connect to Amazon S3 bucket.", + }, + security_hub: { + testingMessage: "Testing connection to AWS Security Hub...", + successMessage: "Successfully connected to AWS Security Hub.", + errorMessage: "Failed to connect to AWS Security Hub.", + }, + // Add new integrations here as needed +}; + +// Helper function to register new integration types +export const registerIntegrationType = ( + type: string, + messages: IntegrationMessages, +): void => { + INTEGRATION_CONFIG[type] = messages; +}; + +// Helper function to get supported integration types +export const getSupportedIntegrationTypes = (): string[] => { + return Object.keys(INTEGRATION_CONFIG); +}; + +interface TestConnectionOptions { + integrationId: string; + integrationType: string; + onSuccess?: (message: string) => void; + onError?: (message: string) => void; + onStart?: () => void; + onComplete?: () => void; +} + +export const runTestConnection = async ({ + integrationId, + integrationType, + onSuccess, + onError, + onStart, + onComplete, +}: TestConnectionOptions) => { + try { + // Start the test without waiting for completion + const result = await testIntegrationConnection(integrationId, false); + + if (!result || (!result.success && !result.error)) { + onError?.("Connection test could not be started. Please try again."); + onComplete?.(); + return; + } + + if (result.error) { + onError?.(result.error); + onComplete?.(); + return; + } + + if (!result.taskId) { + onError?.("Failed to start connection test. No task ID received."); + onComplete?.(); + return; + } + + // Notify that test has started + onStart?.(); + + // Poll for the test completion + const pollResult = await pollConnectionTestStatus(result.taskId); + + if (pollResult.success) { + const config = INTEGRATION_CONFIG[integrationType]; + const defaultMessage = + config?.successMessage || + `Successfully connected to ${integrationType}.`; + onSuccess?.(pollResult.message || defaultMessage); + } else { + const config = INTEGRATION_CONFIG[integrationType]; + const defaultError = + config?.errorMessage || `Failed to connect to ${integrationType}.`; + onError?.(pollResult.error || defaultError); + } + } catch (error) { + onError?.( + "Failed to start connection test. You can try manually using the Test Connection button.", + ); + } finally { + onComplete?.(); + } +}; + +export const triggerTestConnectionWithDelay = ( + integrationId: string | undefined, + shouldTestConnection: boolean | undefined, + integrationType: string, + toast: any, + delay = 200, + onComplete?: () => void, +) => { + if (!integrationId || !shouldTestConnection) { + onComplete?.(); + return; + } + + setTimeout(() => { + runTestConnection({ + integrationId, + integrationType, + onStart: () => { + const config = INTEGRATION_CONFIG[integrationType]; + const description = + config?.testingMessage || + `Testing connection to ${integrationType}...`; + toast({ + title: "Connection test started!", + description, + }); + }, + onSuccess: (message) => { + toast({ + title: "Connection test successful!", + description: message, + }); + }, + onError: (message) => { + toast({ + variant: "destructive", + title: "Connection test failed", + description: message, + }); + }, + onComplete, + }); + }, delay); +}; diff --git a/ui/lib/lighthouse/utils.ts b/ui/lib/lighthouse/utils.ts index 296bdced18..78f760c8c7 100644 --- a/ui/lib/lighthouse/utils.ts +++ b/ui/lib/lighthouse/utils.ts @@ -6,6 +6,8 @@ import { } from "@langchain/core/messages"; import type { Message } from "ai"; +import type { ModelParams } from "@/types/lighthouse"; + // https://stackoverflow.com/questions/79081298/how-to-stream-langchain-langgraphs-final-generation /** * Converts a Vercel message to a LangChain message. @@ -46,3 +48,20 @@ export const convertLangChainMessageToVercelMessage = ( return { content: message.content, role: message.getType() }; } }; + +export const getModelParams = (config: any): ModelParams => { + const modelId = config.model; + + const params: ModelParams = { + maxTokens: config.max_tokens, + temperature: config.temperature, + reasoningEffort: undefined, + }; + + if (modelId.startsWith("gpt-5")) { + params.temperature = undefined; + params.reasoningEffort = "minimal"; + } + + return params; +}; diff --git a/ui/lib/lighthouse/workflow.ts b/ui/lib/lighthouse/workflow.ts index ff5996b95b..d07b04cc51 100644 --- a/ui/lib/lighthouse/workflow.ts +++ b/ui/lib/lighthouse/workflow.ts @@ -47,28 +47,28 @@ import { getMyProfileInfoTool, getUsersTool, } from "@/lib/lighthouse/tools/users"; +import { getModelParams } from "@/lib/lighthouse/utils"; export async function initLighthouseWorkflow() { const apiKey = await getAIKey(); - const aiConfig = await getLighthouseConfig(); - const modelConfig = aiConfig?.data?.attributes; + const lighthouseConfig = await getLighthouseConfig(); + + const modelParams = getModelParams(lighthouseConfig); // Initialize models without API keys const llm = new ChatOpenAI({ - model: modelConfig?.model || "gpt-4o", - temperature: modelConfig?.temperature || 0, - maxTokens: modelConfig?.max_tokens || 4000, + model: lighthouseConfig.model, apiKey: apiKey, tags: ["agent"], + ...modelParams, }); const supervisorllm = new ChatOpenAI({ - model: modelConfig?.model || "gpt-4o", - temperature: modelConfig?.temperature || 0, - maxTokens: modelConfig?.max_tokens || 4000, + model: lighthouseConfig.model, apiKey: apiKey, streaming: true, tags: ["supervisor"], + ...modelParams, }); const providerAgent = createReactAgent({ diff --git a/ui/package-lock.json b/ui/package-lock.json index 848d06cba1..0d53f26376 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@hookform/resolvers": "^3.9.0", "@langchain/langgraph-supervisor": "^0.0.12", - "@langchain/openai": "0.5.10", + "@langchain/openai": "^0.6.9", "@next/third-parties": "^15.3.3", "@nextui-org/react": "2.4.8", "@nextui-org/system": "2.2.1", @@ -1052,9 +1052,9 @@ } }, "node_modules/@langchain/core": { - "version": "0.3.62", - "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.3.62.tgz", - "integrity": "sha512-GqRTcoUPnozGRMUcA6QkP7LHL/OvanGdB51Jgb0w7IIPDI3wFugxMHZ4gphnGDtxsD1tQY5ykyEpYNxFK8kl1w==", + "version": "0.3.72", + "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.3.72.tgz", + "integrity": "sha512-WsGWVZYnlKffj2eEfDocPNiaTRoxyYiLSQdQ7oxZvxGZBqo/90vpjbC33UGK1uPNBM4kT+pkdaol/MnvKUh8TQ==", "license": "MIT", "peer": true, "dependencies": { @@ -1063,7 +1063,7 @@ "camelcase": "6", "decamelize": "1.2.0", "js-tiktoken": "^1.0.12", - "langsmith": "^0.3.33", + "langsmith": "^0.3.46", "mustache": "^4.2.0", "p-queue": "^6.6.2", "p-retry": "4", @@ -1228,21 +1228,20 @@ } }, "node_modules/@langchain/openai": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-0.5.10.tgz", - "integrity": "sha512-hBQIWjcVxGS7tgVvgBBmrZ5jSaJ8nu9g6V64/Tx6KGjkW7VdGmUvqCO+koiQCOZVL7PBJkHWAvDsbghPYXiZEA==", + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-0.6.9.tgz", + "integrity": "sha512-Dl+YVBTFia7WE4/jFemQEVchPbsahy/dD97jo6A9gLnYfTkWa/jh8Q78UjHQ3lobif84j2ebjHPcDHG1L0NUWg==", "license": "MIT", "dependencies": { "js-tiktoken": "^1.0.12", - "openai": "^4.96.0", - "zod": "^3.22.4", - "zod-to-json-schema": "^3.22.3" + "openai": "5.12.2", + "zod": "^3.25.32" }, "engines": { "node": ">=18" }, "peerDependencies": { - "@langchain/core": ">=0.3.48 <0.4.0" + "@langchain/core": ">=0.3.68 <0.4.0" } }, "node_modules/@napi-rs/wasm-runtime": { @@ -6688,18 +6687,9 @@ "version": "20.5.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.7.tgz", "integrity": "sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA==", + "dev": true, "license": "MIT" }, - "node_modules/@types/node-fetch": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", - "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "form-data": "^4.0.0" - } - }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", @@ -7213,18 +7203,6 @@ "win32" ] }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "license": "MIT", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -7263,18 +7241,6 @@ "node": ">= 14" } }, - "node_modules/agentkeepalive": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", - "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", - "license": "MIT", - "dependencies": { - "humanize-ms": "^1.2.1" - }, - "engines": { - "node": ">= 8.0.0" - } - }, "node_modules/ai": { "version": "4.3.16", "resolved": "https://registry.npmjs.org/ai/-/ai-4.3.16.tgz", @@ -7606,12 +7572,6 @@ "node": ">= 0.4" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, "node_modules/autoprefixer": { "version": "10.4.19", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", @@ -7863,6 +7823,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -8268,18 +8229,6 @@ "dev": true, "license": "MIT" }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", @@ -8698,15 +8647,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -8811,6 +8751,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -8944,6 +8885,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8953,6 +8895,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8990,6 +8933,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -9002,6 +8946,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -9827,15 +9772,6 @@ "node": ">=0.10.0" } }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", @@ -10084,41 +10020,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/form-data-encoder": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", - "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", - "license": "MIT" - }, - "node_modules/formdata-node": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", - "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", - "license": "MIT", - "dependencies": { - "node-domexception": "1.0.0", - "web-streams-polyfill": "4.0.0-beta.3" - }, - "engines": { - "node": ">= 12.20" - } - }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -10264,6 +10165,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -10297,6 +10199,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -10442,6 +10345,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -10518,6 +10422,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -10530,6 +10435,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -10626,15 +10532,6 @@ "node": ">=16.17.0" } }, - "node_modules/humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.0.0" - } - }, "node_modules/husky": { "version": "9.1.7", "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", @@ -11533,9 +11430,9 @@ } }, "node_modules/langsmith": { - "version": "0.3.38", - "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.3.38.tgz", - "integrity": "sha512-2f3h427UCEi9jpGvdZp6eZNDhOzcSFSdwyZAax4Wo80NWozkXunepKkiXdQNloUZzE++fh3lxrjOqvimAJ0SSg==", + "version": "0.3.62", + "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.3.62.tgz", + "integrity": "sha512-ApoGLs28cJCxL91l1PDDkjsA4oLrbeNlE1pyTvyopqXq9bNJrP8JPUNWZm/tpU0DzZpvZFctRzru4gNAr/bkxg==", "license": "MIT", "peer": true, "dependencies": { @@ -12075,6 +11972,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -12703,27 +12601,6 @@ "node": ">=8.6" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/mimic-fn": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", @@ -13026,26 +12903,6 @@ "node": ">=10.5.0" } }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -13265,19 +13122,10 @@ } }, "node_modules/openai": { - "version": "4.104.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz", - "integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==", + "version": "5.12.2", + "resolved": "https://registry.npmjs.org/openai/-/openai-5.12.2.tgz", + "integrity": "sha512-xqzHHQch5Tws5PcKR2xsZGX9xtch+JQFz5zb14dGqlshmmDAFBFEWmeIpf7wVqWV+w7Emj7jRgkNJakyKE0tYQ==", "license": "Apache-2.0", - "dependencies": { - "@types/node": "^18.11.18", - "@types/node-fetch": "^2.6.4", - "abort-controller": "^3.0.0", - "agentkeepalive": "^4.2.1", - "form-data-encoder": "1.7.2", - "formdata-node": "^4.3.2", - "node-fetch": "^2.6.7" - }, "bin": { "openai": "bin/cli" }, @@ -13294,15 +13142,6 @@ } } }, - "node_modules/openai/node_modules/@types/node": { - "version": "18.19.115", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.115.tgz", - "integrity": "sha512-kNrFiTgG4a9JAn1LMQeLOv3MvXIPokzXziohMrMsvpYgLpdEt/mMiVYc4sGKtDfyxM5gIDF4VgrPRyCw4fHOYg==", - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -15984,12 +15823,6 @@ "node": ">=8.0" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -16195,12 +16028,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "license": "MIT" - }, "node_modules/unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", @@ -16564,31 +16391,6 @@ "defaults": "^1.0.3" } }, - "node_modules/web-streams-polyfill": { - "version": "4.0.0-beta.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", - "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/ui/package.json b/ui/package.json index d569e10eff..05484fe77d 100644 --- a/ui/package.json +++ b/ui/package.json @@ -2,7 +2,7 @@ "dependencies": { "@hookform/resolvers": "^3.9.0", "@langchain/langgraph-supervisor": "^0.0.12", - "@langchain/openai": "0.5.10", + "@langchain/openai": "^0.6.9", "@next/third-parties": "^15.3.3", "@nextui-org/react": "2.4.8", "@nextui-org/system": "2.2.1", @@ -43,9 +43,9 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-hook-form": "^7.52.2", - "rss-parser": "^3.13.0", "react-markdown": "^10.1.0", "recharts": "^2.15.2", + "rss-parser": "^3.13.0", "server-only": "^0.0.1", "shadcn-ui": "^0.2.3", "sharp": "^0.33.5", diff --git a/ui/tests/auth-login.spec.ts b/ui/tests/auth-login.spec.ts index fc612a71a5..7176159d2b 100644 --- a/ui/tests/auth-login.spec.ts +++ b/ui/tests/auth-login.spec.ts @@ -134,25 +134,56 @@ test.describe("Session Persistence", () => { await goToLogin(page); await login(page, TEST_CREDENTIALS.VALID); await verifySuccessfulLogin(page); + // Logout await logout(page); await verifyLogoutSuccess(page); + // Verify cannot access protected route after logout await page.goto(URLS.DASHBOARD); await expect(page).toHaveURL(URLS.LOGIN); }); - test("should handle session timeout gracefully", async ({ page }) => { - // Login first - await goToLogin(page); - await login(page, TEST_CREDENTIALS.VALID); - await verifySuccessfulLogin(page); - // Simulate session timeout by clearing cookies - await page.context().clearCookies(); - // Try to navigate to a protected route - await page.goto(URLS.PROFILE); - // Should be redirected to login - await expect(page).toHaveURL(URLS.LOGIN); + test("should handle session timeout gracefully", async ({ browser }) => { + // Test approach: Verify that a new browser context without auth cookies + // gets redirected to login when accessing protected routes + + // First, login in one context to verify auth works + const authContext = await browser.newContext(); + const authPage = await authContext.newPage(); + + await goToLogin(authPage); + await login(authPage, TEST_CREDENTIALS.VALID); + await verifySuccessfulLogin(authPage); + + // Verify session exists in authenticated context + const authResponse = await authPage.request.get("/api/auth/session"); + const authSession = await authResponse.json(); + expect(authSession).toBeTruthy(); + expect(authSession.user).toBeTruthy(); + + // Now create a completely separate context without any auth + const unauthContext = await browser.newContext(); + const unauthPage = await unauthContext.newPage(); + + // Try to access protected route in unauthenticated context + await unauthPage.goto(URLS.PROFILE, { + waitUntil: "networkidle", + }); + + // Should be redirected to login since this context has no auth + await expect(unauthPage).toHaveURL(URLS.LOGIN); + + // Verify session is null in unauthenticated context + const unauthResponse = await unauthPage.request.get("/api/auth/session"); + const unauthSessionText = await unauthResponse.text(); + expect(unauthSessionText).toBe("null"); + + // Clean up + await authPage.close(); + await authContext.close(); + await unauthPage.close(); + await unauthContext.close(); }); }); diff --git a/ui/types/integrations.ts b/ui/types/integrations.ts index 7601ae4e6d..a95174cb45 100644 --- a/ui/types/integrations.ts +++ b/ui/types/integrations.ts @@ -32,59 +32,45 @@ export interface IntegrationProps { links: { self: string }; } -const baseS3IntegrationSchema = z.object({ - integration_type: z.literal("amazon_s3"), - bucket_name: z.string().min(1, "Bucket name is required"), - output_directory: z.string().min(1, "Output directory is required"), - providers: z.array(z.string()).optional(), - enabled: z.boolean().optional(), - // AWS Credentials fields compatible with AWSCredentialsRole +// Shared AWS credential fields schema +const awsCredentialFields = { credentials_type: z.enum(["aws-sdk-default", "access-secret-key"]), aws_access_key_id: z.string().optional(), aws_secret_access_key: z.string().optional(), aws_session_token: z.string().optional(), - // IAM Role fields role_arn: z.string().optional(), external_id: z.string().optional(), role_session_name: z.string().optional(), session_duration: z.string().optional(), -}); + show_role_section: z.boolean().optional(), +}; -const s3IntegrationValidation = (data: any, ctx: z.RefinementCtx) => { - // If using access-secret-key, require AWS credentials (for create form) - if (data.credentials_type === "access-secret-key") { +// Shared validation helper for AWS credentials (create mode) +const validateAwsCredentialsCreate = ( + data: any, + ctx: z.RefinementCtx, + requireCredentials: boolean = true, +) => { + if (data.credentials_type === "access-secret-key" && requireCredentials) { if (!data.aws_access_key_id) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: - "AWS Access Key ID is required when using access and secret key", + message: "AWS Access Key ID is required when using access and secret key", path: ["aws_access_key_id"], }); } if (!data.aws_secret_access_key) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: - "AWS Secret Access Key is required when using access and secret key", + message: "AWS Secret Access Key is required when using access and secret key", path: ["aws_secret_access_key"], }); } } - - // If role_arn is provided, external_id is required - if (data.role_arn && data.role_arn.trim() !== "") { - if (!data.external_id || data.external_id.trim() === "") { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "External ID is required when using Role ARN", - path: ["external_id"], - }); - } - } }; -const s3IntegrationEditValidation = (data: any, ctx: z.RefinementCtx) => { - // If using access-secret-key, and credentials are provided, require both +// Shared validation helper for AWS credentials (edit mode) +const validateAwsCredentialsEdit = (data: any, ctx: z.RefinementCtx) => { if (data.credentials_type === "access-secret-key") { const hasAccessKey = !!data.aws_access_key_id; const hasSecretKey = !!data.aws_secret_access_key; @@ -92,8 +78,7 @@ const s3IntegrationEditValidation = (data: any, ctx: z.RefinementCtx) => { if (hasAccessKey && !hasSecretKey) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: - "AWS Secret Access Key is required when providing Access Key ID", + message: "AWS Secret Access Key is required when providing Access Key ID", path: ["aws_secret_access_key"], }); } @@ -101,16 +86,29 @@ const s3IntegrationEditValidation = (data: any, ctx: z.RefinementCtx) => { if (hasSecretKey && !hasAccessKey) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: - "AWS Access Key ID is required when providing Secret Access Key", + message: "AWS Access Key ID is required when providing Secret Access Key", path: ["aws_access_key_id"], }); } } +}; - // If role_arn is provided, external_id is required - if (data.role_arn && data.role_arn.trim() !== "") { - if (!data.external_id || data.external_id.trim() === "") { +// Shared validation helper for IAM Role fields +const validateIamRole = ( + data: any, + ctx: z.RefinementCtx, + checkShowSection: boolean = true, +) => { + const shouldValidate = checkShowSection ? data.show_role_section === true : true; + + if (shouldValidate && data.role_arn) { + if (data.role_arn.trim() === "") { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Role ARN is required", + path: ["role_arn"], + }); + } else if (!data.external_id || data.external_id.trim() === "") { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "External ID is required when using Role ARN", @@ -118,8 +116,35 @@ const s3IntegrationEditValidation = (data: any, ctx: z.RefinementCtx) => { }); } } + + if (checkShowSection && data.show_role_section === true) { + if (!data.role_arn || data.role_arn.trim() === "") { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Role ARN is required", + path: ["role_arn"], + }); + } + if (!data.external_id || data.external_id.trim() === "") { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "External ID is required", + path: ["external_id"], + }); + } + } }; +// S3 Integration Schemas +const baseS3IntegrationSchema = z.object({ + integration_type: z.literal("amazon_s3"), + bucket_name: z.string().min(1, "Bucket name is required"), + output_directory: z.string().min(1, "Output directory is required"), + providers: z.array(z.string()).optional(), + enabled: z.boolean().optional(), + ...awsCredentialFields, +}); + export const s3IntegrationFormSchema = baseS3IntegrationSchema .extend({ enabled: z.boolean().default(true), @@ -127,18 +152,67 @@ export const s3IntegrationFormSchema = baseS3IntegrationSchema .enum(["aws-sdk-default", "access-secret-key"]) .default("aws-sdk-default"), }) - .superRefine(s3IntegrationValidation); + .superRefine((data, ctx) => { + validateAwsCredentialsCreate(data, ctx); + validateIamRole(data, ctx); + }); export const editS3IntegrationFormSchema = baseS3IntegrationSchema .extend({ bucket_name: z.string().min(1, "Bucket name is required").optional(), - output_directory: z - .string() - .min(1, "Output directory is required") - .optional(), + output_directory: z.string().min(1, "Output directory is required").optional(), providers: z.array(z.string()).optional(), + credentials_type: z.enum(["aws-sdk-default", "access-secret-key"]).optional(), + }) + .superRefine((data, ctx) => { + validateAwsCredentialsEdit(data, ctx); + validateIamRole(data, ctx); + }); + +// Security Hub Integration Schemas +const baseSecurityHubIntegrationSchema = z.object({ + integration_type: z.literal("aws_security_hub"), + provider_id: z.string().min(1, "AWS Provider is required"), + send_only_fails: z.boolean().optional(), + archive_previous_findings: z.boolean().optional(), + use_custom_credentials: z.boolean().optional(), + enabled: z.boolean().optional(), + ...awsCredentialFields, +}); + +export const securityHubIntegrationFormSchema = baseSecurityHubIntegrationSchema + .extend({ + enabled: z.boolean().default(true), + send_only_fails: z.boolean().default(true), + archive_previous_findings: z.boolean().default(false), + use_custom_credentials: z.boolean().default(false), credentials_type: z .enum(["aws-sdk-default", "access-secret-key"]) - .optional(), + .default("aws-sdk-default"), }) - .superRefine(s3IntegrationEditValidation); + .superRefine((data, ctx) => { + if (data.use_custom_credentials) { + validateAwsCredentialsCreate(data, ctx); + validateIamRole(data, ctx); + } + // Always validate role if role_arn is provided + if (!data.use_custom_credentials && data.role_arn) { + validateIamRole(data, ctx, false); + } + }); + +export const editSecurityHubIntegrationFormSchema = baseSecurityHubIntegrationSchema + .extend({ + provider_id: z.string().optional(), + send_only_fails: z.boolean().optional(), + archive_previous_findings: z.boolean().optional(), + use_custom_credentials: z.boolean().optional(), + credentials_type: z.enum(["aws-sdk-default", "access-secret-key"]).optional(), + }) + .superRefine((data, ctx) => { + if (data.use_custom_credentials !== false) { + validateAwsCredentialsEdit(data, ctx); + } + // Always validate role if role_arn is provided + validateIamRole(data, ctx, false); + }); \ No newline at end of file diff --git a/ui/types/lighthouse/index.ts b/ui/types/lighthouse/index.ts index 882cd2833c..38bcc6a1cd 100644 --- a/ui/types/lighthouse/index.ts +++ b/ui/types/lighthouse/index.ts @@ -1,6 +1,7 @@ export * from "./checks"; export * from "./compliances"; export * from "./findings"; +export * from "./model-params"; export * from "./overviews"; export * from "./providers"; export * from "./resources"; diff --git a/ui/types/lighthouse/model-params.ts b/ui/types/lighthouse/model-params.ts new file mode 100644 index 0000000000..455146751a --- /dev/null +++ b/ui/types/lighthouse/model-params.ts @@ -0,0 +1,5 @@ +export type ModelParams = { + maxTokens: number | undefined; + temperature: number | undefined; + reasoningEffort: string | undefined; +}; diff --git a/util/prowler-bulk-provisioning/README.md b/util/prowler-bulk-provisioning/README.md new file mode 100644 index 0000000000..3851979248 --- /dev/null +++ b/util/prowler-bulk-provisioning/README.md @@ -0,0 +1,422 @@ +# Prowler Provider Bulk Provisioning + +A Python script to bulk-provision cloud providers in Prowler Cloud/App via REST API. This tool streamlines the process of adding multiple cloud providers to Prowler by reading configuration from YAML and making API calls with concurrency and retry support. + +## Supported Providers + +- **AWS** (Amazon Web Services) +- **Azure** (Microsoft Azure) +- **GCP** (Google Cloud Platform) +- **Kubernetes** +- **M365** (Microsoft 365) +- **GitHub** + +## Features + +- **Concurrent Processing:** Configurable concurrency for faster bulk operations +- **Retry Logic:** Built-in retry mechanism for handling temporary API failures +- **Dry-Run Mode:** Test configuration without making actual API calls +- **Flexible Authentication:** Supports various authentication methods per provider +- **Error Handling:** Comprehensive error reporting and validation +- **Connection Testing:** Built-in provider connection verification + +## How It Works + +The script uses a two-step process to provision providers in Prowler: + +1. **Provider Creation:** Creates the provider with basic information (provider type, UID, alias) +2. **Secret Creation:** Creates and links authentication credentials as a separate secret resource + +This two-step approach follows the Prowler API design where providers and their credentials are managed as separate but linked resources, providing better security and flexibility. + +## Installation + +### Requirements + +- Python 3.7 or higher +- Required packages (install via requirements.txt) + +### Setup + +1. Clone or navigate to the Prowler repository: + ```bash + cd contrib/other-contrib/provider-bulk-importer + ``` + +2. Install dependencies: + ```bash + pip install -r requirements.txt + ``` + +3. Get your Prowler API token: + - **Prowler Cloud:** Generate token at https://api.prowler.com + - **Self-hosted Prowler App:** Generate token in your local instance + + ```bash + export PROWLER_API_TOKEN=$(curl --location 'https://api.prowler.com/api/v1/tokens' \ + --header 'Content-Type: application/vnd.api+json' \ + --header 'Accept: application/vnd.api+json' \ + --data-raw '{ + "data": { + "type": "tokens", + "attributes": { + "email": "your@email.com", + "password": "your-password" + } + } + }' | jq -r .data.attributes.access) + ``` + + +## Configuration + +### Environment Variables + +```bash +export PROWLER_API_TOKEN="your-prowler-token" +export PROWLER_API_BASE="https://api.prowler.com/api/v1" # Optional, defaults to Prowler Cloud +``` + +### Provider Configuration Files + +Create a configuration file (YAML recommended) listing the providers to add: + +#### YAML Format (Recommended) + +```yaml +# providers.yaml +- provider: aws + uid: "123456789012" # AWS Account ID + alias: "prod-root" + auth_method: role # role | credentials + credentials: + role_arn: "arn:aws:iam::123456789012:role/ProwlerScan" + external_id: "ext-abc123" # optional + session_name: "prowler-bulk" # optional + duration_seconds: 3600 # optional + +- provider: aws + uid: "210987654321" + alias: "dev" + auth_method: credentials # long/short-lived keys + credentials: + access_key_id: "AKIA..." + secret_access_key: "..." + session_token: "..." # optional + +- provider: azure + uid: "00000000-1111-2222-3333-444444444444" # Subscription ID + alias: "sub-eastus" + auth_method: service_principal + credentials: + tenant_id: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" + client_id: "ffffffff-1111-2222-3333-444444444444" + client_secret: "..." + +- provider: gcp + uid: "my-gcp-project-id" # Project ID + alias: "gcp-prod" + auth_method: service_account # Service Account authentication + credentials: + service_account_key_json_path: "./gcp-key.json" + +- provider: kubernetes + uid: "my-eks-context" # kubeconfig context name + alias: "eks-prod" + auth_method: kubeconfig + credentials: + kubeconfig_path: "~/.kube/config" + +- provider: m365 + uid: "contoso.onmicrosoft.com" # Domain ID + alias: "contoso" + auth_method: service_principal + credentials: + tenant_id: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" + client_id: "ffffffff-1111-2222-3333-444444444444" + client_secret: "..." + +- provider: github + uid: "my-org" # organization or username + alias: "gh-org" + auth_method: personal_access_token # oauth_app_token | github_app + credentials: + token: "ghp_..." +``` + +## Usage + +### Basic Usage + +```bash +python prowler_bulk_provisioning.py providers.yaml +``` + +### Advanced Usage + +```bash +python prowler_bulk_provisioning.py providers.yaml \ + --base-url https://api.prowler.com/api/v1 \ + --providers-endpoint /providers \ + --concurrency 6 \ + --timeout 120 +``` + +### Command Line Options + +| Option | Description | Default | +|--------|-------------|---------| +| `input_file` | YAML file with provider entries | Required | +| `--base-url` | API base URL | `https://api.prowler.com/api/v1` | +| `--token` | Bearer token | `PROWLER_API_TOKEN` env var | +| `--providers-endpoint` | Providers API endpoint | `/providers` | +| `--concurrency` | Number of concurrent requests | `5` | +| `--timeout` | Per-request timeout in seconds | `60` | +| `--insecure` | Disable TLS verification | `False` | +| `--dry-run` | Print payloads without sending | `False` | +| `--test-provider` | Test connection after creating each provider (true/false) | `true` (enabled by default) | +| `--test-provider-only` | Only test connections for existing providers (skip creation) | `False` | + +### Self-hosted Prowler App + +For self-hosted installations: + +```bash +python prowler_bulk_provisioning.py providers.yaml \ + --base-url http://localhost:8080/api/v1 +``` + +## Provider-Specific Configuration + +### AWS Authentication Methods + +#### IAM Role (Recommended) +```yaml +- provider: aws + uid: "123456789012" + alias: "prod" + auth_method: role + credentials: + role_arn: "arn:aws:iam::123456789012:role/ProwlerScan" + external_id: "optional-external-id" +``` + +#### Access Keys +```yaml +- provider: aws + uid: "123456789012" + alias: "dev" + auth_method: credentials + credentials: + access_key_id: "AKIA..." + secret_access_key: "..." + session_token: "..." # optional for temporary credentials +``` + +### Azure Authentication + +```yaml +- provider: azure + uid: "subscription-uuid" + alias: "azure-prod" + auth_method: service_principal + credentials: + tenant_id: "tenant-uuid" + client_id: "client-uuid" + client_secret: "client-secret" +``` + +### GCP Authentication + +The Prowler API supports the following authentication methods for GCP: + +#### Method 1: Service Account JSON (Recommended) +```yaml +- provider: gcp + uid: "project-id" + alias: "gcp-prod" + auth_method: service_account # or 'service_account_json' + credentials: + service_account_key_json_path: "/path/to/key.json" + # OR inline: + # inline_json: + # type: "service_account" + # project_id: "your-project" + # private_key_id: "key-id" + # private_key: "-----BEGIN PRIVATE KEY-----\n..." + # client_email: "service-account@project.iam.gserviceaccount.com" + # client_id: "1234567890" + # auth_uri: "https://accounts.google.com/o/oauth2/auth" + # token_uri: "https://oauth2.googleapis.com/token" + # auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs" + # client_x509_cert_url: "https://www.googleapis.com/robot/v1/metadata/x509/..." +``` + +#### Method 2: OAuth2 Credentials +```yaml +- provider: gcp + uid: "project-id" + alias: "gcp-prod" + auth_method: oauth2 # or 'adc' for Application Default Credentials + credentials: + client_id: "123456789012345678901.apps.googleusercontent.com" + client_secret: "GOCSPX-xxxxxxxxxxxxxxxxx" + refresh_token: "1//0exxxxxxxxxxxxxxxxx" +``` + +### Kubernetes Authentication + +```yaml +- provider: kubernetes + uid: "context-name" + alias: "k8s-prod" + auth_method: kubeconfig + credentials: + kubeconfig_path: "~/.kube/config" + # OR + # kubeconfig_inline: | + # apiVersion: v1 + # clusters: ... +``` + +### Microsoft 365 Authentication + +```yaml +- provider: m365 + uid: "domain.onmicrosoft.com" + alias: "m365-tenant" + auth_method: service_principal + credentials: + tenant_id: "tenant-uuid" + client_id: "client-uuid" + client_secret: "client-secret" +``` + +### GitHub Authentication + +#### Personal Access Token +```yaml +- provider: github + uid: "organization-name" + alias: "gh-org" + auth_method: personal_access_token + credentials: + token: "ghp_..." +``` + +#### GitHub App +```yaml +- provider: github + uid: "organization-name" + alias: "gh-org" + auth_method: github_app + credentials: + app_id: "123456" + private_key_path: "/path/to/private-key.pem" + # OR + # private_key_inline: "-----BEGIN RSA PRIVATE KEY-----\n..." +``` + +## Connection Testing + +The script includes built-in connection testing to verify that providers can successfully authenticate with their respective cloud services. + +By default, the script tests connections immediately after creating providers: + +```bash +python prowler_bulk_provisioning.py providers.yaml +``` + +This will: +1. Create the provider +2. Add credentials +3. Test the connection +4. Report connection status + +To skip connection testing, use: + +```bash +python prowler_bulk_provisioning.py providers.yaml --test-provider false +``` + +### Test Existing Providers + +Test connections for already existing providers without creating new ones: + +```bash +python prowler_bulk_provisioning.py providers.yaml --test-provider-only +``` + +This is useful for: +- Verifying existing provider configurations +- Debugging authentication issues +- Regular connection health checks +- Testing after credential updates + +### Example Output + +``` +[1] ✅ Created provider (id=db9a8985-f9ec-4dd8-b5a0-e05ab3880bed) +[1] ✅ Created secret (id=466f76c6-5878-4602-a4bc-13f9522c1fd2) +[1] ✅ Connection test: Connected + +[2] ✅ Created provider (id=7a99f789-0cf5-4329-8279-2d443a962676) +[2] ✅ Created secret (id=c5702180-f7c4-40fd-be0e-f6433479b126) +[2] ❌ Connection test: Not connected +``` + +## Advanced Features + +### Dry Run Mode + +Test your configuration without making API calls: + +```bash +python prowler_bulk_provisioning.py providers.yaml --dry-run +``` + +## Troubleshooting + +### Common Issues + +1. **Invalid API Token** + ``` + Error: 401 Unauthorized + Solution: Check your PROWLER_API_TOKEN or --token parameter + ``` + +2. **Network Timeouts** + ``` + Error: Request timeout + Solution: Increase --timeout value or check network connectivity + ``` + +3. **Invalid Provider Configuration** + ``` + Error: Each item must include 'provider' and 'uid' + Solution: Verify all required fields are present in your config file + ``` + +4. **File Not Found Errors** + ``` + Error: No such file or directory + Solution: Check file paths for credentials files (JSON keys, kubeconfig, etc.) + ``` + +## Examples + +See the `examples/` directory for sample configuration files: + +- `examples/simple-providers.yaml` - Basic example with minimal configuration + +## Support + +For issues and questions: + +1. Check the [Prowler documentation](https://docs.prowler.com) +2. Review the [API documentation](https://api.prowler.com/api/v1/docs) +3. Open an issue in the [Prowler repository](https://github.com/prowler-cloud/prowler) + +## License + +This tool is part of the Prowler project and follows the same licensing terms. diff --git a/util/prowler-bulk-provisioning/examples/simple-providers.yaml b/util/prowler-bulk-provisioning/examples/simple-providers.yaml new file mode 100644 index 0000000000..3a5b53e523 --- /dev/null +++ b/util/prowler-bulk-provisioning/examples/simple-providers.yaml @@ -0,0 +1,56 @@ +# Simple Prowler Provider Configuration Example +# +# This is a minimal example showing the basic required fields for each provider type. +# Use this as a starting point and refer to providers.yaml for complete examples. + +# AWS with IAM Role (Recommended) +- provider: aws + uid: "123456789012" + alias: "my-aws-account" + auth_method: role + credentials: + role_arn: "arn:aws:iam::123456789012:role/ProwlerScan" + +# Azure with Service Principal +- provider: azure + uid: "00000000-1111-2222-3333-444444444444" + alias: "my-azure-subscription" + auth_method: service_principal + credentials: + tenant_id: "tenant-id-here" + client_id: "client-id-here" + client_secret: "client-secret-here" + +# GCP with Service Account +- provider: gcp + uid: "my-gcp-project" + alias: "my-gcp-project" + auth_method: service_account_json + credentials: + service_account_key_json_path: "/path/to/service-account-key.json" + +# Kubernetes with kubeconfig +- provider: kubernetes + uid: "my-cluster-context" + alias: "my-k8s-cluster" + auth_method: kubeconfig + credentials: + kubeconfig_path: "~/.kube/config" + +# Microsoft 365 with Service Principal +- provider: m365 + uid: "company.onmicrosoft.com" + alias: "my-m365-tenant" + auth_method: service_principal + credentials: + tenant_id: "tenant-id-here" + client_id: "client-id-here" + client_secret: "client-secret-here" + +# GitHub with Personal Access Token +- provider: github + uid: "my-organization" + alias: "my-github-org" + auth_method: personal_access_token + credentials: + token: "ghp_token_here" diff --git a/util/prowler-bulk-provisioning/nuke_providers.py b/util/prowler-bulk-provisioning/nuke_providers.py new file mode 100755 index 0000000000..348855611f --- /dev/null +++ b/util/prowler-bulk-provisioning/nuke_providers.py @@ -0,0 +1,449 @@ +#!/usr/bin/env python3 +""" +Delete ALL providers from Prowler Cloud/App via REST API. + +⚠️ WARNING: This script will DELETE ALL PROVIDERS in your Prowler account! +Use with extreme caution. There is no undo. + +Environment: + PROWLER_API_BASE (default: https://api.prowler.com/api/v1) + PROWLER_API_TOKEN (required unless --token is provided) + +Usage: + python nuke_providers.py --confirm + python nuke_providers.py --confirm --filter-provider aws + python nuke_providers.py --confirm --filter-alias "prod-*" + +Safety features: + * Requires explicit --confirm flag + * Shows preview of what will be deleted + * Optional filters to limit scope + * Dry-run mode available + +Author: Prowler Contributors ✨ +""" + +from __future__ import annotations + +import argparse +import fnmatch +import json +import os +import sys +from concurrent.futures import ThreadPoolExecutor, as_completed +from dataclasses import dataclass +from typing import Any, Dict, List, Optional, Tuple + +import requests + +# ----------------------------- CLI / Utils --------------------------------- # + + +def env_or_arg(token_arg: Optional[str]) -> str: + """Get API token from argument or environment variable.""" + token = token_arg or os.getenv("PROWLER_API_TOKEN") + if not token: + sys.exit("Missing API token. Set --token or PROWLER_API_TOKEN.") + return token + + +def normalize_base_url(url: str) -> str: + """Normalize base URL format.""" + url = url.rstrip("/") + if not url.lower().startswith(("http://", "https://")): + url = "https://" + url + return url + + +# ----------------------------- HTTP client --------------------------------- # + + +@dataclass +class ApiClient: + """HTTP client for Prowler API.""" + + base_url: str + token: str + verify_ssl: bool = True + timeout: int = 60 + + def _headers(self) -> Dict[str, str]: + """Generate HTTP headers for API requests.""" + return { + "Authorization": f"Bearer {self.token}", + "Content-Type": "application/vnd.api+json", + "Accept": "application/vnd.api+json", + } + + def get(self, path: str) -> requests.Response: + """Make GET request to API endpoint.""" + url = f"{self.base_url}{path}" + return requests.get( + url, + headers=self._headers(), + timeout=self.timeout, + verify=self.verify_ssl, + ) + + def delete(self, path: str) -> requests.Response: + """Make DELETE request to API endpoint.""" + url = f"{self.base_url}{path}" + return requests.delete( + url, + headers=self._headers(), + timeout=self.timeout, + verify=self.verify_ssl, + ) + + +def fetch_all_providers(client: ApiClient) -> List[Dict[str, Any]]: + """Fetch all providers from the API with pagination.""" + all_providers = [] + page = 1 + per_page = 100 # Max allowed by API + + while True: + try: + # API uses page[number] and page[size] parameters + resp = client.get(f"/providers?page[number]={page}&page[size]={per_page}") + + if resp.status_code != 200: + print(f"Error fetching providers (page {page}): {resp.status_code}") + print(f"Response: {resp.text}") + break + + data = resp.json() + providers = data.get("data", []) + + if not providers: + break + + all_providers.extend(providers) + + # Check if there's a next page + links = data.get("links", {}) + if not links.get("next"): + break + + page += 1 + + except Exception as e: + print(f"Error fetching providers: {e}") + break + + return all_providers + + +def apply_filters( + providers: List[Dict[str, Any]], + filter_provider: Optional[str] = None, + filter_alias: Optional[str] = None, + filter_uid: Optional[str] = None, +) -> List[Dict[str, Any]]: + """Apply filters to provider list.""" + filtered = providers + + if filter_provider: + filtered = [ + p + for p in filtered + if p.get("attributes", {}).get("provider") == filter_provider.lower() + ] + + if filter_alias: + filtered = [ + p + for p in filtered + if fnmatch.fnmatch(p.get("attributes", {}).get("alias", ""), filter_alias) + ] + + if filter_uid: + filtered = [ + p + for p in filtered + if fnmatch.fnmatch(p.get("attributes", {}).get("uid", ""), filter_uid) + ] + + return filtered + + +def delete_provider(client: ApiClient, provider_id: str) -> Tuple[bool, Dict[str, Any]]: + """Delete a single provider.""" + try: + resp = client.delete(f"/providers/{provider_id}") + + if resp.status_code in [200, 202, 204]: + # 202 means accepted for async processing (which is what Prowler returns) + # Check if it's a task response + try: + data = resp.json() + if data.get("data", {}).get("type") == "tasks": + task_state = data.get("data", {}).get("attributes", {}).get("state") + # If it's a deletion task that's available or completed, consider it success + if task_state in ["available", "completed"]: + return True, { + "status": "deleted (async)", + "id": provider_id, + "task": data, + } + except (json.JSONDecodeError, ValueError, KeyError): + pass + + return True, {"status": "deleted", "id": provider_id} + else: + try: + data = resp.json() + except ValueError: + data = {"text": resp.text} + return False, {"status": resp.status_code, "body": data} + + except Exception as e: + return False, {"error": str(e)} + + +def print_provider_summary(providers: List[Dict[str, Any]]) -> None: + """Print a summary of providers to be deleted.""" + if not providers: + print("No providers found matching the criteria.") + return + + # Group by provider type + by_type: Dict[str, List[Dict[str, Any]]] = {} + for p in providers: + provider_type = p.get("attributes", {}).get("provider", "unknown") + if provider_type not in by_type: + by_type[provider_type] = [] + by_type[provider_type].append(p) + + print(f"\n{'=' * 60}") + print(f"PROVIDERS TO BE DELETED: {len(providers)} total") + print(f"{'=' * 60}") + + for provider_type, items in sorted(by_type.items()): + print(f"\n{provider_type.upper()}: {len(items)} providers") + print("-" * 40) + + # Show first 5 and last 2 if more than 7 + if len(items) > 7: + for p in items[:5]: + attrs = p.get("attributes", {}) + print( + f" • {attrs.get('alias', 'N/A'):30} (UID: {attrs.get('uid', 'N/A')})" + ) + print(f" ... and {len(items) - 7} more ...") + for p in items[-2:]: + attrs = p.get("attributes", {}) + print( + f" • {attrs.get('alias', 'N/A'):30} (UID: {attrs.get('uid', 'N/A')})" + ) + else: + for p in items: + attrs = p.get("attributes", {}) + print( + f" • {attrs.get('alias', 'N/A'):30} (UID: {attrs.get('uid', 'N/A')})" + ) + + print(f"\n{'=' * 60}\n") + + +# ----------------------------- Main ---------------------------------------- # + + +def main(): + """Main function to delete providers.""" + parser = argparse.ArgumentParser( + description="⚠️ DELETE ALL providers from Prowler (use with caution!)" + ) + parser.add_argument( + "--confirm", + action="store_true", + required=True, + help="Required confirmation flag to proceed with deletion", + ) + parser.add_argument( + "--base-url", + default=os.getenv("PROWLER_API_BASE", "https://api.prowler.com/api/v1"), + help="API base URL (default: env PROWLER_API_BASE or Prowler Cloud SaaS)", + ) + parser.add_argument( + "--token", default=None, help="Bearer token (default: PROWLER_API_TOKEN)" + ) + parser.add_argument( + "--filter-provider", + help="Only delete specific provider type (aws, azure, gcp, kubernetes, github, m365)", + ) + parser.add_argument( + "--filter-alias", + help="Only delete providers matching alias pattern (supports wildcards: prod-*)", + ) + parser.add_argument( + "--filter-uid", + help="Only delete providers matching UID pattern (supports wildcards: 100000*)", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Show what would be deleted without actually deleting", + ) + parser.add_argument( + "--concurrency", + type=int, + default=5, + help="Number of concurrent deletion requests", + ) + parser.add_argument( + "--timeout", type=int, default=60, help="Per-request timeout (seconds)" + ) + parser.add_argument( + "--insecure", + action="store_true", + help="Disable TLS verification (not recommended)", + ) + parser.add_argument( + "--yes", + action="store_true", + help="Skip interactive confirmation prompt", + ) + + args = parser.parse_args() + + token = env_or_arg(args.token) + base_url = normalize_base_url(args.base_url) + + client = ApiClient( + base_url=base_url, + token=token, + verify_ssl=not args.insecure, + timeout=args.timeout, + ) + + # Fetch all providers + print("Fetching providers from Prowler...") + all_providers = fetch_all_providers(client) + + if not all_providers: + print("No providers found in your account.") + return + + print(f"Found {len(all_providers)} total providers in your account.") + + # Apply filters + providers_to_delete = apply_filters( + all_providers, + filter_provider=args.filter_provider, + filter_alias=args.filter_alias, + filter_uid=args.filter_uid, + ) + + if not providers_to_delete: + print("No providers match the specified filters.") + return + + # Show what will be deleted + print_provider_summary(providers_to_delete) + + if args.dry_run: + print("DRY RUN MODE - No providers will be deleted.") + print(f"Would delete {len(providers_to_delete)} providers.") + return + + # Final confirmation + if not args.yes: + print("⚠️ WARNING: This action cannot be undone!") + print(f"⚠️ You are about to DELETE {len(providers_to_delete)} providers!") + print() + response = input("Type 'DELETE ALL' to confirm: ") + if response != "DELETE ALL": + print("Cancelled. No providers were deleted.") + return + + # Perform deletion + print(f"\nDeleting {len(providers_to_delete)} providers...") + + successes = 0 + failures = 0 + results: List[Tuple[str, bool, Dict[str, Any]]] = [] + + with ThreadPoolExecutor(max_workers=max(1, args.concurrency)) as executor: + futures = { + executor.submit(delete_provider, client, p.get("id")): ( + p.get("id"), + p.get("attributes", {}).get("alias", "unknown"), + p.get("attributes", {}).get("provider", "unknown"), + ) + for p in providers_to_delete + } + + for fut in as_completed(futures): + provider_id, alias, provider_type = futures[fut] + try: + ok, data = fut.result() + results.append((provider_id, ok, data)) + + if ok: + successes += 1 + # Check if it was an async deletion + if data.get("status") == "deleted (async)": + print( + f"✅ Deleting: {alias} ({provider_type}/{provider_id}) - queued" + ) + else: + print(f"✅ Deleted: {alias} ({provider_type}/{provider_id})") + else: + failures += 1 + print(f"❌ Failed: {alias} ({provider_type}/{provider_id})") + if "body" in data: + # Sanitize error data to avoid printing sensitive information + error_body = data["body"] + # Simple sanitization - just show error messages without full details + if isinstance(error_body, dict) and "errors" in error_body: + print( + f" Error: {error_body.get('errors', 'Unknown error')}" + ) + else: + print(" Error: API request failed") + + except Exception as e: + failures += 1 + print(f"❌ Exception deleting {alias}: {e}") + + # Summary with nuclear explosion art if successful + if successes > 0 and failures == 0: + # Nuclear explosion ASCII art + print( + r""" + _.-^^---....,,-- + _-- --_ + < >) + | | + \._ _./ + ```--. . , ; .--''' + | | | + .-=|| | |=-. + `-=#$%&%$#=-' + | ; :| + _____.,-#%&$@%#&#~,._____ + """ + ) + print(f"\n{'=' * 60}") + print("💥 NUCLEAR DELETION COMPLETE 💥") + print(f"{'=' * 60}") + print(f"✅ Successfully deleted: {successes} providers") + print("☢️ All targets eliminated!") + else: + print(f"\n{'=' * 60}") + print("DELETION COMPLETE") + print(f"{'=' * 60}") + print(f"✅ Successfully deleted: {successes} providers") + if failures > 0: + print(f"❌ Failed to delete: {failures} providers") + + print(f"{'=' * 60}\n") + + # Exit with error code if any failures + if failures > 0: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/util/prowler-bulk-provisioning/prowler_bulk_provisioning.py b/util/prowler-bulk-provisioning/prowler_bulk_provisioning.py new file mode 100755 index 0000000000..e7ebdaac3b --- /dev/null +++ b/util/prowler-bulk-provisioning/prowler_bulk_provisioning.py @@ -0,0 +1,811 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import csv +import json +import os +import sys +import time +from concurrent.futures import ThreadPoolExecutor, as_completed +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple + +import requests + +# ----------------------------- CLI / I/O utils ----------------------------- # + + +def sanitize_sensitive_data(data: Any, depth: int = 0) -> Any: + """ + Recursively sanitize sensitive data in dictionaries and lists. + Replaces sensitive field values with masked versions. + """ + if depth > 10: # Prevent infinite recursion + return data + + # List of sensitive field names to mask + sensitive_fields = { + "password", + "secret", + "token", + "key", + "credentials", + "client_secret", + "refresh_token", + "access_key_id", + "secret_access_key", + "session_token", + "private_key", + "api_key", + "apikey", + "auth", + "authorization", + "private_key_id", + "client_id", + "tenant_id", + "service_account_key", + "kubeconfig", + "role_arn", + "external_id", + } + + if isinstance(data, dict): + sanitized = {} + for key, value in data.items(): + # Check if the key name suggests sensitive data + key_lower = key.lower() + if any(sensitive in key_lower for sensitive in sensitive_fields): + if isinstance(value, str) and value: + # Mask the value but show first few chars for debugging + if len(value) > 8: + sanitized[key] = ( + f"{value[:4]}...{value[-2:]}" if len(value) > 6 else "***" + ) + else: + sanitized[key] = "***" + elif isinstance(value, (dict, list)): + # Still recurse into nested structures + sanitized[key] = sanitize_sensitive_data(value, depth + 1) + else: + sanitized[key] = "***" if value else value + else: + # Recurse into non-sensitive fields + if isinstance(value, (dict, list)): + sanitized[key] = sanitize_sensitive_data(value, depth + 1) + else: + sanitized[key] = value + return sanitized + elif isinstance(data, list): + return [sanitize_sensitive_data(item, depth + 1) for item in data] + else: + return data + + +def load_items(path: Path) -> List[Dict[str, Any]]: + """Load provider items from YAML, JSON, or CSV file.""" + ext = path.suffix.lower() + if ext in (".yaml", ".yml"): + try: + import yaml # type: ignore + except Exception: + sys.exit("PyYAML is required for YAML inputs. pip install pyyaml") + with path.open("r", encoding="utf-8") as f: + data = yaml.safe_load(f) + if isinstance(data, dict): + # allow single object with "items" key + data = data.get("items") or [] + if not isinstance(data, list): + sys.exit("YAML root must be a list (or dict with 'items').") + return data + + if ext == ".json": + with path.open("r", encoding="utf-8") as f: + data = json.load(f) + if isinstance(data, dict): + data = data.get("items") or [] + if not isinstance(data, list): + sys.exit("JSON root must be a list (or dict with 'items').") + return data + + if ext == ".csv": + items: List[Dict[str, Any]] = [] + with path.open("r", encoding="utf-8") as f: + reader = csv.DictReader(f) + for row in reader: + # Support a 'credentials' column containing JSON + creds = row.get("credentials") + if creds: + try: + row["credentials"] = json.loads(creds) + except Exception: + sys.exit("Invalid JSON in 'credentials' column") + # Normalize empty strings to None + for k, v in list(row.items()): + if isinstance(v, str) and v.strip() == "": + row[k] = None + items.append(row) + return items + + sys.exit(f"Unsupported input file type: {ext}") + + +def env_or_arg(token_arg: Optional[str]) -> str: + """Get API token from argument or environment variable.""" + token = token_arg or os.getenv("PROWLER_API_TOKEN") + if not token: + sys.exit("Missing API token. Set --token or PROWLER_API_TOKEN.") + return token + + +def normalize_base_url(url: str) -> str: + """Normalize base URL format.""" + url = url.rstrip("/") + if not url.lower().startswith(("http://", "https://")): + url = "https://" + url + return url + + +# ----------------------------- Payload builders ---------------------------- # + + +def read_text_file(path: Optional[str]) -> Optional[str]: + """Read text content from file path.""" + if not path: + return None + p = Path(os.path.expanduser(path)) + return p.read_text(encoding="utf-8") + + +def build_payload( + item: Dict[str, Any], +) -> Tuple[str, Dict[str, Any], Optional[Dict[str, Any]]]: + """ + Returns (endpoint_path, provider_payload, secret_payload) to POST. + + The API requires two steps: + 1. Create provider with minimal info + 2. Create secret linked to the provider + """ + provider = str(item.get("provider", "")).strip().lower() + uid = item.get("uid") # account id / subscription id / project id / etc. + alias = item.get("alias") + auth_method = str(item.get("auth_method", "")).strip().lower() + creds: Dict[str, Any] = item.get("credentials") or {} + + if not provider or not uid: + raise ValueError("Each item must include 'provider' and 'uid'.") + + # Step 1: Build provider creation payload (minimal) + provider_payload: Dict[str, Any] = { + "data": { + "type": "providers", + "attributes": { + "provider": provider, + "uid": uid, + "alias": alias, + }, + } + } + + # Step 2: Build secret creation payload if credentials are provided + secret_payload: Optional[Dict[str, Any]] = None + + if auth_method and creds: + # Determine secret_type based on auth_method and provider + if auth_method == "role": + secret_type = "role" + elif provider == "gcp" and auth_method in [ + "service_account", + "service_account_json", + ]: + secret_type = "service_account" + else: + secret_type = "static" + secret_data: Dict[str, Any] = {} + + if provider == "aws": + if auth_method == "role": + external_id = creds.get("external_id") + if not external_id: + raise ValueError( + "AWS role authentication requires 'external_id' in credentials" + ) + secret_data = { + "role_arn": creds.get("role_arn"), + "external_id": external_id, + } + # Optional fields for role + if creds.get("session_name"): + secret_data["role_session_name"] = creds.get("session_name") + if creds.get("duration_seconds"): + secret_data["session_duration"] = creds.get("duration_seconds") + if creds.get("access_key_id"): + secret_data["aws_access_key_id"] = creds.get("access_key_id") + if creds.get("secret_access_key"): + secret_data["aws_secret_access_key"] = creds.get( + "secret_access_key" + ) + if creds.get("session_token"): + secret_data["aws_session_token"] = creds.get("session_token") + elif auth_method == "credentials": + secret_type = "static" + secret_data = { + "aws_access_key_id": creds.get("access_key_id"), + "aws_secret_access_key": creds.get("secret_access_key"), + } + if creds.get("session_token"): + secret_data["aws_session_token"] = creds.get("session_token") + else: + raise ValueError("AWS 'auth_method' must be 'role' or 'credentials'.") + + elif provider == "azure": + if auth_method != "service_principal": + raise ValueError("Azure 'auth_method' must be 'service_principal'.") + secret_data = { + "tenant_id": creds.get("tenant_id"), + "client_id": creds.get("client_id"), + "client_secret": creds.get("client_secret"), + } + + elif provider == "gcp": + # GCP supports 3 authentication methods + if ( + auth_method == "service_account" + or auth_method == "service_account_json" + ): + # Method 1: Service Account JSON key + inline = creds.get("inline_json") + path = creds.get("service_account_key_json_path") + + # Load the service account JSON + sa_data = None + if path and not inline: + inline_content = read_text_file(path) + if inline_content: + try: + import json + + sa_data = json.loads(inline_content) + except (json.JSONDecodeError, ValueError): + # If parsing fails, try sending as string + sa_data = {"private_key": inline_content} + elif inline: + if isinstance(inline, dict): + sa_data = inline + else: + try: + import json + + sa_data = json.loads(inline) + except (json.JSONDecodeError, ValueError): + sa_data = {"private_key": inline} + + # The API expects the service account JSON wrapped in a service_account_key field + if sa_data and isinstance(sa_data, dict): + # Wrap the service account JSON in the service_account_key field + secret_data = {"service_account_key": sa_data} + else: + raise ValueError("Could not parse service account JSON") + + elif auth_method == "oauth2" or auth_method == "adc": + # Method 2: OAuth2 credentials (Application Default Credentials) + secret_data = { + "client_id": creds.get("client_id"), + "client_secret": creds.get("client_secret"), + "refresh_token": creds.get("refresh_token"), + } + elif ( + auth_method == "workload_identity" + or auth_method == "workload_identity_federation" + ): + # Method 3: Workload Identity Federation + secret_data = { + "type": creds.get("type", "external_account"), + "audience": creds.get("audience"), + "subject_token_type": creds.get("subject_token_type"), + "service_account_impersonation_url": creds.get( + "service_account_impersonation_url" + ), + "token_url": creds.get("token_url"), + "credential_source": creds.get("credential_source"), + } + else: + raise ValueError( + "GCP 'auth_method' must be 'service_account', 'oauth2', or 'workload_identity'." + ) + + elif provider == "kubernetes": + if auth_method != "kubeconfig": + raise ValueError("Kubernetes 'auth_method' must be 'kubeconfig'.") + inline = creds.get("kubeconfig_inline") + path = creds.get("kubeconfig_path") + if path and not inline: + inline = read_text_file(path) + secret_data = {"kubeconfig_content": inline} + + elif provider == "m365": + # M365 is not in the API schema, might need special handling + if auth_method != "service_principal": + raise ValueError("M365 'auth_method' must be 'service_principal'.") + secret_data = { + "tenant_id": creds.get("tenant_id"), + "client_id": creds.get("client_id"), + "client_secret": creds.get("client_secret"), + } + # User/password might be additional fields + if creds.get("username"): + secret_data["user"] = creds.get("username") + if creds.get("password"): + secret_data["password"] = creds.get("password") + + elif provider == "github": + if auth_method == "personal_access_token": + secret_data = {"personal_access_token": creds.get("token")} + elif auth_method == "oauth_app_token": + secret_data = {"oauth_app_token": creds.get("oauth_token")} + elif auth_method == "github_app": + # Accept inline PK or path + pk = creds.get("private_key_inline") + if not pk and creds.get("private_key_path"): + pk = read_text_file(creds.get("private_key_path")) + secret_data = { + "github_app_id": int(creds.get("app_id", 0)), + "github_app_key": pk, + } + else: + raise ValueError( + "GitHub 'auth_method' must be personal_access_token | oauth_app_token | github_app." + ) + + else: + raise ValueError(f"Unsupported provider: {provider}") + + # Build secret payload + secret_payload = { + "data": { + "type": "provider-secrets", + "attributes": { + "secret_type": secret_type, + "secret": secret_data, + "name": alias, # Use alias as the secret name + }, + "relationships": { + "provider": { + "data": { + "type": "providers", + "id": None, # Will be filled after provider creation + } + } + }, + } + } + + # Return both payloads + return "/providers", provider_payload, secret_payload + + +# ----------------------------- HTTP client --------------------------------- # + + +@dataclass +class ApiClient: + """HTTP client for Prowler API.""" + + base_url: str + token: str + verify_ssl: bool = True + timeout: int = 60 + + def _headers(self) -> Dict[str, str]: + """Generate HTTP headers for API requests.""" + return { + "Authorization": f"Bearer {self.token}", + "Content-Type": "application/vnd.api+json", + "Accept": "application/vnd.api+json", + } + + def post(self, path: str, json_body: Dict[str, Any]) -> requests.Response: + """Make POST request to API endpoint.""" + url = f"{self.base_url}{path}" + return requests.post( + url, + headers=self._headers(), + json=json_body, + timeout=self.timeout, + verify=self.verify_ssl, + ) + + def get(self, path: str) -> requests.Response: + """Make GET request to API endpoint.""" + url = f"{self.base_url}{path}" + return requests.get( + url, + headers=self._headers(), + timeout=self.timeout, + verify=self.verify_ssl, + ) + + +def with_retries( + func, *, retries=4, base_delay=1.25, exceptions=(requests.RequestException,) +): + """Decorator to add retry logic to HTTP requests.""" + + def wrapper(*args, **kwargs): + for attempt in range(retries + 1): + try: + return func(*args, **kwargs) + except exceptions: + if attempt >= retries: + raise + sleep = base_delay * (2**attempt) + time.sleep(sleep) + # Shouldn't reach here + return func(*args, **kwargs) + + return wrapper + + +@with_retries +def create_one( + client: ApiClient, + provider_endpoint: str, + provider_payload: Dict[str, Any], + secret_payload: Optional[Dict[str, Any]] = None, + test_provider: bool = False, +) -> Tuple[bool, Dict[str, Any]]: + """Create a single provider with optional secret using two-step process.""" + # Step 1: Create provider + resp = client.post(provider_endpoint, provider_payload) + try: + data = resp.json() + except ValueError: + data = {"text": resp.text} + + if not (200 <= resp.status_code < 300): + return False, {"status": resp.status_code, "body": data, "step": "provider"} + + provider_id = data.get("data", {}).get("id") + if not provider_id: + return False, {"error": "No provider ID returned", "body": data} + + result = {"provider": data} + + # Step 2: Create secret if provided + if secret_payload: + # Update the provider ID in the secret payload + secret_payload["data"]["relationships"]["provider"]["data"]["id"] = provider_id + + # POST to /providers/secrets endpoint + secret_resp = client.post("/providers/secrets", secret_payload) + try: + secret_data = secret_resp.json() + except ValueError: + secret_data = {"text": secret_resp.text} + + if not (200 <= secret_resp.status_code < 300): + # Provider was created but secret failed + result["secret_error"] = { + "status": secret_resp.status_code, + "body": secret_data, + } + return False, result + + result["secret"] = secret_data + + # Step 3: Test connection if requested + if test_provider: + connection_result = test_provider_connection(client, provider_id) + result["connection_test"] = connection_result + + return True, result + + +def test_provider_connection(client: ApiClient, provider_id: str) -> Dict[str, Any]: + """Test connection for a provider.""" + try: + # Trigger connection test + resp = client.post(f"/providers/{provider_id}/connection", {}) + if resp.status_code in [200, 202]: + # Wait a bit for the connection test to complete + time.sleep(2) + + # Check the connection status + status_resp = client.get(f"/providers/{provider_id}") + if status_resp.status_code == 200: + provider_data = status_resp.json() + connection = ( + provider_data.get("data", {}) + .get("attributes", {}) + .get("connection", {}) + ) + return { + "success": True, + "connected": connection.get("connected"), + "last_checked_at": connection.get("last_checked_at"), + } + + return { + "success": False, + "error": f"Connection test failed with status {resp.status_code}", + } + except Exception as e: + return {"success": False, "error": str(e)} + + +def find_existing_provider(client: ApiClient, provider: str, uid: str) -> Optional[str]: + """Find an existing provider by provider type and UID.""" + try: + # Query for the specific provider + resp = client.get(f"/providers?filter[provider]={provider}&filter[uid]={uid}") + if resp.status_code == 200: + data = resp.json() + providers = data.get("data", []) + if providers: + return providers[0].get("id") + except Exception: + pass + return None + + +# ----------------------------- main ---------------------------------------- # + + +def main(): + """Main function to process bulk provider provisioning.""" + parser = argparse.ArgumentParser(description="Bulk provision providers in Prowler.") + parser.add_argument("input_file", help="YAML/JSON/CSV file with provider entries.") + parser.add_argument( + "--base-url", + default=os.getenv("PROWLER_API_BASE", "https://api.prowler.com/api/v1"), + help="API base URL (default: env PROWLER_API_BASE or Prowler Cloud SaaS).", + ) + parser.add_argument( + "--token", default=None, help="Bearer token (default: PROWLER_API_TOKEN)." + ) + parser.add_argument( + "--providers-endpoint", + default="/providers", + help="Path to the providers create endpoint (default: /providers).", + ) + parser.add_argument( + "--concurrency", type=int, default=5, help="Number of concurrent requests." + ) + parser.add_argument( + "--timeout", type=int, default=60, help="Per-request timeout (seconds)." + ) + parser.add_argument( + "--insecure", + action="store_true", + help="Disable TLS verification (not recommended).", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Print what would be sent without calling the API.", + ) + parser.add_argument( + "--test-provider", + type=lambda x: x.lower() in ["true", "1", "yes"], + default=True, + help="Test provider connection after creating each provider (default: true). Use --test-provider false to disable.", + ) + parser.add_argument( + "--test-provider-only", + action="store_true", + help="Only test connections for existing providers (skip creation).", + ) + args = parser.parse_args() + + token = env_or_arg(args.token) + base_url = normalize_base_url(args.base_url) + + items = load_items(Path(args.input_file)) + if not items: + print("No items found in input file.") + return + + client = ApiClient( + base_url=base_url, + token=token, + verify_ssl=not args.insecure, + timeout=args.timeout, + ) + + # Handle test-only mode + if args.test_provider_only: + print( + "Running in test-only mode: checking connections for existing providers..." + ) + tested, connected, failed = 0, 0, 0 + + for idx, item in enumerate(items, start=1): + provider = str(item.get("provider", "")).strip().lower() + uid = item.get("uid") + alias = item.get("alias", "") + + if not provider or not uid: + print(f"[{idx}] ❌ Skipping: missing provider or uid") + continue + + # Find existing provider + provider_id = find_existing_provider(client, provider, uid) + if not provider_id: + print(f"[{idx}] ⚠️ Provider not found: {provider}/{uid} ({alias})") + continue + + print(f"[{idx}] Testing connection for {provider}/{uid} ({alias})...") + result = test_provider_connection(client, provider_id) + tested += 1 + + if result.get("success"): + if result.get("connected"): + connected += 1 + print(f"[{idx}] ✅ Connected successfully") + else: + failed += 1 + print(f"[{idx}] ❌ Connection failed") + else: + failed += 1 + # Sanitize error message to avoid potential sensitive data + error = str(result.get("error", "Unknown error")) + if any( + word in error.lower() + for word in [ + "key", + "secret", + "token", + "password", + "credential", + "bearer", + ] + ): + print(f"[{idx}] ❌ Test failed: Authentication error") + else: + print(f"[{idx}] ❌ Test failed: {error}") + + print( + f"\nConnection Test Results: Tested: {tested}, Connected: {connected}, Failed: {failed}" + ) + return + + # Regular mode: create providers + requests_to_send: List[ + Tuple[int, str, Dict[str, Any], Optional[Dict[str, Any]]] + ] = [] + for idx, item in enumerate(items, start=1): + try: + endpoint, provider_payload, secret_payload = build_payload(item) + except Exception as e: + # Sanitize exception message to avoid leaking sensitive data + error_msg = str(e) + if any( + word in error_msg.lower() + for word in ["key", "secret", "token", "password", "credential"] + ): + print( + f"[{idx}] ❌ Skipping item due to build error: Invalid credentials format" + ) + else: + print(f"[{idx}] ❌ Skipping item due to build error: {error_msg}") + continue + + # Allow overriding endpoint path globally (for standard creation) + if endpoint == "/providers": + endpoint = args.providers_endpoint + + if args.dry_run: + print(f"[{idx}] DRY-RUN → Provider Creation") + print(f" POST {base_url}{endpoint}") + # Sanitize provider payload (usually safe but might contain some sensitive data) + sanitized_provider = sanitize_sensitive_data(provider_payload) + print(f" {json.dumps(sanitized_provider, indent=2)}") + if secret_payload: + print("\n Then Secret Creation:") + print(f" POST {base_url}/providers/secrets") + # Always sanitize secret payload as it contains credentials + sanitized_secret = sanitize_sensitive_data(secret_payload) + print(f" {json.dumps(sanitized_secret, indent=2)}") + if args.test_provider: + print("\n Then Test Connection") + print() + else: + requests_to_send.append((idx, endpoint, provider_payload, secret_payload)) + + if args.dry_run or not requests_to_send: + if not requests_to_send: + print("Nothing to send.") + return + + successes, failures = 0, 0 + results: List[Tuple[int, bool, Dict[str, Any]]] = [] + + with ThreadPoolExecutor(max_workers=max(1, args.concurrency)) as executor: + futures = { + executor.submit( + create_one, + client, + endpoint, + provider_payload, + secret_payload, + args.test_provider, + ): idx + for (idx, endpoint, provider_payload, secret_payload) in requests_to_send + } + for fut in as_completed(futures): + idx = futures[fut] + try: + ok, data = fut.result() + results.append((idx, ok, data)) + if ok: + successes += 1 + provider_id = data.get("provider", {}).get("data", {}).get("id") + print(f"[{idx}] ✅ Created provider (id={provider_id})") + if "secret" in data: + secret_id = data.get("secret", {}).get("data", {}).get("id") + print(f"[{idx}] ✅ Created secret (id={secret_id})") + if "connection_test" in data: + conn = data["connection_test"] + if conn.get("success") and conn.get("connected"): + print(f"[{idx}] ✅ Connection test: Connected") + elif conn.get("success"): + print(f"[{idx}] ⚠️ Connection test: Not connected") + else: + # Sanitize error message to avoid potential sensitive data + error = str(conn.get("error", "Unknown error")) + if any( + word in error.lower() + for word in [ + "key", + "secret", + "token", + "password", + "credential", + "bearer", + ] + ): + print( + f"[{idx}] ❌ Connection test failed: Authentication error" + ) + else: + print(f"[{idx}] ❌ Connection test failed: {error}") + else: + failures += 1 + if "secret_error" in data: + print(f"[{idx}] ⚠️ Provider created but secret failed:") + # Sanitize error data which might contain sensitive information + sanitized_error = sanitize_sensitive_data(data["secret_error"]) + print(f" {json.dumps(sanitized_error, indent=2)}") + else: + # Sanitize general error data + sanitized_data = sanitize_sensitive_data(data) + print( + f"[{idx}] ❌ API error: {json.dumps(sanitized_data, indent=2)}" + ) + except Exception as e: + failures += 1 + # Sanitize exception message to avoid leaking sensitive data + error_msg = str(e) + if any( + word in error_msg.lower() + for word in [ + "key", + "secret", + "token", + "password", + "credential", + "bearer", + ] + ): + print(f"[{idx}] ❌ Request failed: Authentication or network error") + else: + print(f"[{idx}] ❌ Request failed: {error_msg}") + + print(f"\nDone. Success: {successes} Failures: {failures}") + + +if __name__ == "__main__": + main() diff --git a/util/prowler-bulk-provisioning/requirements.txt b/util/prowler-bulk-provisioning/requirements.txt new file mode 100644 index 0000000000..772e1a1565 --- /dev/null +++ b/util/prowler-bulk-provisioning/requirements.txt @@ -0,0 +1,10 @@ +# Prowler Provider Bulk Importer Dependencies +# +# Core HTTP library for API requests +requests>=2.28.0 + +# YAML parsing support (optional, only needed for YAML input files) +PyYAML>=6.0 + +# Type hints support for older Python versions (optional) +typing-extensions>=4.0.0; python_version < '3.8'