From f80fe1e0c5212931cde80aea1b3a8c42ff627ae3 Mon Sep 17 00:00:00 2001 From: msankows Date: Thu, 14 Aug 2025 12:28:08 +0200 Subject: [PATCH 01/29] Broken code --- .gitignore | 179 +++ .pre-commit-config.yaml | 82 - .pre-commit-config.yml | 11 + CONTRIBUTING.md | 3 - LICENSE | 201 --- NOTICE | 32 - README.md | 398 ----- __init__.py | 14 - http.json | 1427 ----------------- http_connector.py | 927 ----------- http_consts.py | 46 - logo.svg | 7 + logo_dark.svg | 7 + logo_splunk.svg | 1 - logo_splunk_dark.svg | 1 - manual_readme_content.md | 10 - pyproject.toml | 115 +- release_notes/2.0.10.md | 6 - release_notes/2.0.9.md | 6 - release_notes/2.1.10.md | 6 - release_notes/2.1.13.md | 7 - release_notes/2.1.15.md | 6 - release_notes/2.1.7.md | 8 - release_notes/3.0.15.md | 7 - release_notes/3.0.22.md | 8 - release_notes/3.1.3.md | 6 - release_notes/3.2.3.md | 6 - release_notes/3.3.2.md | 7 - release_notes/3.4.1.md | 7 - release_notes/3.5.0.md | 1 - release_notes/3.6.0.md | 2 - release_notes/3.7.0.md | 1 - release_notes/3.7.1.md | 1 - release_notes/3.7.2.md | 3 - release_notes/3.7.3.md | 5 - release_notes/3.7.4.md | 1 - release_notes/3.7.5.md | 1 - release_notes/3.7.6.md | 1 - release_notes/3.7.7.md | 1 - release_notes/3.7.8.md | 1 - release_notes/unreleased.md | 1 - requirements.txt | 3 - src/__init__.py | 3 + src/app.py | 95 ++ uv.lock | 921 +++++++++++ wheels/py3/validators-0.35.0-py3-none-any.whl | Bin 44712 -> 0 bytes 46 files changed, 1310 insertions(+), 3272 deletions(-) create mode 100644 .gitignore delete mode 100644 .pre-commit-config.yaml create mode 100644 .pre-commit-config.yml delete mode 100644 CONTRIBUTING.md delete mode 100644 LICENSE delete mode 100644 NOTICE delete mode 100644 README.md delete mode 100644 __init__.py delete mode 100644 http.json delete mode 100644 http_connector.py delete mode 100644 http_consts.py create mode 100644 logo.svg create mode 100644 logo_dark.svg delete mode 100644 logo_splunk.svg delete mode 100644 logo_splunk_dark.svg delete mode 100644 manual_readme_content.md delete mode 100644 release_notes/2.0.10.md delete mode 100644 release_notes/2.0.9.md delete mode 100644 release_notes/2.1.10.md delete mode 100644 release_notes/2.1.13.md delete mode 100644 release_notes/2.1.15.md delete mode 100644 release_notes/2.1.7.md delete mode 100644 release_notes/3.0.15.md delete mode 100644 release_notes/3.0.22.md delete mode 100644 release_notes/3.1.3.md delete mode 100644 release_notes/3.2.3.md delete mode 100644 release_notes/3.3.2.md delete mode 100644 release_notes/3.4.1.md delete mode 100644 release_notes/3.5.0.md delete mode 100644 release_notes/3.6.0.md delete mode 100644 release_notes/3.7.0.md delete mode 100644 release_notes/3.7.1.md delete mode 100644 release_notes/3.7.2.md delete mode 100644 release_notes/3.7.3.md delete mode 100644 release_notes/3.7.4.md delete mode 100644 release_notes/3.7.5.md delete mode 100644 release_notes/3.7.6.md delete mode 100644 release_notes/3.7.7.md delete mode 100644 release_notes/3.7.8.md delete mode 100644 release_notes/unreleased.md delete mode 100644 requirements.txt create mode 100644 src/__init__.py create mode 100644 src/app.py create mode 100644 uv.lock delete mode 100644 wheels/py3/validators-0.35.0-py3-none-any.whl diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..730c7ef --- /dev/null +++ b/.gitignore @@ -0,0 +1,179 @@ +# Created by https://www.toptal.com/developers/gitignore/api/python +# Edit at https://www.toptal.com/developers/gitignore?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +# End of https://www.toptal.com/developers/gitignore/api/python + +test_asset.json +*.tgz diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 95de823..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,82 +0,0 @@ -# See https://pre-commit.com for more information -# See https://pre-commit.com/hooks.html for more hooks - -# By default, run each hook only in the standard pre-commit stage -default_install_hook_types: [pre-commit, commit-msg] -default_stages: [pre-commit] - -# This is a template for connector pre-commit hooks -repos: - - repo: https://github.com/compilerla/conventional-pre-commit - rev: v4.1.0 - hooks: - - id: conventional-pre-commit - stages: [commit-msg] - args: [--verbose] - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 - hooks: - - id: check-merge-conflict - - id: end-of-file-fixer - exclude: ^NOTICE$ - exclude_types: [ markdown ] - - id: trailing-whitespace - exclude: ^NOTICE$ - exclude_types: [ markdown ] - - id: requirements-txt-fixer - - id: check-json - - id: check-yaml - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.7 - hooks: - - id: ruff - args: [ "--fix", "--unsafe-fixes"] # Allow unsafe fixes (ruff pretty strict about what it can fix) - - id: ruff-format - - repo: https://github.com/djlint/djLint - rev: v1.36.4 - hooks: - - id: djlint-reformat-django - - id: djlint-django - - repo: https://github.com/hukkin/mdformat - rev: 0.7.22 - hooks: - - id: mdformat - exclude: "release_notes/.*" - - repo: https://github.com/returntocorp/semgrep - rev: v1.89.0 - hooks: - - id: semgrep - - repo: https://github.com/Yelp/detect-secrets - rev: v1.5.0 - hooks: - - id: detect-secrets - args: ['--no-verify'] - exclude_types: [json] - exclude: "README.md" - # Central hooks - - repo: https://github.com/phantomcyber/dev-cicd-tools - rev: v2.0.7 - hooks: - - id: build-docs - language: python - additional_dependencies: ["local-hooks"] - args: ['.'] - - id: copyright - language: python - additional_dependencies: ["local-hooks"] - args: ['.'] - - id: package-app-dependencies - language: python - additional_dependencies: ["local-hooks"] - - id: notice-file - language: python - additional_dependencies: ["local-hooks"] - args: ['.'] - - id: release-notes - language: python - additional_dependencies: ["local-hooks"] - args: ['.'] - - id: static-tests - language: python - additional_dependencies: ["local-hooks"] - args: ['.'] diff --git a/.pre-commit-config.yml b/.pre-commit-config.yml new file mode 100644 index 0000000..22b03af --- /dev/null +++ b/.pre-commit-config.yml @@ -0,0 +1,11 @@ +repos: +- repo: https://github.com/phantomcyber/dev-cicd-tools + rev: v1.26 + hooks: + - id: org-hook + - id: package-app-dependencies +- repo: https://github.com/Yelp/detect-secrets + rev: v1.5.0 + hooks: + - id: detect-secrets + args: ['--no-verify'] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index c5b339e..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,3 +0,0 @@ -# Contributing - -For more information about contributing to Splunk SOAR Apps please take a look at our app [Contribution Guide](https://github.com/splunk-soar-connectors/.github/blob/main/.github/CONTRIBUTING.md)! diff --git a/LICENSE b/LICENSE deleted file mode 100644 index aac0063..0000000 --- a/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright (c) 2016-2025 Splunk Inc. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/NOTICE b/NOTICE deleted file mode 100644 index f5dd57e..0000000 --- a/NOTICE +++ /dev/null @@ -1,32 +0,0 @@ -Splunk SOAR App: HTTP -Copyright (c) 2016-2025 Splunk Inc. -Third Party Software Attributions: - -@@@@============================================================================ - -Library: validators - 0.35.0 -Homepage: https://python-validators.github.io/validators -License: MIT License -License Text: - -The MIT License (MIT) - -Copyright (c) 2013 - 2025 Konsta Vesterinen - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff --git a/README.md b/README.md deleted file mode 100644 index 992e80d..0000000 --- a/README.md +++ /dev/null @@ -1,398 +0,0 @@ -# HTTP - -Publisher: Splunk
-Connector Version: 3.7.8
-Product Vendor: Generic
-Product Name: HTTP
-Minimum Product Version: 6.2.1 - -This App facilitates making HTTP requests as actions - -For security reasons, accessing 127.0.0.1 is not allowed. - -This app requires access to port 80(for request send over HTTP) or port 443(for request send over -HTTPS) on your Phantom host(s) in order to function. - -**Authentication is carried out in following priority order** - -1. Basic Auth (username and password) -1. OAuth (oauth token url, client id and client secret) -1. Provided Auth token (auth_token_name, auth_token) - -### Configuration variables - -This table lists the configuration variables required to operate HTTP. These variables are specified when configuring a HTTP asset in Splunk SOAR. - -VARIABLE | REQUIRED | TYPE | DESCRIPTION --------- | -------- | ---- | ----------- -**base_url** | required | string | Base URL for making queries. (e.g. https://myservice/) | -**test_path** | optional | string | Endpoint for test connectivity. (e.g. /some/specific/endpoint , appended to Base URL) | -**auth_token_name** | optional | string | Type of authentication token | -**auth_token** | optional | password | Value of authentication token | -**username** | optional | string | Username (for HTTP basic auth) | -**password** | optional | password | Password (for HTTP basic auth) | -**oauth_token_url** | optional | string | URL to fetch oauth token from | -**client_id** | optional | string | Client ID (for OAuth) | -**client_secret** | optional | password | Client Secret (for OAuth) | -**timeout** | optional | numeric | Timeout for HTTP calls | -**test_http_method** | optional | string | HTTP Method for Test Connectivity | - -### Supported Actions - -[test connectivity](#action-test-connectivity) - Validate connection using the configured credentials
-[put data](#action-put-data) - Perform a REST PUT call to the server
-[patch data](#action-patch-data) - Perform a REST PATCH call to the server
-[delete data](#action-delete-data) - Perform a REST DELETE call to the server
-[get headers](#action-get-headers) - Perform a REST HEAD call to the server
-[get options](#action-get-options) - Perform a REST OPTIONS call to the server
-[get data](#action-get-data) - Perform a REST GET call to the server
-[post data](#action-post-data) - Perform a REST POST call to the server
-[get file](#action-get-file) - Retrieve a file from the endpoint and save it to the vault
-[put file](#action-put-file) - Put a file from the vault to another location - -## action: 'test connectivity' - -Validate connection using the configured credentials - -Type: **test**
-Read only: **True** - -This action will perform a GET on the configured base_url. The action will succeed if given a status code between 200 and 399 (inclusive). Therefore, if the given base URL requires no authentication, this action may pass even if the supplied credentials are incorrect. Alternately, if the given base URL does not point to a valid endpoint, this action could fail even if other actions may succeed with valid location parameters. - -#### Action Parameters - -No parameters are required for this action - -#### Action Output - -No Output - -## action: 'put data' - -Perform a REST PUT call to the server - -Type: **generic**
-Read only: **False** - -#### Action Parameters - -PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS ---------- | -------- | ----------- | ---- | -------- -**location** | required | Location (e.g. path/to/endpoint?query=string) | string | `endpoint` | -**body** | required | PATCH body (query string, JSON, etc.) | string | | -**verify_certificate** | optional | Verify certificates (if using HTTPS) | boolean | | -**headers** | optional | Additional headers (JSON object with headers) | string | | - -#### Action Output - -DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES ---------- | ---- | -------- | -------------- -action_result.status | string | | success failed | -action_result.parameter.body | string | | {"name": "Bad IP"} | -action_result.parameter.headers | string | | {"Content-Type": "application/json"} | -action_result.parameter.location | string | `endpoint` | /rest/assets | -action_result.parameter.verify_certificate | boolean | | False True | -action_result.data.\*.location | string | `url` | http://192.168.1.26/rest/assets | -action_result.data.\*.method | string | | PUT | -action_result.data.\*.parsed_response_body | string | | {"failed": true, "message": "Requested item not found"} | -action_result.data.\*.response_body | string | | {"failed": true, "message": "Requested item not found"} | -action_result.data.\*.response_headers | string | | | -action_result.summary.reason | string | | Not Found | -action_result.summary.status_code | numeric | | 404 | -action_result.message | string | | Can't process response from server. Status Code: 404 Data from server: {"failed": true, "message": "Requested item not found"} | -summary.total_objects | numeric | | 1 | -summary.total_objects_successful | numeric | | 0 | - -## action: 'patch data' - -Perform a REST PATCH call to the server - -Type: **generic**
-Read only: **False** - -#### Action Parameters - -PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS ---------- | -------- | ----------- | ---- | -------- -**location** | required | Location (e.g. path/to/endpoint?query=string) | string | `endpoint` | -**body** | optional | PATCH body (query string, JSON, etc.) | string | | -**verify_certificate** | optional | Verify certificates (if using HTTPS) | boolean | | -**headers** | optional | Additional headers (JSON object with headers) | string | | - -#### Action Output - -DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES ---------- | ---- | -------- | -------------- -action_result.status | string | | success failed | -action_result.parameter.body | string | | {"name": "Bad IP"} | -action_result.parameter.headers | string | | {"Content-Type": "application/json"} | -action_result.parameter.location | string | `endpoint` | /rest/assets | -action_result.parameter.verify_certificate | boolean | | False True | -action_result.data.\*.location | string | `url` | http://192.168.1.26/rest/assets | -action_result.data.\*.method | string | | PATCH | -action_result.data.\*.parsed_response_body | string | | {"failed": true, "message": "Requested item not found"} | -action_result.data.\*.response_body | string | | {"failed": true, "message": "Requested item not found"} | -action_result.data.\*.response_headers | string | | | -action_result.summary.reason | string | | Not Found | -action_result.summary.status_code | numeric | | 404 | -action_result.message | string | | Can't process response from server. Status Code: 404 Data from server: {"failed": true, "message": "Requested item not found"} | -summary.total_objects | numeric | | 1 | -summary.total_objects_successful | numeric | | 0 | - -## action: 'delete data' - -Perform a REST DELETE call to the server - -Type: **generic**
-Read only: **False** - -#### Action Parameters - -PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS ---------- | -------- | ----------- | ---- | -------- -**location** | required | Location (e.g. path/to/endpoint?query=string) | string | `endpoint` | -**body** | optional | DELETE body (query string, JSON, etc.) | string | | -**verify_certificate** | optional | Verify certificates (if using HTTPS) | boolean | | -**headers** | optional | Additional headers (JSON object with headers) | string | | - -#### Action Output - -DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES ---------- | ---- | -------- | -------------- -action_result.status | string | | success failed | -action_result.parameter.body | string | | {"name": "Bad IP"} | -action_result.parameter.headers | string | | {"Content-Type": "application/json"} | -action_result.parameter.location | string | `endpoint` | /rest/assets | -action_result.parameter.verify_certificate | boolean | | False True | -action_result.data.\*.location | string | `url` | http://192.168.1.26/rest/assets | -action_result.data.\*.method | string | | DELETE | -action_result.data.\*.parsed_response_body | string | | {"failed": true, "message": "Requested item not found"} | -action_result.data.\*.response_body | string | | {"failed": true, "message": "Requested item not found"} | -action_result.data.\*.response_headers | string | | | -action_result.summary.reason | string | | Not Found | -action_result.summary.status_code | numeric | | 404 | -action_result.message | string | | Can't process response from server. Status Code: 404 Data from server: {"failed": true, "message": "Requested item not found"} | -summary.total_objects | numeric | | 1 | -summary.total_objects_successful | numeric | | 0 | - -## action: 'get headers' - -Perform a REST HEAD call to the server - -Type: **investigate**
-Read only: **True** - -#### Action Parameters - -PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS ---------- | -------- | ----------- | ---- | -------- -**location** | required | Location (e.g. path/to/endpoint?query=string) | string | `endpoint` | -**verify_certificate** | optional | Verify certificates (if using HTTPS) | boolean | | -**headers** | optional | Additional headers (JSON object with headers) | string | | - -#### Action Output - -DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES ---------- | ---- | -------- | -------------- -action_result.status | string | | success failed | -action_result.parameter.headers | string | | {"User-Agent": "automation"} | -action_result.parameter.location | string | `endpoint` | /rest/cont | -action_result.parameter.verify_certificate | boolean | | False True | -action_result.data.\*.location | string | `url` | http://192.168.1.26/rest/cont | -action_result.data.\*.method | string | | HEAD | -action_result.data.\*.response_headers | string | | | -action_result.summary.reason | string | | OK | -action_result.summary.status_code | numeric | | 200 | -action_result.message | string | | Status code: 200, Reason: OK | -summary.total_objects | numeric | | 1 | -summary.total_objects_successful | numeric | | 1 | - -## action: 'get options' - -Perform a REST OPTIONS call to the server - -Type: **investigate**
-Read only: **True** - -#### Action Parameters - -PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS ---------- | -------- | ----------- | ---- | -------- -**location** | required | Location (e.g. path/to/endpoint?query=string) | string | `endpoint` | -**verify_certificate** | optional | Verify certificates (if using HTTPS) | boolean | | -**headers** | optional | Additional headers (JSON object with headers) | string | | - -#### Action Output - -DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES ---------- | ---- | -------- | -------------- -action_result.status | string | | success failed | -action_result.parameter.headers | string | | | -action_result.parameter.location | string | `endpoint` | /rest/cont | -action_result.parameter.verify_certificate | boolean | | False True | -action_result.data.\*.location | string | `url` | http://192.168.1.26/rest/cont | -action_result.data.\*.method | string | | OPTIONS | -action_result.data.\*.parsed_response_body | string | | GET,HEAD,PUT,POST,DELETE,PATCH | -action_result.data.\*.response_body | string | | GET,HEAD,PUT,POST,DELETE,PATCH | -action_result.data.\*.response_headers | string | | | -action_result.summary.reason | string | | OK | -action_result.summary.status_code | numeric | | 200 | -action_result.message | string | | Status code: 200, Reason: OK | -summary.total_objects | numeric | | 1 | -summary.total_objects_successful | numeric | | 1 | - -## action: 'get data' - -Perform a REST GET call to the server - -Type: **investigate**
-Read only: **True** - -#### Action Parameters - -PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS ---------- | -------- | ----------- | ---- | -------- -**location** | required | Location (e.g. path/to/endpoint?query=string) | string | `endpoint` | -**verify_certificate** | optional | Verify certificates (if using HTTPS) | boolean | | -**headers** | optional | Additional headers (JSON object with headers) | string | | - -#### Action Output - -DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES ---------- | ---- | -------- | -------------- -action_result.status | string | | success failed | -action_result.parameter.headers | string | | {"Content-Type": "application/json"} | -action_result.parameter.location | string | `endpoint` | /rest/cont | -action_result.parameter.verify_certificate | boolean | | False True | -action_result.data.\*.location | string | `url` | http://192.168.1.26/rest/cont | -action_result.data.\*.method | string | | GET | -action_result.data.\*.parsed_response_body | string | | {"failed": true, "message": "Requested item not found"} | -action_result.data.\*.response_body | string | | {"failed": true, "message": "Requested item not found"} | -action_result.data.\*.response_headers | string | | | -action_result.summary.reason | string | | Not Found | -action_result.summary.status_code | numeric | | 404 | -action_result.message | string | | Can't process response from server. Status Code: 404 Data from server: {"failed": true, "message": "Requested item not found"} | -summary.total_objects | numeric | | 1 | -summary.total_objects_successful | numeric | | 0 | - -## action: 'post data' - -Perform a REST POST call to the server - -Type: **generic**
-Read only: **False** - -#### Action Parameters - -PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS ---------- | -------- | ----------- | ---- | -------- -**location** | required | Location (e.g. path/to/endpoint) | string | `endpoint` | -**body** | optional | POST body (query string, JSON, etc.) | string | | -**verify_certificate** | optional | Verify certificates (if using HTTPS) | boolean | | -**headers** | optional | Additional headers (JSON object with headers) | string | | - -#### Action Output - -DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES ---------- | ---- | -------- | -------------- -action_result.status | string | | success failed | -action_result.parameter.body | string | | {"name": "Bad IP"} | -action_result.parameter.headers | string | | {"Content-Type": "application/json"} | -action_result.parameter.location | string | `endpoint` | /rest/assets | -action_result.parameter.verify_certificate | boolean | | False True | -action_result.data.\*.location | string | `url` | http://192.168.1.26/rest/assets | -action_result.data.\*.method | string | | POST | -action_result.data.\*.parsed_response_body | string | | {"failed": true, "message": "Requested item not found"} | -action_result.data.\*.response_body | string | | {"failed": true, "message": "Requested item not found"} | -action_result.data.\*.response_headers | string | | | -action_result.summary.reason | string | | Not Found | -action_result.summary.status_code | numeric | | 404 | -action_result.message | string | | Can't process response from server. Status Code: 404 Data from server: {"failed": true, "message": "Requested item not found"} | -summary.total_objects | numeric | | 1 | -summary.total_objects_successful | numeric | | 0 | - -## action: 'get file' - -Retrieve a file from the endpoint and save it to the vault - -Type: **investigate**
-Read only: **True** - -Provide the file path and file name to download into the vault. For example, /web_storage/file.tgz. - -#### Action Parameters - -PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS ---------- | -------- | ----------- | ---- | -------- -**hostname** | optional | Hostname to execute command on | string | `host name` | -**file_path** | required | Path of the file to download (include filename) | string | `file path` | -**verify_certificate** | optional | Verify certificates (if using HTTPS) | boolean | | - -#### Action Output - -DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES ---------- | ---- | -------- | -------------- -action_result.status | string | | success failed | -action_result.parameter.file_path | string | `file path` | /web_storage/file.tgz | -action_result.parameter.verify_certificate | boolean | | False True | -action_result.parameter.hostname | string | `host name` | http://192.168.0.1 | -action_result.data | string | | | -action_result.summary.exit_status | numeric | | 0 | -action_result.summary.name | string | | file.tgz | -action_result.summary.size | numeric | | 412 | -action_result.summary.vault_id | string | `vault id` | dc871f816c4d179f3a3cea24b4bc81a67562c | -action_result.message | string | | Transferred file | -summary.total_objects | numeric | | 1 | -summary.total_objects_successful | numeric | | 1 | - -## action: 'put file' - -Put a file from the vault to another location - -Type: **generic**
-Read only: **False** - -Provide the path to store the file on the file server. For example, /web_storage/test_repo/. - -#### Action Parameters - -PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS ---------- | -------- | ----------- | ---- | -------- -**host** | optional | Hostname/IP with port number to execute command on | string | `host name` | -**vault_id** | required | Vault ID of file | string | `vault id` | -**file_destination** | required | File destination path (exclude filename) | string | `file path` | -**file_name** | optional | Name of the file to be put on endpoint | string | | -**verify_certificate** | optional | Verify certificates (if using HTTPS) | boolean | | - -#### Action Output - -DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES ---------- | ---- | -------- | -------------- -action_result.status | string | | success failed | -action_result.parameter.file_name | string | | test.txt | -action_result.parameter.file_destination | string | `file path` | /web_storage/test_repo/ | -action_result.parameter.host | string | `host name` | http://192.168.0.1:8001 | -action_result.parameter.verify_certificate | boolean | | False True | -action_result.parameter.vault_id | string | `vault id` | dc871f816c4d179f3a3cea24b4bc81a67562c | -action_result.data | string | | | -action_result.summary.file_sent | string | `file path` | http://192.168.0.1:8001/web_storage/test_repo/file.tgz | -action_result.message | string | | File sent: http://192.168.0.1:8001/web_storage/test_repo/file.tgz | -summary.total_objects | numeric | | 1 | -summary.total_objects_successful | numeric | | 1 | - -______________________________________________________________________ - -Auto-generated Splunk SOAR Connector documentation. - -Copyright 2025 Splunk Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and limitations under the License. diff --git a/__init__.py b/__init__.py deleted file mode 100644 index 41abd5e..0000000 --- a/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# File: __init__.py -# -# Copyright (c) 2016-2025 Splunk Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed under -# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, -# either express or implied. See the License for the specific language governing permissions -# and limitations under the License. diff --git a/http.json b/http.json deleted file mode 100644 index 9b07cc1..0000000 --- a/http.json +++ /dev/null @@ -1,1427 +0,0 @@ -{ - "appid": "290b7499-0374-4930-9cdc-5e9b05d65827", - "name": "HTTP", - "description": "This App facilitates making HTTP requests as actions", - "package_name": "phantom_http", - "publisher": "Splunk", - "contributors": [ - { - "name": "Ricardo Moreira" - } - ], - "type": "information", - "main_module": "http_connector.py", - "app_version": "3.7.8", - "utctime_updated": "2025-08-04T22:10:32.578334Z", - "product_vendor": "Generic", - "product_name": "HTTP", - "product_version_regex": ".*", - "min_phantom_version": "6.2.1", - "license": "Copyright (c) 2016-2025 Splunk Inc.", - "logo": "logo_splunk.svg", - "logo_dark": "logo_splunk_dark.svg", - "python_version": "3.9", - "fips_compliant": true, - "latest_tested_versions": [ - "Tested with Crowdstrike on 12th September 2022" - ], - "configuration": { - "base_url": { - "data_type": "string", - "order": 0, - "description": "Base URL for making queries. (e.g. https://myservice/)", - "required": true - }, - "test_path": { - "data_type": "string", - "order": 1, - "description": "Endpoint for test connectivity. (e.g. /some/specific/endpoint , appended to Base URL)" - }, - "auth_token_name": { - "data_type": "string", - "order": 2, - "description": "Type of authentication token", - "default": "ph-auth-token" - }, - "auth_token": { - "data_type": "password", - "order": 3, - "description": "Value of authentication token" - }, - "username": { - "data_type": "string", - "order": 4, - "description": "Username (for HTTP basic auth)" - }, - "password": { - "data_type": "password", - "order": 5, - "description": "Password (for HTTP basic auth)" - }, - "oauth_token_url": { - "data_type": "string", - "order": 6, - "description": "URL to fetch oauth token from" - }, - "placeholder": { - "data_type": "ph", - "order": 7, - "description": "" - }, - "client_id": { - "data_type": "string", - "order": 8, - "description": "Client ID (for OAuth)" - }, - "client_secret": { - "data_type": "password", - "order": 9, - "description": "Client Secret (for OAuth)" - }, - "timeout": { - "data_type": "numeric", - "order": 10, - "description": "Timeout for HTTP calls" - }, - "test_http_method": { - "data_type": "string", - "order": 11, - "description": "HTTP Method for Test Connectivity", - "default": "GET", - "value_list": [ - "GET", - "HEAD", - "POST", - "PUT", - "DELETE", - "OPTIONS", - "TRACE", - "PATCH" - ] - } - }, - "actions": [ - { - "action": "test connectivity", - "identifier": "test_connectivity", - "description": "Validate connection using the configured credentials", - "verbose": "This action will perform a GET on the configured base_url. The action will succeed if given a status code between 200 and 399 (inclusive). Therefore, if the given base URL requires no authentication, this action may pass even if the supplied credentials are incorrect. Alternately, if the given base URL does not point to a valid endpoint, this action could fail even if other actions may succeed with valid location parameters.", - "type": "test", - "read_only": true, - "parameters": {}, - "output": [], - "versions": "EQ(*)" - }, - { - "action": "put data", - "description": "Perform a REST PUT call to the server", - "type": "generic", - "identifier": "http_put", - "read_only": false, - "parameters": { - "location": { - "description": "Location (e.g. path/to/endpoint?query=string)", - "data_type": "string", - "order": 0, - "primary": true, - "contains": [ - "endpoint" - ], - "required": true - }, - "body": { - "description": "PATCH body (query string, JSON, etc.)", - "data_type": "string", - "order": 1, - "required": true - }, - "verify_certificate": { - "description": "Verify certificates (if using HTTPS)", - "data_type": "boolean", - "order": 2 - }, - "headers": { - "description": "Additional headers (JSON object with headers)", - "data_type": "string", - "order": 3 - } - }, - "output": [ - { - "data_path": "action_result.status", - "data_type": "string", - "example_values": [ - "success", - "failed" - ] - }, - { - "data_path": "action_result.parameter.body", - "data_type": "string", - "example_values": [ - "{\"name\": \"Bad IP\"}" - ] - }, - { - "data_path": "action_result.parameter.headers", - "data_type": "string", - "example_values": [ - "{\"Content-Type\": \"application/json\"}" - ] - }, - { - "data_path": "action_result.parameter.location", - "data_type": "string", - "contains": [ - "endpoint" - ], - "example_values": [ - "/rest/assets" - ] - }, - { - "data_path": "action_result.parameter.verify_certificate", - "data_type": "boolean", - "example_values": [ - false, - true - ] - }, - { - "data_path": "action_result.data.*.location", - "data_type": "string", - "contains": [ - "url" - ], - "example_values": [ - "http://192.168.1.26/rest/assets" - ] - }, - { - "data_path": "action_result.data.*.method", - "data_type": "string", - "example_values": [ - "PUT" - ] - }, - { - "data_path": "action_result.data.*.parsed_response_body", - "data_type": "string", - "example_values": [ - "{\"failed\": true, \"message\": \"Requested item not found\"}" - ] - }, - { - "data_path": "action_result.data.*.response_body", - "data_type": "string", - "example_values": [ - "{\"failed\": true, \"message\": \"Requested item not found\"}" - ] - }, - { - "data_path": "action_result.data.*.response_headers", - "data_type": "string" - }, - { - "data_path": "action_result.summary.reason", - "data_type": "string", - "example_values": [ - "Not Found" - ] - }, - { - "data_path": "action_result.summary.status_code", - "data_type": "numeric", - "example_values": [ - 404 - ] - }, - { - "data_path": "action_result.message", - "data_type": "string", - "example_values": [ - "Can't process response from server. Status Code: 404 Data from server: {\"failed\": true, \"message\": \"Requested item not found\"}" - ] - }, - { - "data_path": "summary.total_objects", - "data_type": "numeric", - "example_values": [ - 1 - ] - }, - { - "data_path": "summary.total_objects_successful", - "data_type": "numeric", - "example_values": [ - 0 - ] - } - ], - "render": { - "type": "json" - }, - "versions": "EQ(*)" - }, - { - "action": "patch data", - "description": "Perform a REST PATCH call to the server", - "type": "generic", - "identifier": "http_patch", - "read_only": false, - "parameters": { - "location": { - "description": "Location (e.g. path/to/endpoint?query=string)", - "data_type": "string", - "order": 0, - "primary": true, - "contains": [ - "endpoint" - ], - "required": true - }, - "body": { - "description": "PATCH body (query string, JSON, etc.)", - "data_type": "string", - "order": 1 - }, - "verify_certificate": { - "description": "Verify certificates (if using HTTPS)", - "data_type": "boolean", - "order": 2 - }, - "headers": { - "description": "Additional headers (JSON object with headers)", - "data_type": "string", - "order": 3 - } - }, - "output": [ - { - "data_path": "action_result.status", - "data_type": "string", - "example_values": [ - "success", - "failed" - ] - }, - { - "data_path": "action_result.parameter.body", - "data_type": "string", - "example_values": [ - "{\"name\": \"Bad IP\"}" - ] - }, - { - "data_path": "action_result.parameter.headers", - "data_type": "string", - "example_values": [ - "{\"Content-Type\": \"application/json\"}" - ] - }, - { - "data_path": "action_result.parameter.location", - "data_type": "string", - "contains": [ - "endpoint" - ], - "example_values": [ - "/rest/assets" - ] - }, - { - "data_path": "action_result.parameter.verify_certificate", - "data_type": "boolean", - "example_values": [ - false, - true - ] - }, - { - "data_path": "action_result.data.*.location", - "data_type": "string", - "contains": [ - "url" - ], - "example_values": [ - "http://192.168.1.26/rest/assets" - ] - }, - { - "data_path": "action_result.data.*.method", - "data_type": "string", - "example_values": [ - "PATCH" - ] - }, - { - "data_path": "action_result.data.*.parsed_response_body", - "data_type": "string", - "example_values": [ - "{\"failed\": true, \"message\": \"Requested item not found\"}" - ] - }, - { - "data_path": "action_result.data.*.response_body", - "data_type": "string", - "example_values": [ - "{\"failed\": true, \"message\": \"Requested item not found\"}" - ] - }, - { - "data_path": "action_result.data.*.response_headers", - "data_type": "string" - }, - { - "data_path": "action_result.summary.reason", - "data_type": "string", - "example_values": [ - "Not Found" - ] - }, - { - "data_path": "action_result.summary.status_code", - "data_type": "numeric", - "example_values": [ - 404 - ] - }, - { - "data_path": "action_result.message", - "data_type": "string", - "example_values": [ - "Can't process response from server. Status Code: 404 Data from server: {\"failed\": true, \"message\": \"Requested item not found\"}" - ] - }, - { - "data_path": "summary.total_objects", - "data_type": "numeric", - "example_values": [ - 1 - ] - }, - { - "data_path": "summary.total_objects_successful", - "data_type": "numeric", - "example_values": [ - 0 - ] - } - ], - "render": { - "type": "json" - }, - "versions": "EQ(*)" - }, - { - "action": "delete data", - "description": "Perform a REST DELETE call to the server", - "type": "generic", - "identifier": "http_delete", - "read_only": false, - "parameters": { - "location": { - "description": "Location (e.g. path/to/endpoint?query=string)", - "data_type": "string", - "order": 0, - "primary": true, - "contains": [ - "endpoint" - ], - "required": true - }, - "body": { - "description": "DELETE body (query string, JSON, etc.)", - "data_type": "string", - "order": 1 - }, - "verify_certificate": { - "description": "Verify certificates (if using HTTPS)", - "data_type": "boolean", - "order": 2 - }, - "headers": { - "description": "Additional headers (JSON object with headers)", - "data_type": "string", - "order": 3 - } - }, - "output": [ - { - "data_path": "action_result.status", - "data_type": "string", - "example_values": [ - "success", - "failed" - ] - }, - { - "data_path": "action_result.parameter.body", - "data_type": "string", - "example_values": [ - "{\"name\": \"Bad IP\"}" - ] - }, - { - "data_path": "action_result.parameter.headers", - "data_type": "string", - "example_values": [ - "{\"Content-Type\": \"application/json\"}" - ] - }, - { - "data_path": "action_result.parameter.location", - "data_type": "string", - "contains": [ - "endpoint" - ], - "example_values": [ - "/rest/assets" - ] - }, - { - "data_path": "action_result.parameter.verify_certificate", - "data_type": "boolean", - "example_values": [ - false, - true - ] - }, - { - "data_path": "action_result.data.*.location", - "data_type": "string", - "contains": [ - "url" - ], - "example_values": [ - "http://192.168.1.26/rest/assets" - ] - }, - { - "data_path": "action_result.data.*.method", - "data_type": "string", - "example_values": [ - "DELETE" - ] - }, - { - "data_path": "action_result.data.*.parsed_response_body", - "data_type": "string", - "example_values": [ - "{\"failed\": true, \"message\": \"Requested item not found\"}" - ] - }, - { - "data_path": "action_result.data.*.response_body", - "data_type": "string", - "example_values": [ - "{\"failed\": true, \"message\": \"Requested item not found\"}" - ] - }, - { - "data_path": "action_result.data.*.response_headers", - "data_type": "string" - }, - { - "data_path": "action_result.summary.reason", - "data_type": "string", - "example_values": [ - "Not Found" - ] - }, - { - "data_path": "action_result.summary.status_code", - "data_type": "numeric", - "example_values": [ - 404 - ] - }, - { - "data_path": "action_result.message", - "data_type": "string", - "example_values": [ - "Can't process response from server. Status Code: 404 Data from server: {\"failed\": true, \"message\": \"Requested item not found\"}" - ] - }, - { - "data_path": "summary.total_objects", - "data_type": "numeric", - "example_values": [ - 1 - ] - }, - { - "data_path": "summary.total_objects_successful", - "data_type": "numeric", - "example_values": [ - 0 - ] - } - ], - "render": { - "type": "json" - }, - "versions": "EQ(*)" - }, - { - "action": "get headers", - "description": "Perform a REST HEAD call to the server", - "type": "investigate", - "identifier": "http_head", - "read_only": true, - "parameters": { - "location": { - "description": "Location (e.g. path/to/endpoint?query=string)", - "data_type": "string", - "order": 0, - "primary": true, - "contains": [ - "endpoint" - ], - "required": true - }, - "verify_certificate": { - "description": "Verify certificates (if using HTTPS)", - "data_type": "boolean", - "order": 1 - }, - "headers": { - "description": "Additional headers (JSON object with headers)", - "data_type": "string", - "order": 2 - } - }, - "output": [ - { - "data_path": "action_result.status", - "data_type": "string", - "example_values": [ - "success", - "failed" - ] - }, - { - "data_path": "action_result.parameter.headers", - "data_type": "string", - "example_values": [ - "{\"User-Agent\": \"automation\"}" - ] - }, - { - "data_path": "action_result.parameter.location", - "data_type": "string", - "contains": [ - "endpoint" - ], - "example_values": [ - "/rest/cont" - ] - }, - { - "data_path": "action_result.parameter.verify_certificate", - "data_type": "boolean", - "example_values": [ - false, - true - ] - }, - { - "data_path": "action_result.data.*.location", - "data_type": "string", - "contains": [ - "url" - ], - "example_values": [ - "http://192.168.1.26/rest/cont" - ] - }, - { - "data_path": "action_result.data.*.method", - "data_type": "string", - "example_values": [ - "HEAD" - ] - }, - { - "data_path": "action_result.data.*.response_headers", - "data_type": "string" - }, - { - "data_path": "action_result.summary.reason", - "data_type": "string", - "example_values": [ - "OK" - ] - }, - { - "data_path": "action_result.summary.status_code", - "data_type": "numeric", - "example_values": [ - 200 - ] - }, - { - "data_path": "action_result.message", - "data_type": "string", - "example_values": [ - "Status code: 200, Reason: OK" - ] - }, - { - "data_path": "summary.total_objects", - "data_type": "numeric", - "example_values": [ - 1 - ] - }, - { - "data_path": "summary.total_objects_successful", - "data_type": "numeric", - "example_values": [ - 1 - ] - } - ], - "render": { - "type": "json" - }, - "versions": "EQ(*)" - }, - { - "action": "get options", - "description": "Perform a REST OPTIONS call to the server", - "type": "investigate", - "identifier": "http_options", - "read_only": true, - "parameters": { - "location": { - "description": "Location (e.g. path/to/endpoint?query=string)", - "data_type": "string", - "order": 0, - "primary": true, - "contains": [ - "endpoint" - ], - "required": true - }, - "verify_certificate": { - "description": "Verify certificates (if using HTTPS)", - "data_type": "boolean", - "order": 1 - }, - "headers": { - "description": "Additional headers (JSON object with headers)", - "data_type": "string", - "order": 2 - } - }, - "output": [ - { - "data_path": "action_result.status", - "data_type": "string", - "example_values": [ - "success", - "failed" - ] - }, - { - "data_path": "action_result.parameter.headers", - "data_type": "string" - }, - { - "data_path": "action_result.parameter.location", - "data_type": "string", - "contains": [ - "endpoint" - ], - "example_values": [ - "/rest/cont" - ] - }, - { - "data_path": "action_result.parameter.verify_certificate", - "data_type": "boolean", - "example_values": [ - false, - true - ] - }, - { - "data_path": "action_result.data.*.location", - "data_type": "string", - "contains": [ - "url" - ], - "example_values": [ - "http://192.168.1.26/rest/cont" - ] - }, - { - "data_path": "action_result.data.*.method", - "data_type": "string", - "example_values": [ - "OPTIONS" - ] - }, - { - "data_path": "action_result.data.*.parsed_response_body", - "data_type": "string", - "example_values": [ - "GET,HEAD,PUT,POST,DELETE,PATCH" - ] - }, - { - "data_path": "action_result.data.*.response_body", - "data_type": "string", - "example_values": [ - "GET,HEAD,PUT,POST,DELETE,PATCH" - ] - }, - { - "data_path": "action_result.data.*.response_headers", - "data_type": "string" - }, - { - "data_path": "action_result.summary.reason", - "data_type": "string", - "example_values": [ - "OK" - ] - }, - { - "data_path": "action_result.summary.status_code", - "data_type": "numeric", - "example_values": [ - 200 - ] - }, - { - "data_path": "action_result.message", - "data_type": "string", - "example_values": [ - "Status code: 200, Reason: OK" - ] - }, - { - "data_path": "summary.total_objects", - "data_type": "numeric", - "example_values": [ - 1 - ] - }, - { - "data_path": "summary.total_objects_successful", - "data_type": "numeric", - "example_values": [ - 1 - ] - } - ], - "render": { - "type": "json" - }, - "versions": "EQ(*)" - }, - { - "action": "get data", - "description": "Perform a REST GET call to the server", - "type": "investigate", - "identifier": "http_get", - "read_only": true, - "parameters": { - "location": { - "description": "Location (e.g. path/to/endpoint?query=string)", - "data_type": "string", - "order": 0, - "primary": true, - "contains": [ - "endpoint" - ], - "required": true - }, - "verify_certificate": { - "description": "Verify certificates (if using HTTPS)", - "data_type": "boolean", - "order": 1 - }, - "headers": { - "description": "Additional headers (JSON object with headers)", - "data_type": "string", - "order": 2 - } - }, - "render": { - "type": "json", - "width": 10, - "height": 5, - "title": "GET DATA" - }, - "output": [ - { - "data_path": "action_result.status", - "data_type": "string", - "example_values": [ - "success", - "failed" - ] - }, - { - "data_path": "action_result.parameter.headers", - "data_type": "string", - "example_values": [ - "{\"Content-Type\": \"application/json\"}" - ] - }, - { - "data_path": "action_result.parameter.location", - "data_type": "string", - "contains": [ - "endpoint" - ], - "example_values": [ - "/rest/cont" - ] - }, - { - "data_path": "action_result.parameter.verify_certificate", - "data_type": "boolean", - "example_values": [ - false, - true - ] - }, - { - "data_path": "action_result.data.*.location", - "data_type": "string", - "contains": [ - "url" - ], - "example_values": [ - "http://192.168.1.26/rest/cont" - ] - }, - { - "data_path": "action_result.data.*.method", - "data_type": "string", - "example_values": [ - "GET" - ] - }, - { - "data_path": "action_result.data.*.parsed_response_body", - "data_type": "string", - "example_values": [ - "{\"failed\": true, \"message\": \"Requested item not found\"}" - ] - }, - { - "data_path": "action_result.data.*.response_body", - "data_type": "string", - "example_values": [ - "{\"failed\": true, \"message\": \"Requested item not found\"}" - ] - }, - { - "data_path": "action_result.data.*.response_headers", - "data_type": "string" - }, - { - "data_path": "action_result.summary.reason", - "data_type": "string", - "example_values": [ - "Not Found" - ] - }, - { - "data_path": "action_result.summary.status_code", - "data_type": "numeric", - "example_values": [ - 404 - ] - }, - { - "data_path": "action_result.message", - "data_type": "string", - "example_values": [ - "Can't process response from server. Status Code: 404 Data from server: {\"failed\": true, \"message\": \"Requested item not found\"}" - ] - }, - { - "data_path": "summary.total_objects", - "data_type": "numeric", - "example_values": [ - 1 - ] - }, - { - "data_path": "summary.total_objects_successful", - "data_type": "numeric", - "example_values": [ - 0 - ] - } - ], - "versions": "EQ(*)" - }, - { - "action": "post data", - "description": "Perform a REST POST call to the server", - "type": "generic", - "identifier": "http_post", - "read_only": false, - "parameters": { - "location": { - "description": "Location (e.g. path/to/endpoint)", - "data_type": "string", - "order": 0, - "primary": true, - "contains": [ - "endpoint" - ], - "required": true - }, - "body": { - "description": "POST body (query string, JSON, etc.)", - "data_type": "string", - "order": 1 - }, - "verify_certificate": { - "description": "Verify certificates (if using HTTPS)", - "data_type": "boolean", - "order": 2 - }, - "headers": { - "description": "Additional headers (JSON object with headers)", - "data_type": "string", - "order": 3 - } - }, - "render": { - "type": "json", - "width": 10, - "height": 5, - "title": "POST DATA" - }, - "output": [ - { - "data_path": "action_result.status", - "data_type": "string", - "example_values": [ - "success", - "failed" - ] - }, - { - "data_path": "action_result.parameter.body", - "data_type": "string", - "example_values": [ - "{\"name\": \"Bad IP\"}" - ] - }, - { - "data_path": "action_result.parameter.headers", - "data_type": "string", - "example_values": [ - "{\"Content-Type\": \"application/json\"}" - ] - }, - { - "data_path": "action_result.parameter.location", - "data_type": "string", - "contains": [ - "endpoint" - ], - "example_values": [ - "/rest/assets" - ] - }, - { - "data_path": "action_result.parameter.verify_certificate", - "data_type": "boolean", - "example_values": [ - false, - true - ] - }, - { - "data_path": "action_result.data.*.location", - "data_type": "string", - "contains": [ - "url" - ], - "example_values": [ - "http://192.168.1.26/rest/assets" - ] - }, - { - "data_path": "action_result.data.*.method", - "data_type": "string", - "example_values": [ - "POST" - ] - }, - { - "data_path": "action_result.data.*.parsed_response_body", - "data_type": "string", - "example_values": [ - "{\"failed\": true, \"message\": \"Requested item not found\"}" - ] - }, - { - "data_path": "action_result.data.*.response_body", - "data_type": "string", - "example_values": [ - "{\"failed\": true, \"message\": \"Requested item not found\"}" - ] - }, - { - "data_path": "action_result.data.*.response_headers", - "data_type": "string" - }, - { - "data_path": "action_result.summary.reason", - "data_type": "string", - "example_values": [ - "Not Found" - ] - }, - { - "data_path": "action_result.summary.status_code", - "data_type": "numeric", - "example_values": [ - 404 - ] - }, - { - "data_path": "action_result.message", - "data_type": "string", - "example_values": [ - "Can't process response from server. Status Code: 404 Data from server: {\"failed\": true, \"message\": \"Requested item not found\"}" - ] - }, - { - "data_path": "summary.total_objects", - "data_type": "numeric", - "example_values": [ - 1 - ] - }, - { - "data_path": "summary.total_objects_successful", - "data_type": "numeric", - "example_values": [ - 0 - ] - } - ], - "versions": "EQ(*)" - }, - { - "action": "get file", - "description": "Retrieve a file from the endpoint and save it to the vault", - "verbose": "Provide the file path and file name to download into the vault. For example, /web_storage/file.tgz.", - "type": "investigate", - "identifier": "get_file", - "read_only": true, - "parameters": { - "hostname": { - "data_type": "string", - "order": 0, - "description": "Hostname to execute command on", - "contains": [ - "host name" - ], - "primary": true - }, - "file_path": { - "data_type": "string", - "order": 1, - "description": "Path of the file to download (include filename)", - "contains": [ - "file path" - ], - "required": true, - "primary": true - }, - "verify_certificate": { - "description": "Verify certificates (if using HTTPS)", - "data_type": "boolean", - "order": 2 - } - }, - "render": { - "width": 12, - "title": "Get File", - "type": "table", - "height": 5 - }, - "output": [ - { - "data_path": "action_result.status", - "data_type": "string", - "example_values": [ - "success", - "failed" - ] - }, - { - "data_path": "action_result.parameter.file_path", - "data_type": "string", - "contains": [ - "file path" - ], - "example_values": [ - "/web_storage/file.tgz" - ] - }, - { - "data_path": "action_result.parameter.verify_certificate", - "data_type": "boolean", - "example_values": [ - false, - true - ] - }, - { - "data_path": "action_result.parameter.hostname", - "data_type": "string", - "contains": [ - "host name" - ], - "example_values": [ - "http://192.168.0.1" - ] - }, - { - "data_path": "action_result.data", - "data_type": "string" - }, - { - "data_path": "action_result.summary.exit_status", - "data_type": "numeric", - "example_values": [ - 0 - ] - }, - { - "data_path": "action_result.summary.name", - "data_type": "string", - "example_values": [ - "file.tgz" - ] - }, - { - "data_path": "action_result.summary.size", - "data_type": "numeric", - "example_values": [ - 412 - ] - }, - { - "data_path": "action_result.summary.vault_id", - "data_type": "string", - "contains": [ - "vault id" - ], - "column_order": 1, - "column_name": "Vault ID", - "example_values": [ - "dc871f816c4d179f3a3cea24b4bc81a67562c" - ] - }, - { - "data_path": "action_result.message", - "data_type": "string", - "column_order": 0, - "column_name": "Result Message", - "example_values": [ - "Transferred file" - ] - }, - { - "data_path": "summary.total_objects", - "data_type": "numeric", - "example_values": [ - 1 - ] - }, - { - "data_path": "summary.total_objects_successful", - "data_type": "numeric", - "example_values": [ - 1 - ] - } - ], - "versions": "EQ(*)" - }, - { - "action": "put file", - "description": "Put a file from the vault to another location", - "type": "generic", - "identifier": "put_file", - "verbose": "Provide the path to store the file on the file server. For example, /web_storage/test_repo/.", - "read_only": false, - "parameters": { - "host": { - "data_type": "string", - "order": 0, - "description": "Hostname/IP with port number to execute command on", - "contains": [ - "host name" - ], - "primary": true - }, - "vault_id": { - "data_type": "string", - "order": 1, - "contains": [ - "vault id" - ], - "description": "Vault ID of file", - "required": true, - "primary": true - }, - "file_destination": { - "description": "File destination path (exclude filename)", - "data_type": "string", - "order": 2, - "contains": [ - "file path" - ], - "required": true, - "primary": true - }, - "file_name": { - "description": "Name of the file to be put on endpoint", - "data_type": "string", - "order": 3 - }, - "verify_certificate": { - "description": "Verify certificates (if using HTTPS)", - "data_type": "boolean", - "order": 4 - } - }, - "render": { - "width": 12, - "title": "Put File", - "type": "table", - "height": 5 - }, - "output": [ - { - "data_path": "action_result.status", - "data_type": "string", - "example_values": [ - "success", - "failed" - ] - }, - { - "data_path": "action_result.parameter.file_name", - "data_type": "string", - "example_values": [ - "test.txt" - ] - }, - { - "data_path": "action_result.parameter.file_destination", - "data_type": "string", - "contains": [ - "file path" - ], - "example_values": [ - "/web_storage/test_repo/" - ] - }, - { - "data_path": "action_result.parameter.host", - "data_type": "string", - "contains": [ - "host name" - ], - "example_values": [ - "http://192.168.0.1:8001" - ] - }, - { - "data_path": "action_result.parameter.verify_certificate", - "data_type": "boolean", - "example_values": [ - false, - true - ] - }, - { - "data_path": "action_result.parameter.vault_id", - "data_type": "string", - "contains": [ - "vault id" - ], - "column_order": 0, - "column_name": "Vault ID", - "example_values": [ - "dc871f816c4d179f3a3cea24b4bc81a67562c" - ] - }, - { - "data_path": "action_result.data", - "data_type": "string" - }, - { - "data_path": "action_result.summary.file_sent", - "data_type": "string", - "column_order": 1, - "column_name": "File Transferred", - "contains": [ - "file path" - ], - "example_values": [ - "http://192.168.0.1:8001/web_storage/test_repo/file.tgz" - ] - }, - { - "data_path": "action_result.message", - "data_type": "string", - "column_order": 2, - "column_name": "Result Message", - "example_values": [ - "File sent: http://192.168.0.1:8001/web_storage/test_repo/file.tgz" - ] - }, - { - "data_path": "summary.total_objects", - "data_type": "numeric", - "example_values": [ - 1 - ] - }, - { - "data_path": "summary.total_objects_successful", - "data_type": "numeric", - "example_values": [ - 1 - ] - } - ], - "versions": "EQ(*)" - } - ], - "pip39_dependencies": { - "wheel": [ - { - "module": "validators", - "input_file": "wheels/py3/validators-0.35.0-py3-none-any.whl" - } - ] - }, - "pip313_dependencies": { - "wheel": [ - { - "module": "validators", - "input_file": "wheels/py3/validators-0.35.0-py3-none-any.whl" - } - ] - } -} diff --git a/http_connector.py b/http_connector.py deleted file mode 100644 index a8a4ed2..0000000 --- a/http_connector.py +++ /dev/null @@ -1,927 +0,0 @@ -# File: http_connector.py -# -# Copyright (c) 2016-2025 Splunk Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed under -# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, -# either express or implied. See the License for the specific language governing permissions -# and limitations under the License. - -import json -import os -import re -import shutil -import socket -import sys -import uuid -from urllib.parse import quote, unquote_plus, urlparse - -import encryption_helper -import magic -import phantom.app as phantom -import phantom.rules as ph_rules -import requests -import validators -import xmltodict -from bs4 import BeautifulSoup, UnicodeDammit -from phantom.action_result import ActionResult -from phantom.base_connector import BaseConnector -from phantom.vault import Vault as Vault -from requests.auth import HTTPBasicAuth - -from http_consts import * - - -class RetVal(tuple): - def __new__(cls, val1, val2=None): - return tuple.__new__(RetVal, (val1, val2)) - - -class HttpConnector(BaseConnector): - MAGIC_FORMATS = [ - (re.compile("^PE.* Windows"), ["pe file"], ".exe"), - (re.compile("^MS-DOS executable"), ["pe file"], ".exe"), - (re.compile("^PDF "), ["pdf"], ".pdf"), - (re.compile("^MDMP crash"), ["process dump"], ".dmp"), - (re.compile("^Macromedia Flash"), ["flash"], ".flv"), - (re.compile("^tcpdump capture"), ["pcap"], ".pcap"), - ] - - def __init__(self): - super().__init__() - - self._state = None - self._base_url = None - self._test_path = None - self._timeout = None - self._token_name = None - self._token = None - self._username = None - self._password = None - self._oauth_token_url = None - self._client_id = None - self._client_secret = None - self._access_token = None - self.access_token_retry = True - - def encrypt_state(self, encrypt_var, token_name): - """Handle encryption of token. - :param encrypt_var: Variable needs to be encrypted - :return: encrypted variable - """ - self.debug_print(HTTP_ENCRYPT_TOKEN.format(token_name)) # nosemgrep - return encryption_helper.encrypt(encrypt_var, self.get_asset_id()) - - def decrypt_state(self, decrypt_var, token_name): - """Handle decryption of token. - :param decrypt_var: Variable needs to be decrypted - :return: decrypted variable - """ - self.debug_print(HTTP_DECRYPT_TOKEN.format(token_name)) # nosemgrep - return encryption_helper.decrypt(decrypt_var, self.get_asset_id()) - - def _get_error_message_from_exception(self, e): - """This function is used to get appropriate error message from the exception. - :param e: Exception object - :return: error message - """ - error_msg = HTTP_ERR_MSG - error_code = HTTP_ERR_CODE_MSG - try: - if hasattr(e, "args"): - if len(e.args) > 1: - error_code = e.args[0] - error_msg = e.args[1] - elif len(e.args) == 1: - error_code = HTTP_ERR_CODE_MSG - error_msg = e.args[0] - except Exception as ex: - self.error_print("Exception occurred.", ex) - - return f"Error Code: {error_code}. Error Message: {error_msg}" - - def _validate_integers(self, action_result, parameter, key, allow_zero=False): - """This method is to check if the provided input parameter value - is a non-zero positive integer and returns the integer value of the parameter itself. - :param action_result: Action result or BaseConnector object - :param parameter: input parameter - :return: integer value of the parameter or None in case of failure - """ - try: - if not float(parameter).is_integer(): - self.set_status(phantom.APP_ERROR, HTTP_VALIDATE_INTEGER_MSG.format(key=key)) - return None - parameter = int(parameter) - - except Exception as ex: - self.error_print("Exception occurred.", ex) - self.set_status(phantom.APP_ERROR, HTTP_VALIDATE_INTEGER_MSG.format(key=key)) - return None - - if parameter < 0: - self.set_status( - phantom.APP_ERROR, - f"Please provide a valid non-negative integer value in the {key} parameter", - ) - return None - if not allow_zero and parameter == 0: - self.set_status( - phantom.APP_ERROR, - f"Please provide a positive integer value in the {key} parameter", - ) - return None - - return parameter - - def initialize(self): - self._state = self.load_state() - - if not isinstance(self._state, dict): - self.debug_print("Resetting the state file with the default format") - self._state = {"app_version": self.get_app_json().get("app_version")} - - config = self.get_config() - self._base_url = config["base_url"].strip("/") - self._token_name = config.get("auth_token_name", "ph-auth-token") - self._token = config.get("auth_token") - - self._username = config.get("username") - self._password = config.get("password", "") - self._test_http_method = config.get("test_http_method", "get").lower() - - self._oauth_token_url = config.get("oauth_token_url") - if self._oauth_token_url: - self._oauth_token_url = self._oauth_token_url.strip("/") - self._client_id = config.get("client_id") - self._client_secret = config.get("client_secret") - self._access_token = self._state.get(HTTP_JSON_ACCESS_TOKEN) - - if "test_path" in config: - try: - if not config["test_path"].startswith("/"): - self._test_path = "/" + config["test_path"] - else: - self._test_path = config["test_path"] - except Exception as e: - error_message = self._get_error_message_from_exception(e) - return self.set_status( - phantom.APP_ERROR, - f"Given endpoint value is invalid: {error_message}", - ) - - if "timeout" in config: - self._timeout = self._validate_integers(self, config.get("timeout"), "timeout") - if self._timeout is None: - return self.get_status() - - parsed = urlparse(self._base_url) - - if not parsed.scheme or not parsed.hostname: - return self.set_status( - phantom.APP_ERROR, - f'Failed to parse URL ({self._base_url}). Should look like "http(s)://location/optional_path"', - ) - - # Make sure base_url isn't 127.0.0.1 - addr = parsed.hostname - try: - unpacked = socket.gethostbyname(addr) - except Exception as ex: - self.error_print("Exception occurred.", ex) - try: - packed = socket.inet_aton(addr) - unpacked = socket.inet_aton(packed) - except Exception as ex: - self.error_print("Exception occurred.", ex) - # gethostbyname can fail even when the addr is a hostname - # If that happens, I think we can assume that it isn't localhost - unpacked = "" - - if unpacked.startswith("127."): - return self.set_status(phantom.APP_ERROR, "Accessing 127.0.0.1 is not allowed") - - if self._state.get(HTTP_STATE_IS_ENCRYPTED): - try: - if self._access_token: - self._access_token = self.decrypt_state(self._access_token, "access") - except Exception as e: - self.error_print(HTTP_DECRYPTION_ERR, e) - return self.set_status(phantom.APP_ERROR, HTTP_DECRYPTION_ERR) - - return phantom.APP_SUCCESS - - def finalize(self): - try: - if self._access_token: - self._state[HTTP_JSON_ACCESS_TOKEN] = self.encrypt_state(self._access_token, "access") - self._state[HTTP_STATE_IS_ENCRYPTED] = True - except Exception as e: - self.error_print(HTTP_ENCRYPTION_ERR, e) - return self.set_status(phantom.APP_ERROR, HTTP_ENCRYPTION_ERR) - - self.save_state(self._state) - return phantom.APP_SUCCESS - - def _process_empty_reponse(self, response, action_result): - if 200 <= response.status_code < 400: - if response.headers.get("Content-Type") == "application/octet-stream": - return RetVal(phantom.APP_SUCCESS, "Response includes a file") - return RetVal(phantom.APP_SUCCESS, None) - return RetVal( - action_result.set_status(phantom.APP_ERROR, "Empty response and no information in the header"), - None, - ) - - def _process_html_response(self, response, action_result): - status_code = response.status_code - - try: - soup = BeautifulSoup(response.text, "html.parser") - for element in soup(["script", "style", "footer", "nav"]): - element.extract() - error_text = soup.text - split_lines = error_text.split("\n") - split_lines = [x.strip() for x in split_lines if x.strip()] - error_text = "\n".join(split_lines) - except Exception as ex: - self.error_print("Exception occurred.", ex) - error_text = "Cannot parse error details" - - response_data = error_text - if 200 <= response.status_code < 400: - return RetVal(phantom.APP_SUCCESS, response_data) - - message = f"Status Code: {status_code}. Data from server:\n{unquote_plus(error_text)}\n" - - message = message.replace("{", "{{").replace("}", "}}") - - return RetVal(action_result.set_status(phantom.APP_ERROR, message), response_data) - - def _process_json_response(self, response, action_result): - resp_json = {} - try: - if response: - resp_json = response.json() - except Exception as e: - error_message = self._get_error_message_from_exception(e) - self.error_print("Unable to parse the response into a dictionary", error_message) - return RetVal( - action_result.set_status( - phantom.APP_ERROR, - f"Unable to parse JSON response. Error: {error_message}", - ) - ) - - if 200 <= response.status_code < 400: - return RetVal(phantom.APP_SUCCESS, resp_json) - - message_template = "Error from server. Status Code: {0} Data from server: {1}" - message = message_template.format(response.status_code, response.text.replace("{", "{{").replace("}", "}}")) - - error_field_name = "error" - message_field_name = "message" - if error_field_name in resp_json: - error_field = resp_json[error_field_name] - - if isinstance(error_field, dict) and message_field_name in error_field: - error_message = error_field[message_field_name] - else: - error_message = error_field - - message = message_template.format(response.status_code, error_message) - - return RetVal(action_result.set_status(phantom.APP_ERROR, message), resp_json) - - def _process_xml_response(self, r, action_result): - resp_json = None - try: - if r.text: - resp_json = xmltodict.parse(r.text) - except Exception as e: - error_message = self._get_error_message_from_exception(e) - return RetVal( - action_result.set_status( - phantom.APP_ERROR, - f"Unable to parse XML response. Error: {error_message}", - ) - ) - - if 200 <= r.status_code < 400: - return RetVal(phantom.APP_SUCCESS, resp_json) - - message = "Error from server. Status Code: {} Data from server: {}".format(r.status_code, r.text.replace("{", "{{").replace("}", "}}")) - - return RetVal(action_result.set_status(phantom.APP_ERROR, message), resp_json) - - def _process_response(self, r, action_result): - if r.headers.get("Content-Type", "") == "application/octet-stream": - r_text = "" - else: - r_text = r.text - - if hasattr(action_result, "add_debug_data"): - action_result.add_debug_data({"r_status_code": r.status_code}) - action_result.add_debug_data({"r_text": r_text}) - action_result.add_debug_data({"r_headers": r.headers}) - - if not r_text: - return self._process_empty_reponse(r, action_result) - if "xml" in r.headers.get("Content-Type", ""): - return self._process_xml_response(r, action_result) - if "json" in r.headers.get("Content-Type", "") or "javascript" in r.headers.get("Content-Type", ""): - return self._process_json_response(r, action_result) - if "html" in r.headers.get("Content-Type", ""): - return self._process_html_response(r, action_result) - if 200 <= r.status_code < 400: - return RetVal(phantom.APP_SUCCESS, r_text) - - message = "Can't process response from server. Status Code: {} Data from server: {}".format( - r.status_code, r.text.replace("{", "{{").replace("}", "}}") - ) - - return RetVal(action_result.set_status(phantom.APP_ERROR, message), r.text) - - def _make_http_call( - self, - action_result, - endpoint="", - method="get", - headers=None, - params=None, - verify=False, - data=None, - files=None, - use_default_endpoint=False, - ): - auth = None - headers = {} if not headers else headers - access_token = "" - if self._username: - self.save_progress("Using HTTP Basic auth to authenticate") - auth = (self._username, self._password) - elif self._oauth_token_url and self._client_id: - self.save_progress("Using OAuth to authenticate") - access_token = self._generate_api_token(action_result) - if not access_token: - return action_result.get_status(), None - headers["Authorization"] = f"Bearer {access_token}" - elif self._token_name: - self.save_progress("Using provided token to authenticate") - if self._token and self._token_name not in headers: - headers[self._token_name] = self._token - else: - return action_result.set_status(phantom.APP_ERROR, "No authentication method set"), None - - if self.get_action_identifier() == "get_file" or self.get_action_identifier() == "put_file": - url = endpoint - if not use_default_endpoint: - auth = None - else: - url = self._base_url + endpoint - - try: - r = requests.request( - method=method, - url=url, - auth=auth, - params=params, - data=UnicodeDammit(data).unicode_markup.encode("utf-8") if isinstance(data, str) else data, - verify=verify, - headers=headers, - files=files, - timeout=self._timeout, - ) - - except Exception as e: - error_message = self._get_error_message_from_exception(e) - return action_result.set_status( - phantom.APP_ERROR, - f"Error Connecting to server. Details: {error_message}", - ), None - - # fetch new token if old one has expired - if access_token and r.status_code == 401 and self.access_token_retry: - self.save_progress(f"Got error: {r.status_code}") - self._access_token = None - self._state.pop("access_token") - self.access_token_retry = False # make it to false to avoid getting access token after one time (prevents recursive loop) - return self._make_http_call( - action_result, - endpoint=endpoint, - method=method, - headers=headers, - params=params, - verify=verify, - data=data, - files=files, - use_default_endpoint=use_default_endpoint, - ) - - # Return success for get headers action as it returns empty response body - if self.get_action_identifier() == "http_head" and r.status_code == 200: - resp_data = {"method": method.upper(), "location": url} - try: - resp_data["response_headers"] = dict(r.headers) - except Exception: - pass - action_result.add_data(resp_data) - action_result.update_summary({"status_code": r.status_code, "reason": r.reason}) - self.access_token_retry = True - return action_result.set_status(phantom.APP_SUCCESS), None - - ret_val, parsed_body = self._process_response(r, action_result) - - if self.get_action_identifier() == "get_file" or self.get_action_identifier() == "put_file": - return ret_val, r - - content_type = r.headers.get("Content-Type", "") - if "json" not in content_type and "javascript" not in content_type: - response_body = r.text - else: - response_body = parsed_body - - resp_data = { - "method": method.upper(), - "location": url, - "parsed_response_body": parsed_body, - "response_body": response_body, - } - try: - resp_data["response_headers"] = dict(r.headers) - except Exception: - pass - action_result.add_data(resp_data) - action_result.update_summary({"status_code": r.status_code, "reason": r.reason}) - - if self.get_action_identifier() == "test_connectivity": - self.save_progress(f"Got status code {r.status_code}") - - if phantom.is_fail(ret_val): - return ret_val, None - - return action_result.set_status(phantom.APP_SUCCESS), None - - def _get_headers(self, action_result, headers): - # Not to be confused with the action "get headers" - if headers is None: - return RetVal(phantom.APP_SUCCESS) - - if hasattr(headers, "decode"): - headers = headers.decode("utf-8") - - try: - headers = json.loads(headers) - except Exception as e: - error_message = self._get_error_message_from_exception(e) - return RetVal( - action_result.set_status( - phantom.APP_ERROR, - f"Failed to parse headers as JSON object. error: {error_message}, headers: {headers}", - ) - ) - - return RetVal(phantom.APP_SUCCESS, headers) - - def _generate_api_token(self, action_result, new_token=False): - """This function is used to generate token - - :param action_result: object of ActionResult class - :param new_token: boolean. weather to fetch new token or fetch old access token - :return: access token - """ - self.save_progress("Fetching access token") - - if self._access_token and not new_token: - self.save_progress("Using old token") - return self._access_token - - payload = {"grant_type": "client_credentials"} - - self.save_progress("Fetching new token") - # Querying endpoint to generate token - response = requests.post( - self._oauth_token_url, - auth=HTTPBasicAuth(self._client_id, self._client_secret), # nosemgrep - data=payload, - timeout=DEFAULT_REQUEST_TIMEOUT, - ) - if response.status_code not in [200, 201]: - action_result.set_status( - phantom.APP_ERROR, - f"Error fetching token from {self._oauth_token_url}. Server returned {response.status_code}", - ) - return None - - try: - self._access_token = json.loads(response.text).get(HTTP_JSON_ACCESS_TOKEN) - except Exception: - action_result.set_status( - phantom.APP_ERROR, - "Error parsing response from server while fetching token", - ) - return None - - if not self._access_token: - action_result.set_status(phantom.APP_ERROR, "Access token not found in response body") - return None - - self._state[HTTP_JSON_ACCESS_TOKEN] = self._access_token - return self._access_token - - def _handle_test_connectivity(self, param): - self.save_progress(f"In action handler for: {self.get_action_identifier()}") - action_result = self.add_action_result(ActionResult(dict(param))) - test_path = self._test_path - - if test_path: - self.save_progress(f"Querying base url, {self._base_url}{self._test_path}, to test credentials") - ret_val, _ = self._make_http_call(action_result, test_path, method=self._test_http_method) - else: - self.save_progress(f"Querying base url, {self._base_url}, to test credentials") - ret_val, _ = self._make_http_call(action_result, method=self._test_http_method) - - if phantom.is_fail(ret_val): - self.save_progress("Test Connectivity Failed") - else: - self.save_progress("Test Connectivity Passed") - - return ret_val - - def _verb(self, param, method): - # These three are all the same thing except for the 'method' - self.save_progress(f"In action handler for: {self.get_action_identifier()}") - action_result = self.add_action_result(ActionResult(dict(param))) - - location = param["location"] - body = param.get("body") - - if not location.startswith("/"): - location = "/" + location - - if hasattr(location, "decode"): - location = location.decode("utf-8") - - ret_val, headers = self._get_headers(action_result, param.get("headers")) - - if phantom.is_fail(ret_val): - return ret_val - - ret_val, _ = self._make_http_call( - action_result, - endpoint=location, - method=method, - headers=headers, - verify=param.get("verify_certificate", False), - data=body, - ) - return ret_val - - def _handle_get_file(self, param, method): - action_result = ActionResult(dict(param)) - self.add_action_result(action_result) - - file_path = param[HTTP_JSON_FILE_PATH] - file_path = file_path.strip(" ") - file_path = file_path.rstrip("/") - hostname = param.get(HTTP_JSON_HOSTNAME) - if not hostname: - hostname = self._base_url - use_default_endpoint = True - else: - use_default_endpoint = False - hostname = hostname.strip(" ") - hostname = hostname.strip("/") - - if file_path == "": - return action_result.set_status(phantom.APP_ERROR, HTTP_INVALID_PATH_ERR) - - encoded_file_path = quote(file_path) - endpoint = f"{hostname}/{file_path}" - validate_endpoint = f"{hostname}/{encoded_file_path}" - - if not validators.url(validate_endpoint): - return action_result.set_status(phantom.APP_ERROR, HTTP_INVALID_URL_ERR) - file_name = file_path.split("/")[-1] - file_name = unquote_plus(file_name) - try: - ret_val, r = self._make_http_call( - action_result, - endpoint=endpoint, - method=method, - verify=param.get("verify_certificate", False), - use_default_endpoint=use_default_endpoint, - ) - except Exception as e: - err = self._get_error_message_from_exception(e) - return action_result.set_status(phantom.APP_ERROR, HTTP_SERVER_CONNECTION_ERR_MSG.format(error=err)) - - if phantom.is_fail(ret_val): - return action_result.get_status() - - if r.status_code == 200: - return self._save_file_to_vault(action_result, r, file_name) - else: - return action_result.set_status( - phantom.APP_ERROR, - HTTP_SERVER_CONNECTION_ERR_MSG.format(error=r.status_code), - ) - - def _handle_put_file(self, param, method): - action_result = ActionResult(dict(param)) - self.add_action_result(action_result) - - input_filename = param.get(HTTP_JSON_FILE_NAME, "") - - # fetching phantom vault details - try: - success, message, vault_meta_info = ph_rules.vault_info(vault_id=param[HTTP_JSON_VAULT_ID]) - if not success or not vault_meta_info: - error_msg = f" Error Details: {unquote_plus(message)}" if message else "" - return action_result.set_status( - phantom.APP_ERROR, - f"{HTTP_UNABLE_TO_RETRIEVE_VAULT_ITEM_ERR_MSG}.{error_msg}", - ) - vault_meta_info = list(vault_meta_info) - except Exception as e: - err = self._get_error_message_from_exception(e) - return action_result.set_status( - phantom.APP_ERROR, - f"{HTTP_UNABLE_TO_RETRIEVE_VAULT_ITEM_ERR_MSG}. {err}", - ) - - file_is_present = False - if input_filename: - input_filename = input_filename.strip() - for vault_data in vault_meta_info: - if vault_data.get("name") == input_filename: - file_is_present = True - break - if not file_is_present: - return action_result.set_status(phantom.APP_ERROR, HTTP_ERR_FILENAME_NOT_IN_VAULT) - else: - vault_meta_info[0]["name"] = input_filename - - # phantom vault file path - vault_path = vault_meta_info[0].get("path") - try: - f = open(vault_path, "rb") - files = {"file": f} - except Exception as e: - return action_result.set_status(phantom.APP_ERROR, self._get_error_message_from_exception(e)) - - # phantom vault file name - dest_file_name = vault_meta_info[0].get("name") - file_dest = param[HTTP_JSON_FILE_DEST] - endpoint = param.get(HTTP_JSON_HOST) - if not endpoint: - endpoint = self._base_url - use_default_endpoint = True - else: - use_default_endpoint = False - - file_dest = file_dest.strip("/") - endpoint = endpoint.rstrip("/") - - # encoding input file name - dest_file_name = dest_file_name.strip("/") - validate_dest_file_name = quote(dest_file_name) - - # Returning an error if the filename is included in the file_destination path - if dest_file_name in file_dest: - return action_result.set_status(phantom.APP_ERROR, HTTP_EXCLUDE_FILENAME_ERR_MSG) - - destination_path = f"{endpoint}/{file_dest}/{dest_file_name}" - validate_destination_path = f"{endpoint}/{file_dest}/{validate_dest_file_name}" - - if not validators.url(validate_destination_path): - return action_result.set_status(phantom.APP_ERROR, HTTP_INVALID_URL_ERR) - - params = {"file_path": file_dest} - - try: - ret_val, response = self._make_http_call( - action_result, - endpoint=destination_path, - method=method, - params=params, - verify=param.get("verify_certificate", False), - files=files, - use_default_endpoint=use_default_endpoint, - ) - except Exception as e: - err = self._get_error_message_from_exception(e) - return action_result.set_status(phantom.APP_ERROR, HTTP_SERVER_CONNECTION_ERR_MSG.format(error=err)) - finally: - f.close() - - if response.status_code == 200: - summary = {"file_sent": destination_path} - action_result.update_summary(summary) - return action_result.set_status(phantom.APP_SUCCESS) - else: - return action_result.set_status( - phantom.APP_ERROR, - HTTP_SERVER_CONNECTION_ERR_MSG.format(error=response.status_code), - ) - - def _save_file_to_vault(self, action_result, response, file_name): - # Create a tmp directory on the vault partition - - guid = uuid.uuid4() - if hasattr(Vault, "get_vault_tmp_dir"): - temp_dir = Vault.get_vault_tmp_dir() - else: - temp_dir = "/vault/tmp" - - local_dir = temp_dir + f"/{guid}" - self.save_progress(f"Using temp directory: {guid}") - try: - os.makedirs(local_dir) - except Exception as e: - return action_result.set_status( - phantom.APP_ERROR, - f"Unable to create temporary folder {temp_dir}.", - e, - ) - file_path = f"{local_dir}/{file_name}" - # open and download the file - with open(file_path, "wb") as f: - f.write(response.content) - contains = [] - file_ext = "" - magic_str = magic.from_file(file_path) - for regex, cur_contains, extension in self.MAGIC_FORMATS: - if regex.match(magic_str): - contains.extend(cur_contains) - if not file_ext: - file_ext = extension - file_name = f"{file_name}{file_ext}" - - # move the file to the vault - status, vault_ret_message, vault_id = ph_rules.vault_add( - file_location=file_path, - container=self.get_container_id(), - file_name=file_name, - metadata={"contains": contains}, - ) - curr_data = {} - - if status: - curr_data[phantom.APP_JSON_VAULT_ID] = vault_id - curr_data[phantom.APP_JSON_NAME] = file_name - if contains: - curr_data["file_type"] = ",".join(contains) - action_result.add_data(curr_data) - action_result.update_summary(curr_data) - action_result.set_status(phantom.APP_SUCCESS, "File successfully retrieved and added to vault") - else: - action_result.set_status(phantom.APP_ERROR, phantom.APP_ERR_FILE_ADD_TO_VAULT) - action_result.append_to_message(vault_ret_message) - - # remove the /tmp/<> temporary directory - shutil.rmtree(local_dir) - - return action_result.get_status() - - def _handle_http_get(self, param): - return self._verb(param, "get") - - def _handle_http_post(self, param): - return self._verb(param, "post") - - def _handle_http_patch(self, param): - return self._verb(param, "patch") - - def _handle_http_put(self, param): - return self._verb(param, "put") - - def _handle_http_delete(self, param): - return self._verb(param, "delete") - - def _handle_http_head(self, param): - return self._verb(param, "head") - - def _handle_http_options(self, param): - return self._verb(param, "options") - - def handle_action(self, param): - ret_val = phantom.APP_SUCCESS - - action_id = self.get_action_identifier() - - self.debug_print("action_id", self.get_action_identifier()) - - if action_id == "test_connectivity": - ret_val = self._handle_test_connectivity(param) - - elif action_id == "http_get": - ret_val = self._handle_http_get(param) - - elif action_id == "http_post": - ret_val = self._handle_http_post(param) - - elif action_id == "http_put": - ret_val = self._handle_http_put(param) - - elif action_id == "http_patch": - ret_val = self._handle_http_patch(param) - - elif action_id == "http_delete": - ret_val = self._handle_http_delete(param) - - elif action_id == "http_head": - ret_val = self._handle_http_head(param) - - elif action_id == "http_options": - ret_val = self._handle_http_options(param) - - elif action_id == "get_file": - ret_val = self._handle_get_file(param, "get") - - elif action_id == "put_file": - ret_val = self._handle_put_file(param, "post") - - return ret_val - - -if __name__ == "__main__": - import argparse - - import pudb - - pudb.set_trace() - - argparser = argparse.ArgumentParser() - - argparser.add_argument("input_test_json", help="Input Test JSON file") - argparser.add_argument("-u", "--username", help="username", required=False) - argparser.add_argument("-p", "--password", help="password", required=False) - argparser.add_argument( - "-v", - "--verify", - action="store_true", - help="verify", - required=False, - default=False, - ) - - args = argparser.parse_args() - verify = args.verify - session_id = None - verify = args.verify - - if args.username and args.password: - login_url = BaseConnector._get_phantom_base_url() + "login" - try: - print("Accessing the Login page") - r = requests.get(login_url, verify=verify, timeout=DEFAULT_REQUEST_TIMEOUT) - csrftoken = r.cookies["csrftoken"] - data = { - "username": args.username, - "password": args.password, - "csrfmiddlewaretoken": csrftoken, - } - headers = { - "Cookie": f"csrftoken={csrftoken}", - "Referer": login_url, - } - - print("Logging into Platform to get the session id") - r2 = requests.post( - login_url, - verify=verify, - data=data, - headers=headers, - timeout=DEFAULT_REQUEST_TIMEOUT, - ) - session_id = r2.cookies["sessionid"] - - except Exception as e: - print(f"Unable to get session id from the platform. Error: {e!s}") - sys.exit(1) - - if len(sys.argv) < 2: - print("No test json specified as input") - sys.exit(0) - - with open(sys.argv[1]) as f: - in_json = f.read() - in_json = json.loads(in_json) - print(json.dumps(in_json, indent=4)) - - connector = HttpConnector() - connector.print_progress_message = True - - if session_id is not None: - in_json["user_session_token"] = session_id - - ret_val = connector._handle_action(json.dumps(in_json), None) - print(json.dumps(json.loads(ret_val), indent=4)) - - sys.exit(0) diff --git a/http_consts.py b/http_consts.py deleted file mode 100644 index 9122885..0000000 --- a/http_consts.py +++ /dev/null @@ -1,46 +0,0 @@ -# File: http_consts.py -# -# Copyright (c) 2016-2025 Splunk Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed under -# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, -# either express or implied. See the License for the specific language governing permissions -# and limitations under the License. - -HTTP_VALIDATE_INTEGER_MSG = "Please provide a valid integer value in the {key} parameter" -HTTP_ERR_CODE_MSG = "Error code unavailable" -HTTP_ERR_MSG = "Unknown error occurred. Please check the asset configuration and|or action parameters" -TYPE_ERR_MSG = "Error occurred while connecting to the HTTP server. Please check the asset configuration and|or the action parameters" -HTTP_SERVER_CONNECTION_ERR_MSG = "Error Connecting to file server. Error:{error}" -HTTP_ERR_FILE_ADD_TO_VAULT = "Error while adding the file to Vault" -HTTP_FILE_NOT_FOUND_ERR_MSG = "Please verify the file destination and make sure the filename is not included in it" -HTTP_UNABLE_TO_RETRIEVE_VAULT_ITEM_ERR_MSG = "Unable to retrieve vault item details" -HTTP_EXCLUDE_FILENAME_ERR_MSG = "Error: Do not include filename in the file destination" -HTTP_PUT_FILE_ERR_MSG = "Error putting file. {error}" -HTTP_JSON_FILE_PATH = "file_path" -HTTP_INVALID_PATH_ERR = "Invalid file path, please enter a valid file path" -HTTP_INVALID_URL_ERR = "Malformed URL, please enter hostname and filepath in proper format" - -HTTP_JSON_HOSTNAME = "hostname" -HTTP_JSON_ACCESS_TOKEN = "access_token" -HTTP_JSON_HOST = "host" -HTTP_JSON_VAULT_ID = "vault_id" -HTTP_JSON_FILE_DEST = "file_destination" -HTTP_JSON_FILE_NAME = "file_name" -UNKNOWN_ERR_MSG = "UNKNOWN ERR MSG" -UNKNOWN_ERR_CODE_MSG = "UNKNOWN ERR CODE MSG" -HTTP_ERR_FILENAME_NOT_IN_VAULT = "Could not find file with specified filename in vault, please provide a valid filename" -DEFAULT_REQUEST_TIMEOUT = 30 # in seconds -HTTP_STATE_IS_ENCRYPTED = "is_encrypted" - -# For encryption and decryption -HTTP_ENCRYPT_TOKEN = "Encrypting the {} token" -HTTP_DECRYPT_TOKEN = "Decrypting the {} token" -HTTP_ENCRYPTION_ERR = "Error occurred while encrypting the state file" -HTTP_DECRYPTION_ERR = "Error occurred while decrypting the state file" diff --git a/logo.svg b/logo.svg new file mode 100644 index 0000000..843059c --- /dev/null +++ b/logo.svg @@ -0,0 +1,7 @@ + + + + Layer 1 + + + diff --git a/logo_dark.svg b/logo_dark.svg new file mode 100644 index 0000000..6c702fc --- /dev/null +++ b/logo_dark.svg @@ -0,0 +1,7 @@ + + + + Layer 1 + + + diff --git a/logo_splunk.svg b/logo_splunk.svg deleted file mode 100644 index c67198d..0000000 --- a/logo_splunk.svg +++ /dev/null @@ -1 +0,0 @@ -Asset 1 diff --git a/logo_splunk_dark.svg b/logo_splunk_dark.svg deleted file mode 100644 index 9051579..0000000 --- a/logo_splunk_dark.svg +++ /dev/null @@ -1 +0,0 @@ -Asset 2 diff --git a/manual_readme_content.md b/manual_readme_content.md deleted file mode 100644 index c70b8a2..0000000 --- a/manual_readme_content.md +++ /dev/null @@ -1,10 +0,0 @@ -For security reasons, accessing 127.0.0.1 is not allowed. - -This app requires access to port 80(for request send over HTTP) or port 443(for request send over -HTTPS) on your Phantom host(s) in order to function. - -**Authentication is carried out in following priority order** - -1. Basic Auth (username and password) -1. OAuth (oauth token url, client id and client secret) -1. Provided Auth token (auth_token_name, auth_token) diff --git a/pyproject.toml b/pyproject.toml index a816e31..1eca952 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,37 +1,101 @@ -# Ruff linting +[project] +name = "http" +version = "1.0.0" +description = "app" +license = "Copyright (c) 2025 Splunk Inc." +requires-python = ">=3.9, <3.14" +authors = [ +] +dependencies = [ + "splunk-soar-sdk>=1.0.0b55", +] + +[dependency-groups] +dev = [ + "coverage>=7.6.7,<8", + "mypy>=1.2.0,<2", + "pre-commit>=4.2.0,<5", + "pytest>=7.4.2,<8", + "pytest-mock>=3.14.0,<4", + "pytest-watch>=4.2.0,<5", + "ruff>=0.11.6,<1", +] + +[tool.soar.app] +main_module = "src.app:app" + +[[tool.uv.index]] +url = "https://pypi.python.org/simple" + +[tool.uv] +environments = [ + "sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.9'", + "sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.9'", + "sys_platform == 'darwin' and platform_machine == 'x86_64' and python_version == '3.9'", + "sys_platform == 'darwin' and platform_machine == 'arm64' and python_version == '3.9'", + "sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.13'", + "sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.13'", + "sys_platform == 'darwin' and platform_machine == 'x86_64' and python_version == '3.13'", + "sys_platform == 'darwin' and platform_machine == 'arm64' and python_version == '3.13'", +] +required-environments = [ + "sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.9'", + "sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.13'", +] + [tool.ruff] -line-length = 145 +output-format = "full" # +fix = true target-version = "py39" [tool.ruff.lint] -select = [ # Auto-fixable rules only - "I", # isort +select = [ + "ERA", # commented out code + "YTT", # bad use of sys.version_info + "S", # security issues + "B", # bugbear + "A", # builtins + "DTZ", # datetime footguns + "T10", # breakpoints + "ISC", # implicit string concatenation + "PT", # pytest style + "SIM", # simplify + "PTH", # use pathlib + "E", # pycodestyle errors + "F", # pyflakes + "W", # pycodestyle warnings + "PL", # pylint "UP", # pyupgrade - "F401", # unused imports - "RUF" # ruff rules + "RUF", # ruff's own rules ] - ignore = [ - "RUF012", # Not auto-fixable (remove eventually) - "RUF001" # Not auto-fixable (remove eventually) + "E402", # Module level import not at top of file. + "E501", # Line too long. Format covers this. + "PT006", # Opinions about types for pytest.parametrize + "PT007", # Opinions about types for pytest.parametrize + "PTH123", # Allow builtin open() + "PLR", # pylint refactors ] -[tool.ruff.lint.per-file-ignores] -"__init__.py" = ["F401", "UP035"] # __init__.py file exceptions +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" -# Keep complexity below 28 -[tool.ruff.lint.mccabe] -max-complexity = 28 +[tool.ruff.lint.per-file-ignores] +"tests/**/*" = [ + "ANN", # Disable flake8-annotations rules for test files + "S", # Disable flake8-bandit rules for test files +] +"src/**/*" = [ + "PT", # Disable pytest rules for src files +] -[tool.ruff.lint.isort] -combine-as-imports = true -lines-after-imports = 2 +[tool.ruff.lint.pyupgrade] +# Preserve types, even if a file imports `from __future__ import annotations`. +# This is necessary since we rely on runtime type annotations in our code and in pydantic +keep-runtime-typing = true [tool.ruff.format] -quote-style = "double" -indent-style = "space" -skip-magic-trailing-comma = false -line-ending = "auto" +docstring-code-format = true # HTML linting [tool.djlint] @@ -58,22 +122,17 @@ config = [ "r/typescript.react.security.audit.react-dangerouslysetinnerhtml.react-dangerouslysetinnerhtml" # TypeScript React security rule ] ignore-patterns = [ - "node_modules/", - "build/", "dist/", "vendor/", "env/", ".env/", "venv/", ".venv/", - ".tox/", - "*.min.js", "test/", "tests/", - "*_test.go", ".semgrep", "wheels/", ".html", - "*.md", - "*.svg" + ".md", + ".svg" ] diff --git a/release_notes/2.0.10.md b/release_notes/2.0.10.md deleted file mode 100644 index 953a3e3..0000000 --- a/release_notes/2.0.10.md +++ /dev/null @@ -1,6 +0,0 @@ -**HTTP Release Notes - Published by Splunk July 27, 2017** - - -**Version 2.0.10 - Released July 27, 2017** - -* 3.0 compatibility update release only diff --git a/release_notes/2.0.9.md b/release_notes/2.0.9.md deleted file mode 100644 index c72019b..0000000 --- a/release_notes/2.0.9.md +++ /dev/null @@ -1,6 +0,0 @@ -**HTTP Release Notes - Published by Splunk December 9, 2016** - - -**Version 2.0.9 - Released December 9, 2016** - -* Initial release diff --git a/release_notes/2.1.10.md b/release_notes/2.1.10.md deleted file mode 100644 index ee416a1..0000000 --- a/release_notes/2.1.10.md +++ /dev/null @@ -1,6 +0,0 @@ -**HTTP Release Notes - Published by Splunk February 23, 2018** - - -**Version 2.1.10 - Released February 23, 2018** - -* Bug Fix to check for javascript in response headers diff --git a/release_notes/2.1.13.md b/release_notes/2.1.13.md deleted file mode 100644 index 9e5ef4b..0000000 --- a/release_notes/2.1.13.md +++ /dev/null @@ -1,7 +0,0 @@ -**HTTP Release Notes - Published by Splunk July 25, 2018** - - -**Version 2.1.13 - Released July 25, 2018** - -* Bug fix on base URL validation with DNS lookups -* Bug fix on test connectivity when Phantom authorization token is used diff --git a/release_notes/2.1.15.md b/release_notes/2.1.15.md deleted file mode 100644 index 9ba0c1d..0000000 --- a/release_notes/2.1.15.md +++ /dev/null @@ -1,6 +0,0 @@ -**HTTP Release Notes - Published by Splunk September 12, 2018** - - -**Version 2.1.15 - Released September 12, 2018** - -* Added option to support a specific path used by Test Connectivity diff --git a/release_notes/2.1.7.md b/release_notes/2.1.7.md deleted file mode 100644 index f8e6555..0000000 --- a/release_notes/2.1.7.md +++ /dev/null @@ -1,8 +0,0 @@ -**HTTP Release Notes - Published by Splunk January 03, 2018** - - -**Version 2.1.7 - Released January 03, 2018** - -* Added Test Connectivity -* Added Timeout parameter for HTTP calls in the asset settings -* Added support for running requests without password configured diff --git a/release_notes/3.0.15.md b/release_notes/3.0.15.md deleted file mode 100644 index 7d9f622..0000000 --- a/release_notes/3.0.15.md +++ /dev/null @@ -1,7 +0,0 @@ -**HTTP Release Notes - Published by Splunk December 21, 2020** - - -**Version 3.0.15 - Released December 21, 2020** - -* Bug fixes for the 'post data' action [PAPP-16015] -* Compatibility changes for Python 3 support diff --git a/release_notes/3.0.22.md b/release_notes/3.0.22.md deleted file mode 100644 index 463bc65..0000000 --- a/release_notes/3.0.22.md +++ /dev/null @@ -1,8 +0,0 @@ -**HTTP Release Notes - Published by Splunk January 06, 2021** - - -**Version 3.0.22 - Released January 06, 2021** - -* Bug fixes in the 'get headers' action -* Improved output messages -* Updated app documentation diff --git a/release_notes/3.1.3.md b/release_notes/3.1.3.md deleted file mode 100644 index e805421..0000000 --- a/release_notes/3.1.3.md +++ /dev/null @@ -1,6 +0,0 @@ -**HTTP Release Notes - Published by Splunk March 19, 2021** - - -**Version 3.1.3 - Released March 19, 2021** - -* Added French character support for the 'post data' action diff --git a/release_notes/3.2.3.md b/release_notes/3.2.3.md deleted file mode 100644 index 419f28f..0000000 --- a/release_notes/3.2.3.md +++ /dev/null @@ -1,6 +0,0 @@ -**HTTP Release Notes - Published by Splunk October 19, 2021** - - -**Version 3.2.3 - Released October 19, 2021** - -* Added OAuth Authorization [PAPP-17274] diff --git a/release_notes/3.3.2.md b/release_notes/3.3.2.md deleted file mode 100644 index d297497..0000000 --- a/release_notes/3.3.2.md +++ /dev/null @@ -1,7 +0,0 @@ -**HTTP Release Notes - Published by Splunk January 11, 2022** - - -**Version 3.3.2 - Released January 11, 2022** - -* Added 'get file' and 'put file' actions [PAPP-16071] -* Marked the app as FIPS Compliant [PAPP-21537] \ No newline at end of file diff --git a/release_notes/3.4.1.md b/release_notes/3.4.1.md deleted file mode 100644 index 675139c..0000000 --- a/release_notes/3.4.1.md +++ /dev/null @@ -1,7 +0,0 @@ -**HTTP Release Notes - Published by Splunk February 03, 2022** - - -**Version 3.4.1 - Released February 03, 2022** - -* Added new asset configuration option to enable the 'test connectivity' action to send non-GET requests [PAPP_23570] -* Remove unwanted line breaks from output [PAPP_23533] \ No newline at end of file diff --git a/release_notes/3.5.0.md b/release_notes/3.5.0.md deleted file mode 100644 index b39d3f7..0000000 --- a/release_notes/3.5.0.md +++ /dev/null @@ -1 +0,0 @@ -* Updated App documentation [PAPP-24696] \ No newline at end of file diff --git a/release_notes/3.6.0.md b/release_notes/3.6.0.md deleted file mode 100644 index 5e8109a..0000000 --- a/release_notes/3.6.0.md +++ /dev/null @@ -1,2 +0,0 @@ -* Added basic authentication support for 'get file' and 'put file' actions [PAPP-26427] -* Updated the processing for 201 status code while using OAuth [PAPP-26849] diff --git a/release_notes/3.7.0.md b/release_notes/3.7.0.md deleted file mode 100644 index ac0bb26..0000000 --- a/release_notes/3.7.0.md +++ /dev/null @@ -1 +0,0 @@ -* Changed the 'body' parameter to optional for the 'post data' action [PAPP-26977] \ No newline at end of file diff --git a/release_notes/3.7.1.md b/release_notes/3.7.1.md deleted file mode 100644 index 7549a0c..0000000 --- a/release_notes/3.7.1.md +++ /dev/null @@ -1 +0,0 @@ -* Updated requests and certifi dependencies in order to use platform packages [PAPP-30822,PAPP-31096] \ No newline at end of file diff --git a/release_notes/3.7.2.md b/release_notes/3.7.2.md deleted file mode 100644 index 36310ae..0000000 --- a/release_notes/3.7.2.md +++ /dev/null @@ -1,3 +0,0 @@ -* Fix an error where OAuth parameters were handled incorrectly [PAPP-34185] -* Fix a bug where expired access tokens cannot be refreshed [PAPP-34185] -* Fix a bug where not all request parameters were honored while refrehsing the access token [PAPP-34185] \ No newline at end of file diff --git a/release_notes/3.7.3.md b/release_notes/3.7.3.md deleted file mode 100644 index 46df760..0000000 --- a/release_notes/3.7.3.md +++ /dev/null @@ -1,5 +0,0 @@ - -* Update Python dependencies for vulnerabilities, package updates, and platform built-in removals -* Update Python dependencies for Python 3.13 support -* Update NOTICE file with updated dependencies -* Apply pre-commit fixes diff --git a/release_notes/3.7.4.md b/release_notes/3.7.4.md deleted file mode 100644 index 62b2fde..0000000 --- a/release_notes/3.7.4.md +++ /dev/null @@ -1 +0,0 @@ -* PAPP-36204: Update vulnerability with validators wheel \ No newline at end of file diff --git a/release_notes/3.7.5.md b/release_notes/3.7.5.md deleted file mode 100644 index 43c84f0..0000000 --- a/release_notes/3.7.5.md +++ /dev/null @@ -1 +0,0 @@ -* Update Python version for 3.13 \ No newline at end of file diff --git a/release_notes/3.7.6.md b/release_notes/3.7.6.md deleted file mode 100644 index 3febf66..0000000 --- a/release_notes/3.7.6.md +++ /dev/null @@ -1 +0,0 @@ -* Update python 3.13 dependencies in the app json \ No newline at end of file diff --git a/release_notes/3.7.7.md b/release_notes/3.7.7.md deleted file mode 100644 index 04843ba..0000000 --- a/release_notes/3.7.7.md +++ /dev/null @@ -1 +0,0 @@ -* Fix python_version 3.13 format \ No newline at end of file diff --git a/release_notes/3.7.8.md b/release_notes/3.7.8.md deleted file mode 100644 index 80b66fa..0000000 --- a/release_notes/3.7.8.md +++ /dev/null @@ -1 +0,0 @@ -* Remove Python 3.13 from Python version \ No newline at end of file diff --git a/release_notes/unreleased.md b/release_notes/unreleased.md deleted file mode 100644 index fbcb2fd..0000000 --- a/release_notes/unreleased.md +++ /dev/null @@ -1 +0,0 @@ -**Unreleased** diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 0c48bd6..0000000 --- a/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -beautifulsoup4==4.9.1 -validators==0.35.0 -xmltodict==0.12.0 diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..54b6cbf --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,3 @@ +from . import app + +__ALL__ = [app] diff --git a/src/app.py b/src/app.py new file mode 100644 index 0000000..2391949 --- /dev/null +++ b/src/app.py @@ -0,0 +1,95 @@ +import requests +from soar_sdk.abstract import SOARClient +from soar_sdk.action_results import ActionOutput, OutputField +from soar_sdk.app import App +from soar_sdk.asset import AssetField, BaseAsset +from soar_sdk.exceptions import ActionFailure +from soar_sdk.logging import getLogger + +from soar_sdk.params import Param, Params + +logger = getLogger() + + +class Asset(BaseAsset): + base_url: str = AssetField( + required=True, + description="Base URL for making queries. (e.g. https://myservice/)", + ) + test_path: str = AssetField( + required=False, + description="Endpoint for test connectivity. (e.g. /some/specific/endpoint , appended to Base URL)", + ) + auth_token_name: str = AssetField( + required=False, + description="Type of authentication token", + default="ph-auth-token", + ) + auth_token: str = AssetField(required=False, description="Value of authentication token") + username: str = AssetField(required=False, description="Username (for HTTP basic auth)") + password: str = AssetField(required=False, description="Password (for HTTP basic auth)") + oauth_token_url: str = AssetField(required=False, description="URL to fetch oauth token from") + client_id: str = AssetField(required=False, description="Client ID (for OAuth)") + client_secret: str = AssetField(required=False, description="Client Secret (for OAuth)") + timeout: float = AssetField(required=False, description="Timeout for HTTP calls") + test_http_method: str = AssetField( + required=False, + description="HTTP Method for Test Connectivity", + default="GET", + value_list=[ + "GET", + "HEAD", + "POST", + "PUT", + "DELETE", + "OPTIONS", + "TRACE", + "PATCH", + ], + ) + + +app = App( + name="http", + app_type="generic", + logo="logo.svg", + logo_dark="logo_dark.svg", + product_vendor="Splunk Inc.", + product_name="http", + publisher="Splunk Inc.", + appid="dc312038-005f-470f-badb-8a353ba9bb5b", + fips_compliant=False, + asset_cls=Asset, +) + + +@app.test_connectivity() +def test_connectivity(soar: SOARClient, asset: Asset) -> None: + + logger.info("Action 'Test Connectivity' started.") + full_url = asset.base_url.rstrip("/") + if asset.test_path: + full_url = full_url + "/" + asset.test_path.lstrip("/") + logger.info(f"Querying base url, {full_url}, to test credentials.") + + + try: + response = requests.request( + method=asset.test_http_method, + url=full_url, + verify=False, + timeout=asset.timeout, + ) + logger.info(f"Got status code {response.status_code}.") + response.raise_for_status() + + + except requests.exceptions.RequestException as e: + logger.error(f"Test connectivity failed, error: {e}.") + raise ActionFailure(f"Test connectivity failed, details: {e}.") + + logger.info("Test connectivity passed!") + +if __name__ == "__main__": + app.cli() + diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..2bd2d6d --- /dev/null +++ b/uv.lock @@ -0,0 +1,921 @@ +version = 1 +revision = 2 +requires-python = ">=3.9, <3.14" +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin'", +] +supported-markers = [ + "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin'", +] +required-markers = [ + "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", +] + +[[package]] +name = "anyio" +version = "4.10.0" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "idna", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "sniffio", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "typing-extensions", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.10.0" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "soupsieve", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/69/daeee6d8f22c997e522cdbeb59641c4d31ab120aba0f2c799500f7456b7e/beautifulsoup4-4.10.0.tar.gz", hash = "sha256:c23ad23c521d818955a4151a67d81580319d4bf548d3d49f4223ae041ff98891", size = 399890, upload-time = "2021-09-08T00:16:07.708Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/bf/f0f194d3379d3f3347478bd267f754fc68c11cbf2fe302a6ab69447b1417/beautifulsoup4-4.10.0-py3-none-any.whl", hash = "sha256:9a315ce70049920ea4572a4055bc4bd700c940521d36fc858205ad4fcde149bf", size = 97398, upload-time = "2021-09-08T00:16:06.464Z" }, +] + +[[package]] +name = "bleach" +version = "6.2.0" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "webencodings", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/9a/0e33f5054c54d349ea62c277191c020c2d6ef1d65ab2cb1993f91ec846d1/bleach-6.2.0.tar.gz", hash = "sha256:123e894118b8a599fd80d3ec1a6d4cc7ce4e5882b1317a7e1ba69b56e95f991f", size = 203083, upload-time = "2024-10-29T18:30:40.477Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/55/96142937f66150805c25c4d0f31ee4132fd33497753400734f9dfdcbdc66/bleach-6.2.0-py3-none-any.whl", hash = "sha256:117d9c6097a7c3d22fd578fcd8d35ff1e125df6736f554da4e432fdd63f31e5e", size = 163406, upload-time = "2024-10-29T18:30:38.186Z" }, +] + +[[package]] +name = "certifi" +version = "2025.8.3" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.3" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", size = 207695, upload-time = "2025-08-09T07:55:36.452Z" }, + { url = "https://files.pythonhosted.org/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", size = 147153, upload-time = "2025-08-09T07:55:38.467Z" }, + { url = "https://files.pythonhosted.org/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", size = 160428, upload-time = "2025-08-09T07:55:40.072Z" }, + { url = "https://files.pythonhosted.org/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", size = 157627, upload-time = "2025-08-09T07:55:41.706Z" }, + { url = "https://files.pythonhosted.org/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", size = 152388, upload-time = "2025-08-09T07:55:43.262Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", size = 150077, upload-time = "2025-08-09T07:55:44.903Z" }, + { url = "https://files.pythonhosted.org/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", size = 161631, upload-time = "2025-08-09T07:55:46.346Z" }, + { url = "https://files.pythonhosted.org/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", size = 159210, upload-time = "2025-08-09T07:55:47.539Z" }, + { url = "https://files.pythonhosted.org/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", size = 153739, upload-time = "2025-08-09T07:55:48.744Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083, upload-time = "2025-08-09T07:55:57.582Z" }, + { url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295, upload-time = "2025-08-09T07:55:59.147Z" }, + { url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379, upload-time = "2025-08-09T07:56:00.364Z" }, + { url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018, upload-time = "2025-08-09T07:56:01.678Z" }, + { url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430, upload-time = "2025-08-09T07:56:02.87Z" }, + { url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600, upload-time = "2025-08-09T07:56:04.089Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" }, + { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" }, + { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" }, + { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" }, + { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" }, + { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" }, + { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" }, + { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, + { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, + { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, + { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, + { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, + { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ca/9a0983dd5c8e9733565cf3db4df2b0a2e9a82659fd8aa2a868ac6e4a991f/charset_normalizer-3.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05", size = 207520, upload-time = "2025-08-09T07:57:11.026Z" }, + { url = "https://files.pythonhosted.org/packages/39/c6/99271dc37243a4f925b09090493fb96c9333d7992c6187f5cfe5312008d2/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e", size = 147307, upload-time = "2025-08-09T07:57:12.4Z" }, + { url = "https://files.pythonhosted.org/packages/e4/69/132eab043356bba06eb333cc2cc60c6340857d0a2e4ca6dc2b51312886b3/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99", size = 160448, upload-time = "2025-08-09T07:57:13.712Z" }, + { url = "https://files.pythonhosted.org/packages/04/9a/914d294daa4809c57667b77470533e65def9c0be1ef8b4c1183a99170e9d/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7", size = 157758, upload-time = "2025-08-09T07:57:14.979Z" }, + { url = "https://files.pythonhosted.org/packages/b0/a8/6f5bcf1bcf63cb45625f7c5cadca026121ff8a6c8a3256d8d8cd59302663/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7", size = 152487, upload-time = "2025-08-09T07:57:16.332Z" }, + { url = "https://files.pythonhosted.org/packages/c4/72/d3d0e9592f4e504f9dea08b8db270821c909558c353dc3b457ed2509f2fb/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19", size = 150054, upload-time = "2025-08-09T07:57:17.576Z" }, + { url = "https://files.pythonhosted.org/packages/20/30/5f64fe3981677fe63fa987b80e6c01042eb5ff653ff7cec1b7bd9268e54e/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312", size = 161703, upload-time = "2025-08-09T07:57:20.012Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ef/dd08b2cac9284fd59e70f7d97382c33a3d0a926e45b15fc21b3308324ffd/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc", size = 159096, upload-time = "2025-08-09T07:57:21.329Z" }, + { url = "https://files.pythonhosted.org/packages/45/8c/dcef87cfc2b3f002a6478f38906f9040302c68aebe21468090e39cde1445/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34", size = 153852, upload-time = "2025-08-09T07:57:22.608Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.10.3" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/2c/253cc41cd0f40b84c1c34c5363e0407d73d4a1cae005fed6db3b823175bd/coverage-7.10.3.tar.gz", hash = "sha256:812ba9250532e4a823b070b0420a36499859542335af3dca8f47fc6aa1a05619", size = 822936, upload-time = "2025-08-10T21:27:39.968Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/44/e14576c34b37764c821866909788ff7463228907ab82bae188dab2b421f1/coverage-7.10.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:53808194afdf948c462215e9403cca27a81cf150d2f9b386aee4dab614ae2ffe", size = 215964, upload-time = "2025-08-10T21:25:22.828Z" }, + { url = "https://files.pythonhosted.org/packages/e6/15/f4f92d9b83100903efe06c9396ee8d8bdba133399d37c186fc5b16d03a87/coverage-7.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f4d1b837d1abf72187a61645dbf799e0d7705aa9232924946e1f57eb09a3bf00", size = 216361, upload-time = "2025-08-10T21:25:25.603Z" }, + { url = "https://files.pythonhosted.org/packages/23/53/c1d8c2778823b1d95ca81701bb8f42c87dc341a2f170acdf716567523490/coverage-7.10.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d52989685ff5bf909c430e6d7f6550937bc6d6f3e6ecb303c97a86100efd4596", size = 244927, upload-time = "2025-08-10T21:25:28.77Z" }, + { url = "https://files.pythonhosted.org/packages/79/41/1e115fd809031f432b4ff8e2ca19999fb6196ab95c35ae7ad5e07c001130/coverage-7.10.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdb558a1d97345bde3a9f4d3e8d11c9e5611f748646e9bb61d7d612a796671b5", size = 246784, upload-time = "2025-08-10T21:25:30.195Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b2/0eba9bdf8f1b327ae2713c74d4b7aa85451bb70622ab4e7b8c000936677c/coverage-7.10.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c9e6331a8f09cb1fc8bda032752af03c366870b48cce908875ba2620d20d0ad4", size = 244828, upload-time = "2025-08-10T21:25:31.785Z" }, + { url = "https://files.pythonhosted.org/packages/b6/7b/ac183fbe19ac5596c223cb47af5737f4437e7566100b7e46cc29b66695a5/coverage-7.10.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c5595fc4ad6a39312c786ec3326d7322d0cf10e3ac6a6df70809910026d67cfb", size = 243721, upload-time = "2025-08-10T21:25:34.939Z" }, + { url = "https://files.pythonhosted.org/packages/87/04/810e506d7a19889c244d35199cbf3239a2f952b55580aa42ca4287409424/coverage-7.10.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f2ff2e2afdf0d51b9b8301e542d9c21a8d084fd23d4c8ea2b3a1b3c96f5f7397", size = 216075, upload-time = "2025-08-10T21:25:39.891Z" }, + { url = "https://files.pythonhosted.org/packages/2e/50/6b3fbab034717b4af3060bdaea6b13dfdc6b1fad44b5082e2a95cd378a9a/coverage-7.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:18ecc5d1b9a8c570f6c9b808fa9a2b16836b3dd5414a6d467ae942208b095f85", size = 216476, upload-time = "2025-08-10T21:25:41.137Z" }, + { url = "https://files.pythonhosted.org/packages/34/12/5608f76070939395c17053bf16e81fd6c06cf362a537ea9d07e281013a27/coverage-7.10.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3966bc9a76b09a40dc6063c8b10375e827ea5dfcaffae402dd65953bef4cba54", size = 248800, upload-time = "2025-08-10T21:25:44.098Z" }, + { url = "https://files.pythonhosted.org/packages/ce/52/7cc90c448a0ad724283cbcdfd66b8d23a598861a6a22ac2b7b8696491798/coverage-7.10.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:205a95b87ef4eb303b7bc5118b47b6b6604a644bcbdb33c336a41cfc0a08c06a", size = 250904, upload-time = "2025-08-10T21:25:45.384Z" }, + { url = "https://files.pythonhosted.org/packages/e6/70/9967b847063c1c393b4f4d6daab1131558ebb6b51f01e7df7150aa99f11d/coverage-7.10.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b3801b79fb2ad61e3c7e2554bab754fc5f105626056980a2b9cf3aef4f13f84", size = 248597, upload-time = "2025-08-10T21:25:47.059Z" }, + { url = "https://files.pythonhosted.org/packages/8e/27/d27af83ad162eba62c4eb7844a1de6cf7d9f6b185df50b0a3514a6f80ddd/coverage-7.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a83d4f134bab2c7ff758e6bb1541dd72b54ba295ced6a63d93efc2e20cb9b124", size = 247290, upload-time = "2025-08-10T21:25:49.945Z" }, + { url = "https://files.pythonhosted.org/packages/b8/62/13c0b66e966c43d7aa64dadc8cd2afa1f5a2bf9bb863bdabc21fb94e8b63/coverage-7.10.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:449c1e2d3a84d18bd204258a897a87bc57380072eb2aded6a5b5226046207b42", size = 216262, upload-time = "2025-08-10T21:25:55.367Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f0/59fdf79be7ac2f0206fc739032f482cfd3f66b18f5248108ff192741beae/coverage-7.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1d4f9ce50b9261ad196dc2b2e9f1fbbee21651b54c3097a25ad783679fd18294", size = 216496, upload-time = "2025-08-10T21:25:56.759Z" }, + { url = "https://files.pythonhosted.org/packages/0c/29/f8bdf88357956c844bd872e87cb16748a37234f7f48c721dc7e981145eb7/coverage-7.10.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ca3c9530ee072b7cb6a6ea7b640bcdff0ad3b334ae9687e521e59f79b1d0437", size = 250738, upload-time = "2025-08-10T21:25:59.406Z" }, + { url = "https://files.pythonhosted.org/packages/ae/df/6396301d332b71e42bbe624670af9376f63f73a455cc24723656afa95796/coverage-7.10.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b6df359e59fa243c9925ae6507e27f29c46698359f45e568fd51b9315dbbe587", size = 251868, upload-time = "2025-08-10T21:26:00.65Z" }, + { url = "https://files.pythonhosted.org/packages/91/21/d760b2df6139b6ef62c9cc03afb9bcdf7d6e36ed4d078baacffa618b4c1c/coverage-7.10.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a181e4c2c896c2ff64c6312db3bda38e9ade2e1aa67f86a5628ae85873786cea", size = 249790, upload-time = "2025-08-10T21:26:02.009Z" }, + { url = "https://files.pythonhosted.org/packages/38/ed/70c0e871cdfef75f27faceada461206c1cc2510c151e1ef8d60a6fedda39/coverage-7.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:daeefff05993e5e8c6e7499a8508e7bd94502b6b9a9159c84fd1fe6bce3151cb", size = 249344, upload-time = "2025-08-10T21:26:05.11Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ff/239e4de9cc149c80e9cc359fab60592365b8c4cbfcad58b8a939d18c6898/coverage-7.10.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b99e87304ffe0eb97c5308447328a584258951853807afdc58b16143a530518a", size = 216298, upload-time = "2025-08-10T21:26:10.973Z" }, + { url = "https://files.pythonhosted.org/packages/56/da/28717da68f8ba68f14b9f558aaa8f3e39ada8b9a1ae4f4977c8f98b286d5/coverage-7.10.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4af09c7574d09afbc1ea7da9dcea23665c01f3bc1b1feb061dac135f98ffc53a", size = 216546, upload-time = "2025-08-10T21:26:12.616Z" }, + { url = "https://files.pythonhosted.org/packages/ea/2f/6ae1db51dc34db499bfe340e89f79a63bd115fc32513a7bacdf17d33cd86/coverage-7.10.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:913ceddb4289cbba3a310704a424e3fb7aac2bc0c3a23ea473193cb290cf17d4", size = 250141, upload-time = "2025-08-10T21:26:15.787Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ed/33efd8819895b10c66348bf26f011dd621e804866c996ea6893d682218df/coverage-7.10.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b1f91cbc78c7112ab84ed2a8defbccd90f888fcae40a97ddd6466b0bec6ae8a", size = 251415, upload-time = "2025-08-10T21:26:17.535Z" }, + { url = "https://files.pythonhosted.org/packages/26/04/cb83826f313d07dc743359c9914d9bc460e0798da9a0e38b4f4fabc207ed/coverage-7.10.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0bac054d45af7cd938834b43a9878b36ea92781bcb009eab040a5b09e9927e3", size = 249575, upload-time = "2025-08-10T21:26:18.921Z" }, + { url = "https://files.pythonhosted.org/packages/99/e8/b68d1487c6af370b8d5ef223c6d7e250d952c3acfbfcdbf1a773aa0da9d2/coverage-7.10.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c1e2e927ab3eadd7c244023927d646e4c15c65bb2ac7ae3c3e9537c013700d21", size = 249084, upload-time = "2025-08-10T21:26:21.638Z" }, + { url = "https://files.pythonhosted.org/packages/fc/26/1c1f450e15a3bf3eaecf053ff64538a2612a23f05b21d79ce03be9ff5903/coverage-7.10.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07009152f497a0464ffdf2634586787aea0e69ddd023eafb23fc38267db94b84", size = 217003, upload-time = "2025-08-10T21:26:27.231Z" }, + { url = "https://files.pythonhosted.org/packages/29/96/4b40036181d8c2948454b458750960956a3c4785f26a3c29418bbbee1666/coverage-7.10.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dd2ba5f0c7e7e8cc418be2f0c14c4d9e3f08b8fb8e4c0f83c2fe87d03eb655e", size = 217238, upload-time = "2025-08-10T21:26:28.83Z" }, + { url = "https://files.pythonhosted.org/packages/59/95/00e7fcbeda3f632232f4c07dde226afe3511a7781a000aa67798feadc535/coverage-7.10.3-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb329f1046888a36b1dc35504d3029e1dd5afe2196d94315d18c45ee380f67d5", size = 260735, upload-time = "2025-08-10T21:26:32.333Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4c/f4666cbc4571804ba2a65b078ff0de600b0b577dc245389e0bc9b69ae7ca/coverage-7.10.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce01048199a91f07f96ca3074b0c14021f4fe7ffd29a3e6a188ac60a5c3a4af8", size = 262960, upload-time = "2025-08-10T21:26:33.701Z" }, + { url = "https://files.pythonhosted.org/packages/c1/a5/8a9e8a7b12a290ed98b60f73d1d3e5e9ced75a4c94a0d1a671ce3ddfff2a/coverage-7.10.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:08b989a06eb9dfacf96d42b7fb4c9a22bafa370d245dc22fa839f2168c6f9fa1", size = 260515, upload-time = "2025-08-10T21:26:35.16Z" }, + { url = "https://files.pythonhosted.org/packages/cc/22/3646f8903743c07b3e53fded0700fed06c580a980482f04bf9536657ac17/coverage-7.10.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3262d19092771c83f3413831d9904b1ccc5f98da5de4ffa4ad67f5b20c7aaf7b", size = 259408, upload-time = "2025-08-10T21:26:37.954Z" }, + { url = "https://files.pythonhosted.org/packages/f1/66/c06f4a93c65b6fc6578ef4f1fe51f83d61fc6f2a74ec0ce434ed288d834a/coverage-7.10.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:da749daa7e141985487e1ff90a68315b0845930ed53dc397f4ae8f8bab25b551", size = 215951, upload-time = "2025-08-10T21:27:19.815Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ea/cc18c70a6f72f8e4def212eaebd8388c64f29608da10b3c38c8ec76f5e49/coverage-7.10.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3126fb6a47d287f461d9b1aa5d1a8c97034d1dffb4f452f2cf211289dae74ef", size = 216335, upload-time = "2025-08-10T21:27:21.737Z" }, + { url = "https://files.pythonhosted.org/packages/5a/e5/4223bdb28b992a19a13ab1410c761e2bfe92ca1e7bba8e85ee2024eeda85/coverage-7.10.3-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4e27bebbd184ef8d1c1e092b74a2b7109dcbe2618dce6e96b1776d53b14b3fe8", size = 244596, upload-time = "2025-08-10T21:27:25.842Z" }, + { url = "https://files.pythonhosted.org/packages/d2/13/d646ba28613669d487c654a760571c10128247d12d9f50e93f69542679a2/coverage-7.10.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8fd4ee2580b9fefbd301b4f8f85b62ac90d1e848bea54f89a5748cf132782118", size = 246370, upload-time = "2025-08-10T21:27:27.503Z" }, + { url = "https://files.pythonhosted.org/packages/02/7c/aff99c67d8c383142b0877ee435caf493765356336211c4899257325d6c7/coverage-7.10.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6999920bdd73259ce11cabfc1307484f071ecc6abdb2ca58d98facbcefc70f16", size = 244254, upload-time = "2025-08-10T21:27:29.357Z" }, + { url = "https://files.pythonhosted.org/packages/d8/4b/6119be0089c89ad49d2e5a508d55a1485c878642b706a7f95b26e299137d/coverage-7.10.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:25b902c5e15dea056485d782e420bb84621cc08ee75d5131ecb3dbef8bd1365f", size = 243281, upload-time = "2025-08-10T21:27:32.815Z" }, + { url = "https://files.pythonhosted.org/packages/84/19/e67f4ae24e232c7f713337f3f4f7c9c58afd0c02866fb07c7b9255a19ed7/coverage-7.10.3-py3-none-any.whl", hash = "sha256:416a8d74dc0adfd33944ba2f405897bab87b7e9e84a391e09d241956bd953ce1", size = 207921, upload-time = "2025-08-10T21:27:38.254Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "distro" +version = "1.8.0" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/89/eaa3a3587ebf8bed93e45aa79be8c2af77d50790d15b53f6dfc85b57f398/distro-1.8.0.tar.gz", hash = "sha256:02e111d1dc6a50abb8eed6bf31c3e48ed8b0830d1ea2a1b78c61765c2513fdd8", size = 59428, upload-time = "2022-10-10T15:30:33.395Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/2c/c90a3adaf0ddb70afe193f5ebfb539612af57cffe677c3126be533df3098/distro-1.8.0-py3-none-any.whl", hash = "sha256:99522ca3e365cac527b44bde033f64c6945d90eb9f769703caaec52b09bbd3ff", size = 20315, upload-time = "2022-10-10T15:30:26.903Z" }, +] + +[[package]] +name = "docopt" +version = "0.6.2" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/55/8f8cab2afd404cf578136ef2cc5dfb50baa1761b68c9da1fb1e4eed343c9/docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491", size = 25901, upload-time = "2014-06-16T11:18:57.406Z" } + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, +] + +[[package]] +name = "filelock" +version = "3.18.0" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "http3" +version = "1.0.0" +source = { virtual = "." } +dependencies = [ + { name = "splunk-soar-sdk", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] + +[package.dev-dependencies] +dev = [ + { name = "coverage", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "mypy", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pre-commit", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pytest", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pytest-mock", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pytest-watch", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "ruff", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] + +[package.metadata] +requires-dist = [{ name = "splunk-soar-sdk", specifier = ">=1.0.0b55" }] + +[package.metadata.requires-dev] +dev = [ + { name = "coverage", specifier = ">=7.6.7,<8" }, + { name = "mypy", specifier = ">=1.2.0,<2" }, + { name = "pre-commit", specifier = ">=4.2.0,<5" }, + { name = "pytest", specifier = ">=7.4.2,<8" }, + { name = "pytest-mock", specifier = ">=3.14.0,<4" }, + { name = "pytest-watch", specifier = ">=4.2.0,<5" }, + { name = "ruff", specifier = ">=0.11.6,<1" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "certifi", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "h11", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "anyio", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "certifi", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "httpcore", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "idna", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "humanize" +version = "4.12.3" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/d1/bbc4d251187a43f69844f7fd8941426549bbe4723e8ff0a7441796b0789f/humanize-4.12.3.tar.gz", hash = "sha256:8430be3a615106fdfceb0b2c1b41c4c98c6b0fc5cc59663a5539b111dd325fb0", size = 80514, upload-time = "2025-04-30T11:51:07.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/1e/62a2ec3104394a2975a2629eec89276ede9dbe717092f6966fcf963e1bf0/humanize-4.12.3-py3-none-any.whl", hash = "sha256:2cbf6370af06568fa6d2da77c86edb7886f3160ecd19ee1ffef07979efc597f6", size = 128487, upload-time = "2025-04-30T11:51:06.468Z" }, +] + +[[package]] +name = "identify" +version = "2.6.13" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ca/ffbabe3635bb839aa36b3a893c91a9b0d368cb4d8073e03a12896970af82/identify-2.6.13.tar.gz", hash = "sha256:da8d6c828e773620e13bfa86ea601c5a5310ba4bcd65edf378198b56a1f9fb32", size = 99243, upload-time = "2025-08-09T19:35:00.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/ce/461b60a3ee109518c055953729bf9ed089a04db895d47e95444071dcdef2/identify-2.6.13-py2.py3-none-any.whl", hash = "sha256:60381139b3ae39447482ecc406944190f690d4a2997f2584062089848361b33b", size = 99153, upload-time = "2025-08-09T19:34:59.1Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "markupsafe", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "mdurl", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" }, + { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload-time = "2024-10-18T15:20:52.426Z" }, + { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload-time = "2024-10-18T15:20:53.578Z" }, + { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload-time = "2024-10-18T15:20:55.06Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload-time = "2024-10-18T15:20:57.189Z" }, + { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload-time = "2024-10-18T15:20:59.235Z" }, + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ea/9b1530c3fdeeca613faeb0fb5cbcf2389d816072fab72a71b45749ef6062/MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", size = 14344, upload-time = "2024-10-18T15:21:43.721Z" }, + { url = "https://files.pythonhosted.org/packages/4b/c2/fbdbfe48848e7112ab05e627e718e854d20192b674952d9042ebd8c9e5de/MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", size = 12389, upload-time = "2024-10-18T15:21:44.666Z" }, + { url = "https://files.pythonhosted.org/packages/f0/25/7a7c6e4dbd4f867d95d94ca15449e91e52856f6ed1905d58ef1de5e211d0/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", size = 21607, upload-time = "2024-10-18T15:21:45.452Z" }, + { url = "https://files.pythonhosted.org/packages/53/8f/f339c98a178f3c1e545622206b40986a4c3307fe39f70ccd3d9df9a9e425/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", size = 20728, upload-time = "2024-10-18T15:21:46.295Z" }, + { url = "https://files.pythonhosted.org/packages/e6/cf/0a490a4bd363048c3022f2f475c8c05582179bb179defcee4766fb3dcc18/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", size = 21843, upload-time = "2024-10-18T15:21:48.334Z" }, + { url = "https://files.pythonhosted.org/packages/17/d8/5811082f85bb88410ad7e452263af048d685669bbbfb7b595e8689152498/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", size = 20946, upload-time = "2024-10-18T15:21:50.441Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mypy" +version = "1.17.1" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "mypy-extensions", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pathspec", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "tomli", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "typing-extensions", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/22/ea637422dedf0bf36f3ef238eab4e455e2a0dcc3082b5cc067615347ab8e/mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01", size = 3352570, upload-time = "2025-07-31T07:54:19.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/a9/3d7aa83955617cdf02f94e50aab5c830d205cfa4320cf124ff64acce3a8e/mypy-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3fbe6d5555bf608c47203baa3e72dbc6ec9965b3d7c318aa9a4ca76f465bd972", size = 11003299, upload-time = "2025-07-31T07:54:06.425Z" }, + { url = "https://files.pythonhosted.org/packages/83/e8/72e62ff837dd5caaac2b4a5c07ce769c8e808a00a65e5d8f94ea9c6f20ab/mypy-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80ef5c058b7bce08c83cac668158cb7edea692e458d21098c7d3bce35a5d43e7", size = 10125451, upload-time = "2025-07-31T07:53:52.974Z" }, + { url = "https://files.pythonhosted.org/packages/7d/10/f3f3543f6448db11881776f26a0ed079865926b0c841818ee22de2c6bbab/mypy-1.17.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a580f8a70c69e4a75587bd925d298434057fe2a428faaf927ffe6e4b9a98df", size = 11916211, upload-time = "2025-07-31T07:53:18.879Z" }, + { url = "https://files.pythonhosted.org/packages/06/bf/63e83ed551282d67bb3f7fea2cd5561b08d2bb6eb287c096539feb5ddbc5/mypy-1.17.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd86bb649299f09d987a2eebb4d52d10603224500792e1bee18303bbcc1ce390", size = 12652687, upload-time = "2025-07-31T07:53:30.544Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/68f2eeef11facf597143e85b694a161868b3b006a5fbad50e09ea117ef24/mypy-1.17.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a76906f26bd8d51ea9504966a9c25419f2e668f012e0bdf3da4ea1526c534d94", size = 12896322, upload-time = "2025-07-31T07:53:50.74Z" }, + { url = "https://files.pythonhosted.org/packages/46/cf/eadc80c4e0a70db1c08921dcc220357ba8ab2faecb4392e3cebeb10edbfa/mypy-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad37544be07c5d7fba814eb370e006df58fed8ad1ef33ed1649cb1889ba6ff58", size = 10921009, upload-time = "2025-07-31T07:53:23.037Z" }, + { url = "https://files.pythonhosted.org/packages/5d/c1/c869d8c067829ad30d9bdae051046561552516cfb3a14f7f0347b7d973ee/mypy-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:064e2ff508e5464b4bd807a7c1625bc5047c5022b85c70f030680e18f37273a5", size = 10047482, upload-time = "2025-07-31T07:53:26.151Z" }, + { url = "https://files.pythonhosted.org/packages/98/b9/803672bab3fe03cee2e14786ca056efda4bb511ea02dadcedde6176d06d0/mypy-1.17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70401bbabd2fa1aa7c43bb358f54037baf0586f41e83b0ae67dd0534fc64edfd", size = 11832883, upload-time = "2025-07-31T07:53:47.948Z" }, + { url = "https://files.pythonhosted.org/packages/88/fb/fcdac695beca66800918c18697b48833a9a6701de288452b6715a98cfee1/mypy-1.17.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e92bdc656b7757c438660f775f872a669b8ff374edc4d18277d86b63edba6b8b", size = 12566215, upload-time = "2025-07-31T07:54:04.031Z" }, + { url = "https://files.pythonhosted.org/packages/7f/37/a932da3d3dace99ee8eb2043b6ab03b6768c36eb29a02f98f46c18c0da0e/mypy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c1fdf4abb29ed1cb091cf432979e162c208a5ac676ce35010373ff29247bcad5", size = 12751956, upload-time = "2025-07-31T07:53:36.263Z" }, + { url = "https://files.pythonhosted.org/packages/17/a2/7034d0d61af8098ec47902108553122baa0f438df8a713be860f7407c9e6/mypy-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:69e83ea6553a3ba79c08c6e15dbd9bfa912ec1e493bf75489ef93beb65209aeb", size = 11086295, upload-time = "2025-07-31T07:53:28.124Z" }, + { url = "https://files.pythonhosted.org/packages/14/1f/19e7e44b594d4b12f6ba8064dbe136505cec813549ca3e5191e40b1d3cc2/mypy-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b16708a66d38abb1e6b5702f5c2c87e133289da36f6a1d15f6a5221085c6403", size = 10112355, upload-time = "2025-07-31T07:53:21.121Z" }, + { url = "https://files.pythonhosted.org/packages/5b/69/baa33927e29e6b4c55d798a9d44db5d394072eef2bdc18c3e2048c9ed1e9/mypy-1.17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89e972c0035e9e05823907ad5398c5a73b9f47a002b22359b177d40bdaee7056", size = 11875285, upload-time = "2025-07-31T07:53:55.293Z" }, + { url = "https://files.pythonhosted.org/packages/90/13/f3a89c76b0a41e19490b01e7069713a30949d9a6c147289ee1521bcea245/mypy-1.17.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03b6d0ed2b188e35ee6d5c36b5580cffd6da23319991c49ab5556c023ccf1341", size = 12737895, upload-time = "2025-07-31T07:53:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/23/a1/c4ee79ac484241301564072e6476c5a5be2590bc2e7bfd28220033d2ef8f/mypy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c837b896b37cd103570d776bda106eabb8737aa6dd4f248451aecf53030cdbeb", size = 12931025, upload-time = "2025-07-31T07:54:17.125Z" }, + { url = "https://files.pythonhosted.org/packages/5b/82/aec2fc9b9b149f372850291827537a508d6c4d3664b1750a324b91f71355/mypy-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93378d3203a5c0800c6b6d850ad2f19f7a3cdf1a3701d3416dbf128805c6a6a7", size = 11075338, upload-time = "2025-07-31T07:53:38.873Z" }, + { url = "https://files.pythonhosted.org/packages/07/ac/ee93fbde9d2242657128af8c86f5d917cd2887584cf948a8e3663d0cd737/mypy-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15d54056f7fe7a826d897789f53dd6377ec2ea8ba6f776dc83c2902b899fee81", size = 10113066, upload-time = "2025-07-31T07:54:14.707Z" }, + { url = "https://files.pythonhosted.org/packages/5a/68/946a1e0be93f17f7caa56c45844ec691ca153ee8b62f21eddda336a2d203/mypy-1.17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:209a58fed9987eccc20f2ca94afe7257a8f46eb5df1fb69958650973230f91e6", size = 11875473, upload-time = "2025-07-31T07:53:14.504Z" }, + { url = "https://files.pythonhosted.org/packages/9f/0f/478b4dce1cb4f43cf0f0d00fba3030b21ca04a01b74d1cd272a528cf446f/mypy-1.17.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:099b9a5da47de9e2cb5165e581f158e854d9e19d2e96b6698c0d64de911dd849", size = 12744296, upload-time = "2025-07-31T07:53:03.896Z" }, + { url = "https://files.pythonhosted.org/packages/ca/70/afa5850176379d1b303f992a828de95fc14487429a7139a4e0bdd17a8279/mypy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ffadfbe6994d724c5a1bb6123a7d27dd68fc9c059561cd33b664a79578e14", size = 12914657, upload-time = "2025-07-31T07:54:08.576Z" }, + { url = "https://files.pythonhosted.org/packages/29/cb/673e3d34e5d8de60b3a61f44f80150a738bff568cd6b7efb55742a605e98/mypy-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5d1092694f166a7e56c805caaf794e0585cabdbf1df36911c414e4e9abb62ae9", size = 10992466, upload-time = "2025-07-31T07:53:57.574Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d0/fe1895836eea3a33ab801561987a10569df92f2d3d4715abf2cfeaa29cb2/mypy-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79d44f9bfb004941ebb0abe8eff6504223a9c1ac51ef967d1263c6572bbebc99", size = 10117638, upload-time = "2025-07-31T07:53:34.256Z" }, + { url = "https://files.pythonhosted.org/packages/97/f3/514aa5532303aafb95b9ca400a31054a2bd9489de166558c2baaeea9c522/mypy-1.17.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b01586eed696ec905e61bd2568f48740f7ac4a45b3a468e6423a03d3788a51a8", size = 11915673, upload-time = "2025-07-31T07:52:59.361Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c3/c0805f0edec96fe8e2c048b03769a6291523d509be8ee7f56ae922fa3882/mypy-1.17.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43808d9476c36b927fbcd0b0255ce75efe1b68a080154a38ae68a7e62de8f0f8", size = 12649022, upload-time = "2025-07-31T07:53:45.92Z" }, + { url = "https://files.pythonhosted.org/packages/45/3e/d646b5a298ada21a8512fa7e5531f664535a495efa672601702398cea2b4/mypy-1.17.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:feb8cc32d319edd5859da2cc084493b3e2ce5e49a946377663cc90f6c15fb259", size = 12895536, upload-time = "2025-07-31T07:53:06.17Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f3/8fcd2af0f5b806f6cf463efaffd3c9548a28f84220493ecd38d127b6b66d/mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9", size = 2283411, upload-time = "2025-07-31T07:53:24.664Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.3.8" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.3.0" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "cfgv", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "identify", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nodeenv", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pyyaml", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "virtualenv", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16", size = 193792, upload-time = "2025-08-09T18:56:14.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965, upload-time = "2025-08-09T18:56:13.192Z" }, +] + +[[package]] +name = "pydantic" +version = "1.10.22" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/57/5996c63f0deec09e9e901a2b838247c97c6844999562eac4e435bcb83938/pydantic-1.10.22.tar.gz", hash = "sha256:ee1006cebd43a8e7158fb7190bb8f4e2da9649719bff65d0c287282ec38dec6d", size = 356771, upload-time = "2025-04-24T13:38:43.605Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/92/91eb5c75a1460292e1f2f3e577122574ebb942fbac19ad2369ff00b9eb24/pydantic-1.10.22-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:57889565ccc1e5b7b73343329bbe6198ebc472e3ee874af2fa1865cfe7048228", size = 2852481, upload-time = "2025-04-24T13:36:55.045Z" }, + { url = "https://files.pythonhosted.org/packages/08/f3/dd54b49fc5caaed06f5a0d0a5ec35a81cf722cd6b42455f408dad1ef3f7d/pydantic-1.10.22-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:90729e22426de79bc6a3526b4c45ec4400caf0d4f10d7181ba7f12c01bb3897d", size = 2585586, upload-time = "2025-04-24T13:36:58.453Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9b/48d10180cc614ffb66da486e99bc1f8b639fb44edf322864f2fb161e2351/pydantic-1.10.22-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8684d347f351554ec94fdcb507983d3116dc4577fb8799fed63c65869a2d10", size = 3336974, upload-time = "2025-04-24T13:37:00.652Z" }, + { url = "https://files.pythonhosted.org/packages/38/c5/c776d03ec374f22860802b2cee057b41e866be3c80826b53d4c001692db3/pydantic-1.10.22-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4148232aded8dd1dd13cf910a01b32a763c34bd79a0ab4d1ee66164fcb0b7b9d", size = 3485878, upload-time = "2025-04-24T13:37:06.102Z" }, + { url = "https://files.pythonhosted.org/packages/42/03/e435ed85a9abda29e3fbdb49c572fe4131a68c6daf3855a01eebda9e1b27/pydantic-1.10.22-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8e530a8da353f791ad89e701c35787418605d35085f4bdda51b416946070e938", size = 2845682, upload-time = "2025-04-24T13:37:10.142Z" }, + { url = "https://files.pythonhosted.org/packages/72/ea/4a625035672f6c06d3f1c7e33aa0af6bf1929991e27017e98b9c2064ae0b/pydantic-1.10.22-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:654322b85642e9439d7de4c83cb4084ddd513df7ff8706005dada43b34544946", size = 2553286, upload-time = "2025-04-24T13:37:11.946Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f0/424ad837746e69e9f061ba9be68c2a97aef7376d1911692904d8efbcd322/pydantic-1.10.22-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8bece75bd1b9fc1c32b57a32831517943b1159ba18b4ba32c0d431d76a120ae", size = 3141232, upload-time = "2025-04-24T13:37:14.394Z" }, + { url = "https://files.pythonhosted.org/packages/92/35/dffc1b29cb7198aadab68d75447191e59bdbc1f1d2d51826c9a4460d372f/pydantic-1.10.22-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bffe02767d27c39af9ca7dc7cd479c00dda6346bb62ffc89e306f665108317a2", size = 3244258, upload-time = "2025-04-24T13:37:20.929Z" }, + { url = "https://files.pythonhosted.org/packages/f6/a3/ec66239ed7c9e90edfb85b23b6b18eb290ed7aa05f54837cdcb6a14faa98/pydantic-1.10.22-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:92d0f97828a075a71d9efc65cf75db5f149b4d79a38c89648a63d2932894d8c9", size = 2794865, upload-time = "2025-04-24T13:37:25.087Z" }, + { url = "https://files.pythonhosted.org/packages/49/6a/99cf3fee612d93210c85f45a161e98c1c5b45b6dcadb21c9f1f838fa9e28/pydantic-1.10.22-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6af5a2811b6b95b58b829aeac5996d465a5f0c7ed84bd871d603cf8646edf6ff", size = 2534212, upload-time = "2025-04-24T13:37:26.848Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e6/0f8882775cd9a60b221103ee7d6a89e10eb5a892d877c398df0da7140704/pydantic-1.10.22-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6cf06d8d40993e79af0ab2102ef5da77b9ddba51248e4cb27f9f3f591fbb096e", size = 2994027, upload-time = "2025-04-24T13:37:28.683Z" }, + { url = "https://files.pythonhosted.org/packages/1e/6e/b64deccb8a7304d584088972437ea3091e9d99d27a8e7bf2bd08e29ae84e/pydantic-1.10.22-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:82d9a3da1686443fb854c8d2ab9a473251f8f4cdd11b125522efb4d7c646e7bc", size = 3123560, upload-time = "2025-04-24T13:37:34.855Z" }, + { url = "https://files.pythonhosted.org/packages/47/8f/67befe3607b342dd6eb80237134ebcc6e8db42138609306eaf2b30e1f273/pydantic-1.10.22-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b259dc89c9abcd24bf42f31951fb46c62e904ccf4316393f317abeeecda39978", size = 2797042, upload-time = "2025-04-24T13:37:38.753Z" }, + { url = "https://files.pythonhosted.org/packages/aa/91/bfde7d301f8e1c4cff949b3f1eb2c9b27bdd4b2368da0fe88e7350bbe4bc/pydantic-1.10.22-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9238aa0964d80c0908d2f385e981add58faead4412ca80ef0fa352094c24e46d", size = 2538572, upload-time = "2025-04-24T13:37:41.653Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/1b0097ece420354df77d2f01c72278fb43770c8ed732d6b7a303c0c70875/pydantic-1.10.22-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f8029f05b04080e3f1a550575a1bca747c0ea4be48e2d551473d47fd768fc1b", size = 2986271, upload-time = "2025-04-24T13:37:43.551Z" }, + { url = "https://files.pythonhosted.org/packages/1a/47/2d55ec452c9a87347234bbbc70df268e1f081154b1851f0db89638558a1c/pydantic-1.10.22-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:815f0a73d5688d6dd0796a7edb9eca7071bfef961a7b33f91e618822ae7345b7", size = 3117572, upload-time = "2025-04-24T13:37:49.339Z" }, + { url = "https://files.pythonhosted.org/packages/01/6f/9658e94018bc7c4e71863fb0f1ea8d30f8b3439e17df7aa710b2bb72dbca/pydantic-1.10.22-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ec54c89b2568b258bb30d7348ac4d82bec1b58b377fb56a00441e2ac66b24587", size = 2854460, upload-time = "2025-04-24T13:38:23.753Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b3/5184ec7d3423a37c193ffaeced03921f4c34d226f3c6852653784f37d38b/pydantic-1.10.22-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d8f1d1a1532e4f3bcab4e34e8d2197a7def4b67072acd26cfa60e92d75803a48", size = 2587418, upload-time = "2025-04-24T13:38:26.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/25/27d769c5dc7491df5faebfc49a26f83ca2e070a9a788c67fde4c4e51d68b/pydantic-1.10.22-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ad83ca35508c27eae1005b6b61f369f78aae6d27ead2135ec156a2599910121", size = 3331289, upload-time = "2025-04-24T13:38:29.262Z" }, + { url = "https://files.pythonhosted.org/packages/e9/87/eb3408e1c040a6d9f703e089d26a723d6c41f23a192e86bd7584d037d576/pydantic-1.10.22-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8aee040e25843f036192b1a1af62117504a209a043aa8db12e190bb86ad7e611", size = 3483434, upload-time = "2025-04-24T13:38:36.078Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e0/1ed151a56869be1588ad2d8cda9f8c1d95b16f74f09a7cea879ca9b63a8b/pydantic-1.10.22-py3-none-any.whl", hash = "sha256:343037d608bcbd34df937ac259708bfc83664dadf88afe8516c4f282d7d471a9", size = 166503, upload-time = "2025-04-24T13:38:41.374Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "7.4.4" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "iniconfig", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "packaging", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pluggy", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "tomli", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/1f/9d8e98e4133ffb16c90f3b405c43e38d3abb715bb5d7a63a5a684f7e46a3/pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", size = 1357116, upload-time = "2023-12-31T12:00:18.035Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8", size = 325287, upload-time = "2023-12-31T12:00:13.963Z" }, +] + +[[package]] +name = "pytest-mock" +version = "3.14.1" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "pytest", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/28/67172c96ba684058a4d24ffe144d64783d2a270d0af0d9e792737bddc75c/pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e", size = 33241, upload-time = "2025-05-26T13:58:45.167Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/05/77b60e520511c53d1c1ca75f1930c7dd8e971d0c4379b7f4b3f9644685ba/pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0", size = 9923, upload-time = "2025-05-26T13:58:43.487Z" }, +] + +[[package]] +name = "pytest-watch" +version = "4.2.0" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "colorama", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "docopt", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pytest", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "watchdog", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/47/ab65fc1d682befc318c439940f81a0de1026048479f732e84fe714cd69c0/pytest-watch-4.2.0.tar.gz", hash = "sha256:06136f03d5b361718b8d0d234042f7b2f203910d8568f63df2f866b547b3d4b9", size = 16340, upload-time = "2018-05-20T19:52:16.194Z" } + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777, upload-time = "2024-08-06T20:33:25.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318, upload-time = "2024-08-06T20:33:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891, upload-time = "2024-08-06T20:33:28.974Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614, upload-time = "2024-08-06T20:33:34.157Z" }, + { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360, upload-time = "2024-08-06T20:33:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006, upload-time = "2024-08-06T20:33:37.501Z" }, + { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577, upload-time = "2024-08-06T20:33:39.389Z" }, +] + +[[package]] +name = "requests" +version = "2.32.4" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "certifi", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "charset-normalizer", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "idna", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "urllib3", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, +] + +[[package]] +name = "responses" +version = "0.22.0" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "requests", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "toml", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "types-toml", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "urllib3", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/03/cc/c15e28fdbc81909ac18a4c22bebe8517bbafef43947a072da6f042fbd4d5/responses-0.22.0.tar.gz", hash = "sha256:396acb2a13d25297789a5866b4881cf4e46ffd49cc26c43ab1117f40b973102e", size = 71785, upload-time = "2022-10-11T16:34:46.926Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/d0/2b9030eedf7061ac60828fcd92a4b04dc9a7dd07f316300f2841c41421a0/responses-0.22.0-py3-none-any.whl", hash = "sha256:dcf294d204d14c436fddcc74caefdbc5764795a40ff4e6a7740ed8ddbf3294be", size = 51148, upload-time = "2022-10-11T16:34:46.926Z" }, +] + +[[package]] +name = "rich" +version = "14.1.0" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "markdown-it-py", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pygments", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" }, +] + +[[package]] +name = "ruff" +version = "0.12.8" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/da/5bd7565be729e86e1442dad2c9a364ceeff82227c2dece7c29697a9795eb/ruff-0.12.8.tar.gz", hash = "sha256:4cb3a45525176e1009b2b64126acf5f9444ea59066262791febf55e40493a033", size = 5242373, upload-time = "2025-08-07T19:05:47.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/1e/c843bfa8ad1114fab3eb2b78235dda76acd66384c663a4e0415ecc13aa1e/ruff-0.12.8-py3-none-linux_armv6l.whl", hash = "sha256:63cb5a5e933fc913e5823a0dfdc3c99add73f52d139d6cd5cc8639d0e0465513", size = 11675315, upload-time = "2025-08-07T19:05:06.15Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/af6e5c2a8ca3a81676d5480a1025494fd104b8896266502bb4de2a0e8388/ruff-0.12.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a9bbe28f9f551accf84a24c366c1aa8774d6748438b47174f8e8565ab9dedbc", size = 12456653, upload-time = "2025-08-07T19:05:09.759Z" }, + { url = "https://files.pythonhosted.org/packages/99/9d/e91f84dfe3866fa648c10512904991ecc326fd0b66578b324ee6ecb8f725/ruff-0.12.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2fae54e752a3150f7ee0e09bce2e133caf10ce9d971510a9b925392dc98d2fec", size = 11659690, upload-time = "2025-08-07T19:05:12.551Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ac/a363d25ec53040408ebdd4efcee929d48547665858ede0505d1d8041b2e5/ruff-0.12.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0acbcf01206df963d9331b5838fb31f3b44fa979ee7fa368b9b9057d89f4a53", size = 11896923, upload-time = "2025-08-07T19:05:14.821Z" }, + { url = "https://files.pythonhosted.org/packages/58/9f/ea356cd87c395f6ade9bb81365bd909ff60860975ca1bc39f0e59de3da37/ruff-0.12.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae3e7504666ad4c62f9ac8eedb52a93f9ebdeb34742b8b71cd3cccd24912719f", size = 11477612, upload-time = "2025-08-07T19:05:16.712Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c4/f2176a310f26e6160deaf661ef60db6c3bb62b7a35e57ae28f27a09a7d63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dbea798fc0065ad0b84a2947b0aff4233f0cb30f226f00a2c5850ca4393de609", size = 14206885, upload-time = "2025-08-07T19:05:21.025Z" }, + { url = "https://files.pythonhosted.org/packages/87/9d/98e162f3eeeb6689acbedbae5050b4b3220754554526c50c292b611d3a63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:49ebcaccc2bdad86fd51b7864e3d808aad404aab8df33d469b6e65584656263a", size = 13639381, upload-time = "2025-08-07T19:05:23.423Z" }, + { url = "https://files.pythonhosted.org/packages/81/4e/1b7478b072fcde5161b48f64774d6edd59d6d198e4ba8918d9f4702b8043/ruff-0.12.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ac9c570634b98c71c88cb17badd90f13fc076a472ba6ef1d113d8ed3df109fb", size = 12613271, upload-time = "2025-08-07T19:05:25.507Z" }, + { url = "https://files.pythonhosted.org/packages/e8/67/0c3c9179a3ad19791ef1b8f7138aa27d4578c78700551c60d9260b2c660d/ruff-0.12.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:560e0cd641e45591a3e42cb50ef61ce07162b9c233786663fdce2d8557d99818", size = 12847783, upload-time = "2025-08-07T19:05:28.14Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2a/0b6ac3dd045acf8aa229b12c9c17bb35508191b71a14904baf99573a21bd/ruff-0.12.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:71c83121512e7743fba5a8848c261dcc454cafb3ef2934a43f1b7a4eb5a447ea", size = 11702672, upload-time = "2025-08-07T19:05:30.413Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ee/f9fdc9f341b0430110de8b39a6ee5fa68c5706dc7c0aa940817947d6937e/ruff-0.12.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:de4429ef2ba091ecddedd300f4c3f24bca875d3d8b23340728c3cb0da81072c3", size = 11440626, upload-time = "2025-08-07T19:05:32.492Z" }, + { url = "https://files.pythonhosted.org/packages/18/9f/5c5d93e1d00d854d5013c96e1a92c33b703a0332707a7cdbd0a4880a84fb/ruff-0.12.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:45c32487e14f60b88aad6be9fd5da5093dbefb0e3e1224131cb1d441d7cb7d46", size = 12913212, upload-time = "2025-08-07T19:05:36.541Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.7" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418, upload-time = "2025-04-20T18:50:08.518Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677, upload-time = "2025-04-20T18:50:07.196Z" }, +] + +[[package]] +name = "splunk-soar-sdk" +version = "1.0.0b55" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "beautifulsoup4", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "bleach", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "click", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "distro", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "httpx", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "humanize", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "jinja2", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "packaging", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pydantic", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "requests", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "responses", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "toml", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "tqdm", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "typer", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/48/6639d36ff8184c6729de0ef87ff64bec8a6a45fe821907c8c2aa05e84442/splunk_soar_sdk-1.0.0b55.tar.gz", hash = "sha256:375be2f77736427b86d940f2245fbee58a600e1d68ac95c3bb9d6419c82479cd", size = 370757, upload-time = "2025-08-06T20:23:45.368Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/0a/c65d959cfebdb6201511fc9f0c8766311b3abeff0aab060efe261d1fec03/splunk_soar_sdk-1.0.0b55-py3-none-any.whl", hash = "sha256:68a3846fb0aa9822d9c7b8552e0ca533c4589212d7ea32fdf576bd3e8be65e20", size = 146305, upload-time = "2025-08-06T20:23:41.924Z" }, +] + +[[package]] +name = "toml" +version = "0.10.2" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + +[[package]] +name = "typer" +version = "0.16.0" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "click", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "rich", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "shellingham", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "typing-extensions", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/8c/7d682431efca5fd290017663ea4588bf6f2c6aad085c7f108c5dbc316e70/typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b", size = 102625, upload-time = "2025-05-26T14:30:31.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/42/3efaf858001d2c2913de7f354563e3a3a2f0decae3efe98427125a8f441e/typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855", size = 46317, upload-time = "2025-05-26T14:30:30.523Z" }, +] + +[[package]] +name = "types-toml" +version = "0.10.8.20240310" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/47/3e4c75042792bff8e90d7991aa5c51812cc668828cc6cce711e97f63a607/types-toml-0.10.8.20240310.tar.gz", hash = "sha256:3d41501302972436a6b8b239c850b26689657e25281b48ff0ec06345b8830331", size = 4392, upload-time = "2024-03-10T02:18:37.518Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/a2/d32ab58c0b216912638b140ab2170ee4b8644067c293b170e19fba340ccc/types_toml-0.10.8.20240310-py3-none-any.whl", hash = "sha256:627b47775d25fa29977d9c70dc0cbab3f314f32c8d8d0c012f2ef5de7aaec05d", size = 4777, upload-time = "2024-03-10T02:18:36.568Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "virtualenv" +version = "20.33.1" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "distlib", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "filelock", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "platformdirs", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/60/4f20960df6c7b363a18a55ab034c8f2bcd5d9770d1f94f9370ec104c1855/virtualenv-20.33.1.tar.gz", hash = "sha256:1b44478d9e261b3fb8baa5e74a0ca3bc0e05f21aa36167bf9cbf850e542765b8", size = 6082160, upload-time = "2025-08-05T16:10:55.605Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/ff/ded57ac5ff40a09e6e198550bab075d780941e0b0f83cbeabd087c59383a/virtualenv-20.33.1-py3-none-any.whl", hash = "sha256:07c19bc66c11acab6a5958b815cbcee30891cd1c2ccf53785a28651a0d8d8a67", size = 6060362, upload-time = "2025-08-05T16:10:52.81Z" }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, + { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/05/52/7223011bb760fce8ddc53416beb65b83a3ea6d7d13738dde75eeb2c89679/watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8", size = 96390, upload-time = "2024-11-01T14:06:49.325Z" }, + { url = "https://files.pythonhosted.org/packages/9c/62/d2b21bc4e706d3a9d467561f487c2938cbd881c69f3808c43ac1ec242391/watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a", size = 88386, upload-time = "2024-11-01T14:06:50.536Z" }, + { url = "https://files.pythonhosted.org/packages/ea/22/1c90b20eda9f4132e4603a26296108728a8bfe9584b006bd05dd94548853/watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c", size = 89017, upload-time = "2024-11-01T14:06:51.717Z" }, + { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, + { url = "https://files.pythonhosted.org/packages/5b/79/69f2b0e8d3f2afd462029031baafb1b75d11bb62703f0e1022b2e54d49ee/watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa", size = 87903, upload-time = "2024-11-01T14:06:57.052Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2b/dc048dd71c2e5f0f7ebc04dd7912981ec45793a03c0dc462438e0591ba5d/watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e", size = 88381, upload-time = "2024-11-01T14:06:58.193Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, +] + +[[package]] +name = "webencodings" +version = "0.5.1" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, +] diff --git a/wheels/py3/validators-0.35.0-py3-none-any.whl b/wheels/py3/validators-0.35.0-py3-none-any.whl deleted file mode 100644 index 92db5ce21bd9377f3e763a208015ac8d2912cfd5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44712 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 From fcbdab8bf2576a6e0bf58c0447c25f83f08c858d Mon Sep 17 00:00:00 2001 From: Adrian Rosicki Date: Thu, 14 Aug 2025 13:15:29 +0200 Subject: [PATCH 02/29] PSAAS-24764: Move code to 'package' directory --- .gitignore => package/.gitignore | 0 .../.pre-commit-config.yml | 0 logo.svg => package/logo.svg | 0 logo_dark.svg => package/logo_dark.svg | 0 pyproject.toml => package/pyproject.toml | 0 {src => package/src}/__init__.py | 0 {src => package/src}/app.py | 0 uv.lock => package/uv.lock | 11 ++++++----- 8 files changed, 6 insertions(+), 5 deletions(-) rename .gitignore => package/.gitignore (100%) rename .pre-commit-config.yml => package/.pre-commit-config.yml (100%) rename logo.svg => package/logo.svg (100%) rename logo_dark.svg => package/logo_dark.svg (100%) rename pyproject.toml => package/pyproject.toml (100%) rename {src => package/src}/__init__.py (100%) rename {src => package/src}/app.py (100%) rename uv.lock => package/uv.lock (99%) diff --git a/.gitignore b/package/.gitignore similarity index 100% rename from .gitignore rename to package/.gitignore diff --git a/.pre-commit-config.yml b/package/.pre-commit-config.yml similarity index 100% rename from .pre-commit-config.yml rename to package/.pre-commit-config.yml diff --git a/logo.svg b/package/logo.svg similarity index 100% rename from logo.svg rename to package/logo.svg diff --git a/logo_dark.svg b/package/logo_dark.svg similarity index 100% rename from logo_dark.svg rename to package/logo_dark.svg diff --git a/pyproject.toml b/package/pyproject.toml similarity index 100% rename from pyproject.toml rename to package/pyproject.toml diff --git a/src/__init__.py b/package/src/__init__.py similarity index 100% rename from src/__init__.py rename to package/src/__init__.py diff --git a/src/app.py b/package/src/app.py similarity index 100% rename from src/app.py rename to package/src/app.py diff --git a/uv.lock b/package/uv.lock similarity index 99% rename from uv.lock rename to package/uv.lock index 2bd2d6d..f32d897 100644 --- a/uv.lock +++ b/package/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.9, <3.14" resolution-markers = [ "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'", @@ -255,7 +255,7 @@ wheels = [ ] [[package]] -name = "http3" +name = "http" version = "1.0.0" source = { virtual = "." } dependencies = [ @@ -866,16 +866,17 @@ wheels = [ [[package]] name = "virtualenv" -version = "20.33.1" +version = "20.34.0" source = { registry = "https://pypi.python.org/simple" } dependencies = [ { name = "distlib", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "filelock", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "platformdirs", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "typing-extensions", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8b/60/4f20960df6c7b363a18a55ab034c8f2bcd5d9770d1f94f9370ec104c1855/virtualenv-20.33.1.tar.gz", hash = "sha256:1b44478d9e261b3fb8baa5e74a0ca3bc0e05f21aa36167bf9cbf850e542765b8", size = 6082160, upload-time = "2025-08-05T16:10:55.605Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/14/37fcdba2808a6c615681cd216fecae00413c9dab44fb2e57805ecf3eaee3/virtualenv-20.34.0.tar.gz", hash = "sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a", size = 6003808, upload-time = "2025-08-13T14:24:07.464Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/ff/ded57ac5ff40a09e6e198550bab075d780941e0b0f83cbeabd087c59383a/virtualenv-20.33.1-py3-none-any.whl", hash = "sha256:07c19bc66c11acab6a5958b815cbcee30891cd1c2ccf53785a28651a0d8d8a67", size = 6060362, upload-time = "2025-08-05T16:10:52.81Z" }, + { url = "https://files.pythonhosted.org/packages/76/06/04c8e804f813cf972e3262f3f8584c232de64f0cde9f703b46cf53a45090/virtualenv-20.34.0-py3-none-any.whl", hash = "sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026", size = 5983279, upload-time = "2025-08-13T14:24:05.111Z" }, ] [[package]] From 996e475dbb27184d3d392b438a94dc28522d3670 Mon Sep 17 00:00:00 2001 From: msankows Date: Tue, 19 Aug 2025 14:20:05 +0200 Subject: [PATCH 03/29] feat: Implement 'http_get' action for making GET requests --- package/src/actions/http_get.py | 82 +++++++++++++++++++++++++++++++++ package/src/app.py | 12 +++-- 2 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 package/src/actions/http_get.py diff --git a/package/src/actions/http_get.py b/package/src/actions/http_get.py new file mode 100644 index 0000000..b33ffd3 --- /dev/null +++ b/package/src/actions/http_get.py @@ -0,0 +1,82 @@ +import requests +import json +from soar_sdk.abstract import SOARClient +from soar_sdk.action_results import ActionOutput, OutputField +from soar_sdk.app import App +from soar_sdk.asset import AssetField, BaseAsset +from soar_sdk.exceptions import ActionFailure +from soar_sdk.logging import getLogger +from soar_sdk.params import Param, Params +from ..app import Asset + +logger = getLogger() + +class GetDataOutput(ActionOutput): + location: str = OutputField( + cef_types=["url"], example_values=["http://192.168.1.26/rest/cont"] + ) + method: str = OutputField(example_values=["GET"]) + parsed_response_body: str = OutputField( + example_values=['{"failed": true, "message": "Requested item not found"}'] + ) + response_body: str = OutputField( + example_values=['{"failed": true, "message": "Requested item not found"}'] + ) + response_headers: str + +class GetDataParams(Params): + location: str = Param( + description="Location (e.g. path/to/endpoint?query=string)", + primary=True, + cef_types=["endpoint"], + ) + verify_certificate: bool = Param(description="Verify certificates (if using HTTPS)") + headers: str = Param(description="Additional headers (JSON object with headers)", required=False) + +def http_get(params: GetDataParams, soar: SOARClient, asset: Asset) -> GetDataOutput: + logger.info(f"In action handler for: http_get with location: {params.location}") + + parsed_headers = {} + if params.headers: + try: + parsed_headers = json.loads(params.headers) + if not isinstance(parsed_headers, dict): + raise ActionFailure("Headers parameter must be a valid JSON object (dictionary).") + except json.JSONDecodeError as e: + error_message = f"Failed to parse headers. Ensure it's a valid JSON object. Error: {e}" + logger.error(error_message) + raise ActionFailure(error_message) + + full_url = asset.base_url.rstrip("/") + "/" + params.location.lstrip('/') + logger.info(f"Making GET request to: {full_url}") + + try: + response = requests.get( + uri=full_url, + headers=parsed_headers, + verify=params.verify_certificate, + timeout=asset.timeout, + ) + response.raise_for_status() + + try: + parsed_body_object = response.json() + parsed_body_as_string = json.dumps(parsed_body_object, indent=4) + except requests.exceptions.JSONDecodeError: + parsed_body_as_string = response.text + + logger.info(f"Successfully fetched data. Status: {response.status_code}") + + return GetDataOutput( + location=full_url, + method="GET", + parsed_response_body=parsed_body_as_string, + response_body=response.text, + response_headers=str(dict(response.headers)) + ) + + except requests.exceptions.RequestException as e: + error_message = f"Failed to get data from {full_url}. Details: {e}" + logger.error(error_message) + raise ActionFailure(error_message) + \ No newline at end of file diff --git a/package/src/app.py b/package/src/app.py index 2391949..a8c16a2 100644 --- a/package/src/app.py +++ b/package/src/app.py @@ -1,16 +1,12 @@ import requests from soar_sdk.abstract import SOARClient -from soar_sdk.action_results import ActionOutput, OutputField from soar_sdk.app import App from soar_sdk.asset import AssetField, BaseAsset from soar_sdk.exceptions import ActionFailure from soar_sdk.logging import getLogger -from soar_sdk.params import Param, Params - logger = getLogger() - class Asset(BaseAsset): base_url: str = AssetField( required=True, @@ -90,6 +86,14 @@ def test_connectivity(soar: SOARClient, asset: Asset) -> None: logger.info("Test connectivity passed!") +from .actions.http_get import http_get +app.register_action( + http_get, + description="Perform a REST GET call to the server", + action_type="investigate", + +) + if __name__ == "__main__": app.cli() From 4c86c97095cc9c63f660a9a5037a43e6872941ea Mon Sep 17 00:00:00 2001 From: msankows Date: Tue, 19 Aug 2025 14:21:54 +0200 Subject: [PATCH 04/29] important files --- CONTRIBUTING.md | 3 + LICENSE | 201 ++++++++++++++++++++ NOTICE | 32 ++++ README.md | 398 +++++++++++++++++++++++++++++++++++++++ manual_readme_content.md | 10 + 5 files changed, 644 insertions(+) create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 NOTICE create mode 100644 README.md create mode 100644 manual_readme_content.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c5b339e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# Contributing + +For more information about contributing to Splunk SOAR Apps please take a look at our app [Contribution Guide](https://github.com/splunk-soar-connectors/.github/blob/main/.github/CONTRIBUTING.md)! diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..aac0063 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) 2016-2025 Splunk Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..f5dd57e --- /dev/null +++ b/NOTICE @@ -0,0 +1,32 @@ +Splunk SOAR App: HTTP +Copyright (c) 2016-2025 Splunk Inc. +Third Party Software Attributions: + +@@@@============================================================================ + +Library: validators - 0.35.0 +Homepage: https://python-validators.github.io/validators +License: MIT License +License Text: + +The MIT License (MIT) + +Copyright (c) 2013 - 2025 Konsta Vesterinen + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..992e80d --- /dev/null +++ b/README.md @@ -0,0 +1,398 @@ +# HTTP + +Publisher: Splunk
+Connector Version: 3.7.8
+Product Vendor: Generic
+Product Name: HTTP
+Minimum Product Version: 6.2.1 + +This App facilitates making HTTP requests as actions + +For security reasons, accessing 127.0.0.1 is not allowed. + +This app requires access to port 80(for request send over HTTP) or port 443(for request send over +HTTPS) on your Phantom host(s) in order to function. + +**Authentication is carried out in following priority order** + +1. Basic Auth (username and password) +1. OAuth (oauth token url, client id and client secret) +1. Provided Auth token (auth_token_name, auth_token) + +### Configuration variables + +This table lists the configuration variables required to operate HTTP. These variables are specified when configuring a HTTP asset in Splunk SOAR. + +VARIABLE | REQUIRED | TYPE | DESCRIPTION +-------- | -------- | ---- | ----------- +**base_url** | required | string | Base URL for making queries. (e.g. https://myservice/) | +**test_path** | optional | string | Endpoint for test connectivity. (e.g. /some/specific/endpoint , appended to Base URL) | +**auth_token_name** | optional | string | Type of authentication token | +**auth_token** | optional | password | Value of authentication token | +**username** | optional | string | Username (for HTTP basic auth) | +**password** | optional | password | Password (for HTTP basic auth) | +**oauth_token_url** | optional | string | URL to fetch oauth token from | +**client_id** | optional | string | Client ID (for OAuth) | +**client_secret** | optional | password | Client Secret (for OAuth) | +**timeout** | optional | numeric | Timeout for HTTP calls | +**test_http_method** | optional | string | HTTP Method for Test Connectivity | + +### Supported Actions + +[test connectivity](#action-test-connectivity) - Validate connection using the configured credentials
+[put data](#action-put-data) - Perform a REST PUT call to the server
+[patch data](#action-patch-data) - Perform a REST PATCH call to the server
+[delete data](#action-delete-data) - Perform a REST DELETE call to the server
+[get headers](#action-get-headers) - Perform a REST HEAD call to the server
+[get options](#action-get-options) - Perform a REST OPTIONS call to the server
+[get data](#action-get-data) - Perform a REST GET call to the server
+[post data](#action-post-data) - Perform a REST POST call to the server
+[get file](#action-get-file) - Retrieve a file from the endpoint and save it to the vault
+[put file](#action-put-file) - Put a file from the vault to another location + +## action: 'test connectivity' + +Validate connection using the configured credentials + +Type: **test**
+Read only: **True** + +This action will perform a GET on the configured base_url. The action will succeed if given a status code between 200 and 399 (inclusive). Therefore, if the given base URL requires no authentication, this action may pass even if the supplied credentials are incorrect. Alternately, if the given base URL does not point to a valid endpoint, this action could fail even if other actions may succeed with valid location parameters. + +#### Action Parameters + +No parameters are required for this action + +#### Action Output + +No Output + +## action: 'put data' + +Perform a REST PUT call to the server + +Type: **generic**
+Read only: **False** + +#### Action Parameters + +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**location** | required | Location (e.g. path/to/endpoint?query=string) | string | `endpoint` | +**body** | required | PATCH body (query string, JSON, etc.) | string | | +**verify_certificate** | optional | Verify certificates (if using HTTPS) | boolean | | +**headers** | optional | Additional headers (JSON object with headers) | string | | + +#### Action Output + +DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES +--------- | ---- | -------- | -------------- +action_result.status | string | | success failed | +action_result.parameter.body | string | | {"name": "Bad IP"} | +action_result.parameter.headers | string | | {"Content-Type": "application/json"} | +action_result.parameter.location | string | `endpoint` | /rest/assets | +action_result.parameter.verify_certificate | boolean | | False True | +action_result.data.\*.location | string | `url` | http://192.168.1.26/rest/assets | +action_result.data.\*.method | string | | PUT | +action_result.data.\*.parsed_response_body | string | | {"failed": true, "message": "Requested item not found"} | +action_result.data.\*.response_body | string | | {"failed": true, "message": "Requested item not found"} | +action_result.data.\*.response_headers | string | | | +action_result.summary.reason | string | | Not Found | +action_result.summary.status_code | numeric | | 404 | +action_result.message | string | | Can't process response from server. Status Code: 404 Data from server: {"failed": true, "message": "Requested item not found"} | +summary.total_objects | numeric | | 1 | +summary.total_objects_successful | numeric | | 0 | + +## action: 'patch data' + +Perform a REST PATCH call to the server + +Type: **generic**
+Read only: **False** + +#### Action Parameters + +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**location** | required | Location (e.g. path/to/endpoint?query=string) | string | `endpoint` | +**body** | optional | PATCH body (query string, JSON, etc.) | string | | +**verify_certificate** | optional | Verify certificates (if using HTTPS) | boolean | | +**headers** | optional | Additional headers (JSON object with headers) | string | | + +#### Action Output + +DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES +--------- | ---- | -------- | -------------- +action_result.status | string | | success failed | +action_result.parameter.body | string | | {"name": "Bad IP"} | +action_result.parameter.headers | string | | {"Content-Type": "application/json"} | +action_result.parameter.location | string | `endpoint` | /rest/assets | +action_result.parameter.verify_certificate | boolean | | False True | +action_result.data.\*.location | string | `url` | http://192.168.1.26/rest/assets | +action_result.data.\*.method | string | | PATCH | +action_result.data.\*.parsed_response_body | string | | {"failed": true, "message": "Requested item not found"} | +action_result.data.\*.response_body | string | | {"failed": true, "message": "Requested item not found"} | +action_result.data.\*.response_headers | string | | | +action_result.summary.reason | string | | Not Found | +action_result.summary.status_code | numeric | | 404 | +action_result.message | string | | Can't process response from server. Status Code: 404 Data from server: {"failed": true, "message": "Requested item not found"} | +summary.total_objects | numeric | | 1 | +summary.total_objects_successful | numeric | | 0 | + +## action: 'delete data' + +Perform a REST DELETE call to the server + +Type: **generic**
+Read only: **False** + +#### Action Parameters + +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**location** | required | Location (e.g. path/to/endpoint?query=string) | string | `endpoint` | +**body** | optional | DELETE body (query string, JSON, etc.) | string | | +**verify_certificate** | optional | Verify certificates (if using HTTPS) | boolean | | +**headers** | optional | Additional headers (JSON object with headers) | string | | + +#### Action Output + +DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES +--------- | ---- | -------- | -------------- +action_result.status | string | | success failed | +action_result.parameter.body | string | | {"name": "Bad IP"} | +action_result.parameter.headers | string | | {"Content-Type": "application/json"} | +action_result.parameter.location | string | `endpoint` | /rest/assets | +action_result.parameter.verify_certificate | boolean | | False True | +action_result.data.\*.location | string | `url` | http://192.168.1.26/rest/assets | +action_result.data.\*.method | string | | DELETE | +action_result.data.\*.parsed_response_body | string | | {"failed": true, "message": "Requested item not found"} | +action_result.data.\*.response_body | string | | {"failed": true, "message": "Requested item not found"} | +action_result.data.\*.response_headers | string | | | +action_result.summary.reason | string | | Not Found | +action_result.summary.status_code | numeric | | 404 | +action_result.message | string | | Can't process response from server. Status Code: 404 Data from server: {"failed": true, "message": "Requested item not found"} | +summary.total_objects | numeric | | 1 | +summary.total_objects_successful | numeric | | 0 | + +## action: 'get headers' + +Perform a REST HEAD call to the server + +Type: **investigate**
+Read only: **True** + +#### Action Parameters + +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**location** | required | Location (e.g. path/to/endpoint?query=string) | string | `endpoint` | +**verify_certificate** | optional | Verify certificates (if using HTTPS) | boolean | | +**headers** | optional | Additional headers (JSON object with headers) | string | | + +#### Action Output + +DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES +--------- | ---- | -------- | -------------- +action_result.status | string | | success failed | +action_result.parameter.headers | string | | {"User-Agent": "automation"} | +action_result.parameter.location | string | `endpoint` | /rest/cont | +action_result.parameter.verify_certificate | boolean | | False True | +action_result.data.\*.location | string | `url` | http://192.168.1.26/rest/cont | +action_result.data.\*.method | string | | HEAD | +action_result.data.\*.response_headers | string | | | +action_result.summary.reason | string | | OK | +action_result.summary.status_code | numeric | | 200 | +action_result.message | string | | Status code: 200, Reason: OK | +summary.total_objects | numeric | | 1 | +summary.total_objects_successful | numeric | | 1 | + +## action: 'get options' + +Perform a REST OPTIONS call to the server + +Type: **investigate**
+Read only: **True** + +#### Action Parameters + +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**location** | required | Location (e.g. path/to/endpoint?query=string) | string | `endpoint` | +**verify_certificate** | optional | Verify certificates (if using HTTPS) | boolean | | +**headers** | optional | Additional headers (JSON object with headers) | string | | + +#### Action Output + +DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES +--------- | ---- | -------- | -------------- +action_result.status | string | | success failed | +action_result.parameter.headers | string | | | +action_result.parameter.location | string | `endpoint` | /rest/cont | +action_result.parameter.verify_certificate | boolean | | False True | +action_result.data.\*.location | string | `url` | http://192.168.1.26/rest/cont | +action_result.data.\*.method | string | | OPTIONS | +action_result.data.\*.parsed_response_body | string | | GET,HEAD,PUT,POST,DELETE,PATCH | +action_result.data.\*.response_body | string | | GET,HEAD,PUT,POST,DELETE,PATCH | +action_result.data.\*.response_headers | string | | | +action_result.summary.reason | string | | OK | +action_result.summary.status_code | numeric | | 200 | +action_result.message | string | | Status code: 200, Reason: OK | +summary.total_objects | numeric | | 1 | +summary.total_objects_successful | numeric | | 1 | + +## action: 'get data' + +Perform a REST GET call to the server + +Type: **investigate**
+Read only: **True** + +#### Action Parameters + +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**location** | required | Location (e.g. path/to/endpoint?query=string) | string | `endpoint` | +**verify_certificate** | optional | Verify certificates (if using HTTPS) | boolean | | +**headers** | optional | Additional headers (JSON object with headers) | string | | + +#### Action Output + +DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES +--------- | ---- | -------- | -------------- +action_result.status | string | | success failed | +action_result.parameter.headers | string | | {"Content-Type": "application/json"} | +action_result.parameter.location | string | `endpoint` | /rest/cont | +action_result.parameter.verify_certificate | boolean | | False True | +action_result.data.\*.location | string | `url` | http://192.168.1.26/rest/cont | +action_result.data.\*.method | string | | GET | +action_result.data.\*.parsed_response_body | string | | {"failed": true, "message": "Requested item not found"} | +action_result.data.\*.response_body | string | | {"failed": true, "message": "Requested item not found"} | +action_result.data.\*.response_headers | string | | | +action_result.summary.reason | string | | Not Found | +action_result.summary.status_code | numeric | | 404 | +action_result.message | string | | Can't process response from server. Status Code: 404 Data from server: {"failed": true, "message": "Requested item not found"} | +summary.total_objects | numeric | | 1 | +summary.total_objects_successful | numeric | | 0 | + +## action: 'post data' + +Perform a REST POST call to the server + +Type: **generic**
+Read only: **False** + +#### Action Parameters + +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**location** | required | Location (e.g. path/to/endpoint) | string | `endpoint` | +**body** | optional | POST body (query string, JSON, etc.) | string | | +**verify_certificate** | optional | Verify certificates (if using HTTPS) | boolean | | +**headers** | optional | Additional headers (JSON object with headers) | string | | + +#### Action Output + +DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES +--------- | ---- | -------- | -------------- +action_result.status | string | | success failed | +action_result.parameter.body | string | | {"name": "Bad IP"} | +action_result.parameter.headers | string | | {"Content-Type": "application/json"} | +action_result.parameter.location | string | `endpoint` | /rest/assets | +action_result.parameter.verify_certificate | boolean | | False True | +action_result.data.\*.location | string | `url` | http://192.168.1.26/rest/assets | +action_result.data.\*.method | string | | POST | +action_result.data.\*.parsed_response_body | string | | {"failed": true, "message": "Requested item not found"} | +action_result.data.\*.response_body | string | | {"failed": true, "message": "Requested item not found"} | +action_result.data.\*.response_headers | string | | | +action_result.summary.reason | string | | Not Found | +action_result.summary.status_code | numeric | | 404 | +action_result.message | string | | Can't process response from server. Status Code: 404 Data from server: {"failed": true, "message": "Requested item not found"} | +summary.total_objects | numeric | | 1 | +summary.total_objects_successful | numeric | | 0 | + +## action: 'get file' + +Retrieve a file from the endpoint and save it to the vault + +Type: **investigate**
+Read only: **True** + +Provide the file path and file name to download into the vault. For example, /web_storage/file.tgz. + +#### Action Parameters + +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**hostname** | optional | Hostname to execute command on | string | `host name` | +**file_path** | required | Path of the file to download (include filename) | string | `file path` | +**verify_certificate** | optional | Verify certificates (if using HTTPS) | boolean | | + +#### Action Output + +DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES +--------- | ---- | -------- | -------------- +action_result.status | string | | success failed | +action_result.parameter.file_path | string | `file path` | /web_storage/file.tgz | +action_result.parameter.verify_certificate | boolean | | False True | +action_result.parameter.hostname | string | `host name` | http://192.168.0.1 | +action_result.data | string | | | +action_result.summary.exit_status | numeric | | 0 | +action_result.summary.name | string | | file.tgz | +action_result.summary.size | numeric | | 412 | +action_result.summary.vault_id | string | `vault id` | dc871f816c4d179f3a3cea24b4bc81a67562c | +action_result.message | string | | Transferred file | +summary.total_objects | numeric | | 1 | +summary.total_objects_successful | numeric | | 1 | + +## action: 'put file' + +Put a file from the vault to another location + +Type: **generic**
+Read only: **False** + +Provide the path to store the file on the file server. For example, /web_storage/test_repo/. + +#### Action Parameters + +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**host** | optional | Hostname/IP with port number to execute command on | string | `host name` | +**vault_id** | required | Vault ID of file | string | `vault id` | +**file_destination** | required | File destination path (exclude filename) | string | `file path` | +**file_name** | optional | Name of the file to be put on endpoint | string | | +**verify_certificate** | optional | Verify certificates (if using HTTPS) | boolean | | + +#### Action Output + +DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES +--------- | ---- | -------- | -------------- +action_result.status | string | | success failed | +action_result.parameter.file_name | string | | test.txt | +action_result.parameter.file_destination | string | `file path` | /web_storage/test_repo/ | +action_result.parameter.host | string | `host name` | http://192.168.0.1:8001 | +action_result.parameter.verify_certificate | boolean | | False True | +action_result.parameter.vault_id | string | `vault id` | dc871f816c4d179f3a3cea24b4bc81a67562c | +action_result.data | string | | | +action_result.summary.file_sent | string | `file path` | http://192.168.0.1:8001/web_storage/test_repo/file.tgz | +action_result.message | string | | File sent: http://192.168.0.1:8001/web_storage/test_repo/file.tgz | +summary.total_objects | numeric | | 1 | +summary.total_objects_successful | numeric | | 1 | + +______________________________________________________________________ + +Auto-generated Splunk SOAR Connector documentation. + +Copyright 2025 Splunk Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and limitations under the License. diff --git a/manual_readme_content.md b/manual_readme_content.md new file mode 100644 index 0000000..c70b8a2 --- /dev/null +++ b/manual_readme_content.md @@ -0,0 +1,10 @@ +For security reasons, accessing 127.0.0.1 is not allowed. + +This app requires access to port 80(for request send over HTTP) or port 443(for request send over +HTTPS) on your Phantom host(s) in order to function. + +**Authentication is carried out in following priority order** + +1. Basic Auth (username and password) +1. OAuth (oauth token url, client id and client secret) +1. Provided Auth token (auth_token_name, auth_token) From 607d1b7bbc389d620d282ad98396b7b0b13e1b9f Mon Sep 17 00:00:00 2001 From: msankows Date: Tue, 19 Aug 2025 15:28:08 +0200 Subject: [PATCH 05/29] fix: added important files --- CONTRIBUTING.md | 3 + LICENSE | 201 ++++++++++++++++++ NOTICE | 32 +++ README.md | 398 ++++++++++++++++++++++++++++++++++++ manual_readme_content.md | 10 + package/pyproject.toml | 2 +- package/src/app.py | 3 - release_notes/2.0.10.md | 6 + release_notes/2.0.9.md | 6 + release_notes/2.1.10.md | 6 + release_notes/2.1.13.md | 7 + release_notes/2.1.15.md | 6 + release_notes/2.1.7.md | 8 + release_notes/3.0.15.md | 7 + release_notes/3.0.22.md | 8 + release_notes/3.1.3.md | 6 + release_notes/3.2.3.md | 6 + release_notes/3.3.2.md | 7 + release_notes/3.4.1.md | 7 + release_notes/3.5.0.md | 1 + release_notes/3.6.0.md | 2 + release_notes/3.7.0.md | 1 + release_notes/3.7.1.md | 1 + release_notes/3.7.2.md | 3 + release_notes/3.7.3.md | 5 + release_notes/3.7.4.md | 1 + release_notes/3.7.5.md | 1 + release_notes/3.7.6.md | 1 + release_notes/3.7.7.md | 1 + release_notes/3.7.8.md | 1 + release_notes/unreleased.md | 1 + 31 files changed, 744 insertions(+), 4 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 NOTICE create mode 100644 README.md create mode 100644 manual_readme_content.md create mode 100644 release_notes/2.0.10.md create mode 100644 release_notes/2.0.9.md create mode 100644 release_notes/2.1.10.md create mode 100644 release_notes/2.1.13.md create mode 100644 release_notes/2.1.15.md create mode 100644 release_notes/2.1.7.md create mode 100644 release_notes/3.0.15.md create mode 100644 release_notes/3.0.22.md create mode 100644 release_notes/3.1.3.md create mode 100644 release_notes/3.2.3.md create mode 100644 release_notes/3.3.2.md create mode 100644 release_notes/3.4.1.md create mode 100644 release_notes/3.5.0.md create mode 100644 release_notes/3.6.0.md create mode 100644 release_notes/3.7.0.md create mode 100644 release_notes/3.7.1.md create mode 100644 release_notes/3.7.2.md create mode 100644 release_notes/3.7.3.md create mode 100644 release_notes/3.7.4.md create mode 100644 release_notes/3.7.5.md create mode 100644 release_notes/3.7.6.md create mode 100644 release_notes/3.7.7.md create mode 100644 release_notes/3.7.8.md create mode 100644 release_notes/unreleased.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c5b339e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# Contributing + +For more information about contributing to Splunk SOAR Apps please take a look at our app [Contribution Guide](https://github.com/splunk-soar-connectors/.github/blob/main/.github/CONTRIBUTING.md)! diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..aac0063 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) 2016-2025 Splunk Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..f5dd57e --- /dev/null +++ b/NOTICE @@ -0,0 +1,32 @@ +Splunk SOAR App: HTTP +Copyright (c) 2016-2025 Splunk Inc. +Third Party Software Attributions: + +@@@@============================================================================ + +Library: validators - 0.35.0 +Homepage: https://python-validators.github.io/validators +License: MIT License +License Text: + +The MIT License (MIT) + +Copyright (c) 2013 - 2025 Konsta Vesterinen + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..992e80d --- /dev/null +++ b/README.md @@ -0,0 +1,398 @@ +# HTTP + +Publisher: Splunk
+Connector Version: 3.7.8
+Product Vendor: Generic
+Product Name: HTTP
+Minimum Product Version: 6.2.1 + +This App facilitates making HTTP requests as actions + +For security reasons, accessing 127.0.0.1 is not allowed. + +This app requires access to port 80(for request send over HTTP) or port 443(for request send over +HTTPS) on your Phantom host(s) in order to function. + +**Authentication is carried out in following priority order** + +1. Basic Auth (username and password) +1. OAuth (oauth token url, client id and client secret) +1. Provided Auth token (auth_token_name, auth_token) + +### Configuration variables + +This table lists the configuration variables required to operate HTTP. These variables are specified when configuring a HTTP asset in Splunk SOAR. + +VARIABLE | REQUIRED | TYPE | DESCRIPTION +-------- | -------- | ---- | ----------- +**base_url** | required | string | Base URL for making queries. (e.g. https://myservice/) | +**test_path** | optional | string | Endpoint for test connectivity. (e.g. /some/specific/endpoint , appended to Base URL) | +**auth_token_name** | optional | string | Type of authentication token | +**auth_token** | optional | password | Value of authentication token | +**username** | optional | string | Username (for HTTP basic auth) | +**password** | optional | password | Password (for HTTP basic auth) | +**oauth_token_url** | optional | string | URL to fetch oauth token from | +**client_id** | optional | string | Client ID (for OAuth) | +**client_secret** | optional | password | Client Secret (for OAuth) | +**timeout** | optional | numeric | Timeout for HTTP calls | +**test_http_method** | optional | string | HTTP Method for Test Connectivity | + +### Supported Actions + +[test connectivity](#action-test-connectivity) - Validate connection using the configured credentials
+[put data](#action-put-data) - Perform a REST PUT call to the server
+[patch data](#action-patch-data) - Perform a REST PATCH call to the server
+[delete data](#action-delete-data) - Perform a REST DELETE call to the server
+[get headers](#action-get-headers) - Perform a REST HEAD call to the server
+[get options](#action-get-options) - Perform a REST OPTIONS call to the server
+[get data](#action-get-data) - Perform a REST GET call to the server
+[post data](#action-post-data) - Perform a REST POST call to the server
+[get file](#action-get-file) - Retrieve a file from the endpoint and save it to the vault
+[put file](#action-put-file) - Put a file from the vault to another location + +## action: 'test connectivity' + +Validate connection using the configured credentials + +Type: **test**
+Read only: **True** + +This action will perform a GET on the configured base_url. The action will succeed if given a status code between 200 and 399 (inclusive). Therefore, if the given base URL requires no authentication, this action may pass even if the supplied credentials are incorrect. Alternately, if the given base URL does not point to a valid endpoint, this action could fail even if other actions may succeed with valid location parameters. + +#### Action Parameters + +No parameters are required for this action + +#### Action Output + +No Output + +## action: 'put data' + +Perform a REST PUT call to the server + +Type: **generic**
+Read only: **False** + +#### Action Parameters + +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**location** | required | Location (e.g. path/to/endpoint?query=string) | string | `endpoint` | +**body** | required | PATCH body (query string, JSON, etc.) | string | | +**verify_certificate** | optional | Verify certificates (if using HTTPS) | boolean | | +**headers** | optional | Additional headers (JSON object with headers) | string | | + +#### Action Output + +DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES +--------- | ---- | -------- | -------------- +action_result.status | string | | success failed | +action_result.parameter.body | string | | {"name": "Bad IP"} | +action_result.parameter.headers | string | | {"Content-Type": "application/json"} | +action_result.parameter.location | string | `endpoint` | /rest/assets | +action_result.parameter.verify_certificate | boolean | | False True | +action_result.data.\*.location | string | `url` | http://192.168.1.26/rest/assets | +action_result.data.\*.method | string | | PUT | +action_result.data.\*.parsed_response_body | string | | {"failed": true, "message": "Requested item not found"} | +action_result.data.\*.response_body | string | | {"failed": true, "message": "Requested item not found"} | +action_result.data.\*.response_headers | string | | | +action_result.summary.reason | string | | Not Found | +action_result.summary.status_code | numeric | | 404 | +action_result.message | string | | Can't process response from server. Status Code: 404 Data from server: {"failed": true, "message": "Requested item not found"} | +summary.total_objects | numeric | | 1 | +summary.total_objects_successful | numeric | | 0 | + +## action: 'patch data' + +Perform a REST PATCH call to the server + +Type: **generic**
+Read only: **False** + +#### Action Parameters + +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**location** | required | Location (e.g. path/to/endpoint?query=string) | string | `endpoint` | +**body** | optional | PATCH body (query string, JSON, etc.) | string | | +**verify_certificate** | optional | Verify certificates (if using HTTPS) | boolean | | +**headers** | optional | Additional headers (JSON object with headers) | string | | + +#### Action Output + +DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES +--------- | ---- | -------- | -------------- +action_result.status | string | | success failed | +action_result.parameter.body | string | | {"name": "Bad IP"} | +action_result.parameter.headers | string | | {"Content-Type": "application/json"} | +action_result.parameter.location | string | `endpoint` | /rest/assets | +action_result.parameter.verify_certificate | boolean | | False True | +action_result.data.\*.location | string | `url` | http://192.168.1.26/rest/assets | +action_result.data.\*.method | string | | PATCH | +action_result.data.\*.parsed_response_body | string | | {"failed": true, "message": "Requested item not found"} | +action_result.data.\*.response_body | string | | {"failed": true, "message": "Requested item not found"} | +action_result.data.\*.response_headers | string | | | +action_result.summary.reason | string | | Not Found | +action_result.summary.status_code | numeric | | 404 | +action_result.message | string | | Can't process response from server. Status Code: 404 Data from server: {"failed": true, "message": "Requested item not found"} | +summary.total_objects | numeric | | 1 | +summary.total_objects_successful | numeric | | 0 | + +## action: 'delete data' + +Perform a REST DELETE call to the server + +Type: **generic**
+Read only: **False** + +#### Action Parameters + +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**location** | required | Location (e.g. path/to/endpoint?query=string) | string | `endpoint` | +**body** | optional | DELETE body (query string, JSON, etc.) | string | | +**verify_certificate** | optional | Verify certificates (if using HTTPS) | boolean | | +**headers** | optional | Additional headers (JSON object with headers) | string | | + +#### Action Output + +DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES +--------- | ---- | -------- | -------------- +action_result.status | string | | success failed | +action_result.parameter.body | string | | {"name": "Bad IP"} | +action_result.parameter.headers | string | | {"Content-Type": "application/json"} | +action_result.parameter.location | string | `endpoint` | /rest/assets | +action_result.parameter.verify_certificate | boolean | | False True | +action_result.data.\*.location | string | `url` | http://192.168.1.26/rest/assets | +action_result.data.\*.method | string | | DELETE | +action_result.data.\*.parsed_response_body | string | | {"failed": true, "message": "Requested item not found"} | +action_result.data.\*.response_body | string | | {"failed": true, "message": "Requested item not found"} | +action_result.data.\*.response_headers | string | | | +action_result.summary.reason | string | | Not Found | +action_result.summary.status_code | numeric | | 404 | +action_result.message | string | | Can't process response from server. Status Code: 404 Data from server: {"failed": true, "message": "Requested item not found"} | +summary.total_objects | numeric | | 1 | +summary.total_objects_successful | numeric | | 0 | + +## action: 'get headers' + +Perform a REST HEAD call to the server + +Type: **investigate**
+Read only: **True** + +#### Action Parameters + +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**location** | required | Location (e.g. path/to/endpoint?query=string) | string | `endpoint` | +**verify_certificate** | optional | Verify certificates (if using HTTPS) | boolean | | +**headers** | optional | Additional headers (JSON object with headers) | string | | + +#### Action Output + +DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES +--------- | ---- | -------- | -------------- +action_result.status | string | | success failed | +action_result.parameter.headers | string | | {"User-Agent": "automation"} | +action_result.parameter.location | string | `endpoint` | /rest/cont | +action_result.parameter.verify_certificate | boolean | | False True | +action_result.data.\*.location | string | `url` | http://192.168.1.26/rest/cont | +action_result.data.\*.method | string | | HEAD | +action_result.data.\*.response_headers | string | | | +action_result.summary.reason | string | | OK | +action_result.summary.status_code | numeric | | 200 | +action_result.message | string | | Status code: 200, Reason: OK | +summary.total_objects | numeric | | 1 | +summary.total_objects_successful | numeric | | 1 | + +## action: 'get options' + +Perform a REST OPTIONS call to the server + +Type: **investigate**
+Read only: **True** + +#### Action Parameters + +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**location** | required | Location (e.g. path/to/endpoint?query=string) | string | `endpoint` | +**verify_certificate** | optional | Verify certificates (if using HTTPS) | boolean | | +**headers** | optional | Additional headers (JSON object with headers) | string | | + +#### Action Output + +DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES +--------- | ---- | -------- | -------------- +action_result.status | string | | success failed | +action_result.parameter.headers | string | | | +action_result.parameter.location | string | `endpoint` | /rest/cont | +action_result.parameter.verify_certificate | boolean | | False True | +action_result.data.\*.location | string | `url` | http://192.168.1.26/rest/cont | +action_result.data.\*.method | string | | OPTIONS | +action_result.data.\*.parsed_response_body | string | | GET,HEAD,PUT,POST,DELETE,PATCH | +action_result.data.\*.response_body | string | | GET,HEAD,PUT,POST,DELETE,PATCH | +action_result.data.\*.response_headers | string | | | +action_result.summary.reason | string | | OK | +action_result.summary.status_code | numeric | | 200 | +action_result.message | string | | Status code: 200, Reason: OK | +summary.total_objects | numeric | | 1 | +summary.total_objects_successful | numeric | | 1 | + +## action: 'get data' + +Perform a REST GET call to the server + +Type: **investigate**
+Read only: **True** + +#### Action Parameters + +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**location** | required | Location (e.g. path/to/endpoint?query=string) | string | `endpoint` | +**verify_certificate** | optional | Verify certificates (if using HTTPS) | boolean | | +**headers** | optional | Additional headers (JSON object with headers) | string | | + +#### Action Output + +DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES +--------- | ---- | -------- | -------------- +action_result.status | string | | success failed | +action_result.parameter.headers | string | | {"Content-Type": "application/json"} | +action_result.parameter.location | string | `endpoint` | /rest/cont | +action_result.parameter.verify_certificate | boolean | | False True | +action_result.data.\*.location | string | `url` | http://192.168.1.26/rest/cont | +action_result.data.\*.method | string | | GET | +action_result.data.\*.parsed_response_body | string | | {"failed": true, "message": "Requested item not found"} | +action_result.data.\*.response_body | string | | {"failed": true, "message": "Requested item not found"} | +action_result.data.\*.response_headers | string | | | +action_result.summary.reason | string | | Not Found | +action_result.summary.status_code | numeric | | 404 | +action_result.message | string | | Can't process response from server. Status Code: 404 Data from server: {"failed": true, "message": "Requested item not found"} | +summary.total_objects | numeric | | 1 | +summary.total_objects_successful | numeric | | 0 | + +## action: 'post data' + +Perform a REST POST call to the server + +Type: **generic**
+Read only: **False** + +#### Action Parameters + +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**location** | required | Location (e.g. path/to/endpoint) | string | `endpoint` | +**body** | optional | POST body (query string, JSON, etc.) | string | | +**verify_certificate** | optional | Verify certificates (if using HTTPS) | boolean | | +**headers** | optional | Additional headers (JSON object with headers) | string | | + +#### Action Output + +DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES +--------- | ---- | -------- | -------------- +action_result.status | string | | success failed | +action_result.parameter.body | string | | {"name": "Bad IP"} | +action_result.parameter.headers | string | | {"Content-Type": "application/json"} | +action_result.parameter.location | string | `endpoint` | /rest/assets | +action_result.parameter.verify_certificate | boolean | | False True | +action_result.data.\*.location | string | `url` | http://192.168.1.26/rest/assets | +action_result.data.\*.method | string | | POST | +action_result.data.\*.parsed_response_body | string | | {"failed": true, "message": "Requested item not found"} | +action_result.data.\*.response_body | string | | {"failed": true, "message": "Requested item not found"} | +action_result.data.\*.response_headers | string | | | +action_result.summary.reason | string | | Not Found | +action_result.summary.status_code | numeric | | 404 | +action_result.message | string | | Can't process response from server. Status Code: 404 Data from server: {"failed": true, "message": "Requested item not found"} | +summary.total_objects | numeric | | 1 | +summary.total_objects_successful | numeric | | 0 | + +## action: 'get file' + +Retrieve a file from the endpoint and save it to the vault + +Type: **investigate**
+Read only: **True** + +Provide the file path and file name to download into the vault. For example, /web_storage/file.tgz. + +#### Action Parameters + +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**hostname** | optional | Hostname to execute command on | string | `host name` | +**file_path** | required | Path of the file to download (include filename) | string | `file path` | +**verify_certificate** | optional | Verify certificates (if using HTTPS) | boolean | | + +#### Action Output + +DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES +--------- | ---- | -------- | -------------- +action_result.status | string | | success failed | +action_result.parameter.file_path | string | `file path` | /web_storage/file.tgz | +action_result.parameter.verify_certificate | boolean | | False True | +action_result.parameter.hostname | string | `host name` | http://192.168.0.1 | +action_result.data | string | | | +action_result.summary.exit_status | numeric | | 0 | +action_result.summary.name | string | | file.tgz | +action_result.summary.size | numeric | | 412 | +action_result.summary.vault_id | string | `vault id` | dc871f816c4d179f3a3cea24b4bc81a67562c | +action_result.message | string | | Transferred file | +summary.total_objects | numeric | | 1 | +summary.total_objects_successful | numeric | | 1 | + +## action: 'put file' + +Put a file from the vault to another location + +Type: **generic**
+Read only: **False** + +Provide the path to store the file on the file server. For example, /web_storage/test_repo/. + +#### Action Parameters + +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**host** | optional | Hostname/IP with port number to execute command on | string | `host name` | +**vault_id** | required | Vault ID of file | string | `vault id` | +**file_destination** | required | File destination path (exclude filename) | string | `file path` | +**file_name** | optional | Name of the file to be put on endpoint | string | | +**verify_certificate** | optional | Verify certificates (if using HTTPS) | boolean | | + +#### Action Output + +DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES +--------- | ---- | -------- | -------------- +action_result.status | string | | success failed | +action_result.parameter.file_name | string | | test.txt | +action_result.parameter.file_destination | string | `file path` | /web_storage/test_repo/ | +action_result.parameter.host | string | `host name` | http://192.168.0.1:8001 | +action_result.parameter.verify_certificate | boolean | | False True | +action_result.parameter.vault_id | string | `vault id` | dc871f816c4d179f3a3cea24b4bc81a67562c | +action_result.data | string | | | +action_result.summary.file_sent | string | `file path` | http://192.168.0.1:8001/web_storage/test_repo/file.tgz | +action_result.message | string | | File sent: http://192.168.0.1:8001/web_storage/test_repo/file.tgz | +summary.total_objects | numeric | | 1 | +summary.total_objects_successful | numeric | | 1 | + +______________________________________________________________________ + +Auto-generated Splunk SOAR Connector documentation. + +Copyright 2025 Splunk Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and limitations under the License. diff --git a/manual_readme_content.md b/manual_readme_content.md new file mode 100644 index 0000000..c70b8a2 --- /dev/null +++ b/manual_readme_content.md @@ -0,0 +1,10 @@ +For security reasons, accessing 127.0.0.1 is not allowed. + +This app requires access to port 80(for request send over HTTP) or port 443(for request send over +HTTPS) on your Phantom host(s) in order to function. + +**Authentication is carried out in following priority order** + +1. Basic Auth (username and password) +1. OAuth (oauth token url, client id and client secret) +1. Provided Auth token (auth_token_name, auth_token) diff --git a/package/pyproject.toml b/package/pyproject.toml index 1eca952..9a9d078 100644 --- a/package/pyproject.toml +++ b/package/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "http" -version = "1.0.0" +version = "4.0.0" description = "app" license = "Copyright (c) 2025 Splunk Inc." requires-python = ">=3.9, <3.14" diff --git a/package/src/app.py b/package/src/app.py index 2391949..69ffbda 100644 --- a/package/src/app.py +++ b/package/src/app.py @@ -1,13 +1,10 @@ import requests from soar_sdk.abstract import SOARClient -from soar_sdk.action_results import ActionOutput, OutputField from soar_sdk.app import App from soar_sdk.asset import AssetField, BaseAsset from soar_sdk.exceptions import ActionFailure from soar_sdk.logging import getLogger -from soar_sdk.params import Param, Params - logger = getLogger() diff --git a/release_notes/2.0.10.md b/release_notes/2.0.10.md new file mode 100644 index 0000000..953a3e3 --- /dev/null +++ b/release_notes/2.0.10.md @@ -0,0 +1,6 @@ +**HTTP Release Notes - Published by Splunk July 27, 2017** + + +**Version 2.0.10 - Released July 27, 2017** + +* 3.0 compatibility update release only diff --git a/release_notes/2.0.9.md b/release_notes/2.0.9.md new file mode 100644 index 0000000..c72019b --- /dev/null +++ b/release_notes/2.0.9.md @@ -0,0 +1,6 @@ +**HTTP Release Notes - Published by Splunk December 9, 2016** + + +**Version 2.0.9 - Released December 9, 2016** + +* Initial release diff --git a/release_notes/2.1.10.md b/release_notes/2.1.10.md new file mode 100644 index 0000000..ee416a1 --- /dev/null +++ b/release_notes/2.1.10.md @@ -0,0 +1,6 @@ +**HTTP Release Notes - Published by Splunk February 23, 2018** + + +**Version 2.1.10 - Released February 23, 2018** + +* Bug Fix to check for javascript in response headers diff --git a/release_notes/2.1.13.md b/release_notes/2.1.13.md new file mode 100644 index 0000000..9e5ef4b --- /dev/null +++ b/release_notes/2.1.13.md @@ -0,0 +1,7 @@ +**HTTP Release Notes - Published by Splunk July 25, 2018** + + +**Version 2.1.13 - Released July 25, 2018** + +* Bug fix on base URL validation with DNS lookups +* Bug fix on test connectivity when Phantom authorization token is used diff --git a/release_notes/2.1.15.md b/release_notes/2.1.15.md new file mode 100644 index 0000000..9ba0c1d --- /dev/null +++ b/release_notes/2.1.15.md @@ -0,0 +1,6 @@ +**HTTP Release Notes - Published by Splunk September 12, 2018** + + +**Version 2.1.15 - Released September 12, 2018** + +* Added option to support a specific path used by Test Connectivity diff --git a/release_notes/2.1.7.md b/release_notes/2.1.7.md new file mode 100644 index 0000000..f8e6555 --- /dev/null +++ b/release_notes/2.1.7.md @@ -0,0 +1,8 @@ +**HTTP Release Notes - Published by Splunk January 03, 2018** + + +**Version 2.1.7 - Released January 03, 2018** + +* Added Test Connectivity +* Added Timeout parameter for HTTP calls in the asset settings +* Added support for running requests without password configured diff --git a/release_notes/3.0.15.md b/release_notes/3.0.15.md new file mode 100644 index 0000000..7d9f622 --- /dev/null +++ b/release_notes/3.0.15.md @@ -0,0 +1,7 @@ +**HTTP Release Notes - Published by Splunk December 21, 2020** + + +**Version 3.0.15 - Released December 21, 2020** + +* Bug fixes for the 'post data' action [PAPP-16015] +* Compatibility changes for Python 3 support diff --git a/release_notes/3.0.22.md b/release_notes/3.0.22.md new file mode 100644 index 0000000..463bc65 --- /dev/null +++ b/release_notes/3.0.22.md @@ -0,0 +1,8 @@ +**HTTP Release Notes - Published by Splunk January 06, 2021** + + +**Version 3.0.22 - Released January 06, 2021** + +* Bug fixes in the 'get headers' action +* Improved output messages +* Updated app documentation diff --git a/release_notes/3.1.3.md b/release_notes/3.1.3.md new file mode 100644 index 0000000..e805421 --- /dev/null +++ b/release_notes/3.1.3.md @@ -0,0 +1,6 @@ +**HTTP Release Notes - Published by Splunk March 19, 2021** + + +**Version 3.1.3 - Released March 19, 2021** + +* Added French character support for the 'post data' action diff --git a/release_notes/3.2.3.md b/release_notes/3.2.3.md new file mode 100644 index 0000000..419f28f --- /dev/null +++ b/release_notes/3.2.3.md @@ -0,0 +1,6 @@ +**HTTP Release Notes - Published by Splunk October 19, 2021** + + +**Version 3.2.3 - Released October 19, 2021** + +* Added OAuth Authorization [PAPP-17274] diff --git a/release_notes/3.3.2.md b/release_notes/3.3.2.md new file mode 100644 index 0000000..d297497 --- /dev/null +++ b/release_notes/3.3.2.md @@ -0,0 +1,7 @@ +**HTTP Release Notes - Published by Splunk January 11, 2022** + + +**Version 3.3.2 - Released January 11, 2022** + +* Added 'get file' and 'put file' actions [PAPP-16071] +* Marked the app as FIPS Compliant [PAPP-21537] \ No newline at end of file diff --git a/release_notes/3.4.1.md b/release_notes/3.4.1.md new file mode 100644 index 0000000..675139c --- /dev/null +++ b/release_notes/3.4.1.md @@ -0,0 +1,7 @@ +**HTTP Release Notes - Published by Splunk February 03, 2022** + + +**Version 3.4.1 - Released February 03, 2022** + +* Added new asset configuration option to enable the 'test connectivity' action to send non-GET requests [PAPP_23570] +* Remove unwanted line breaks from output [PAPP_23533] \ No newline at end of file diff --git a/release_notes/3.5.0.md b/release_notes/3.5.0.md new file mode 100644 index 0000000..b39d3f7 --- /dev/null +++ b/release_notes/3.5.0.md @@ -0,0 +1 @@ +* Updated App documentation [PAPP-24696] \ No newline at end of file diff --git a/release_notes/3.6.0.md b/release_notes/3.6.0.md new file mode 100644 index 0000000..5e8109a --- /dev/null +++ b/release_notes/3.6.0.md @@ -0,0 +1,2 @@ +* Added basic authentication support for 'get file' and 'put file' actions [PAPP-26427] +* Updated the processing for 201 status code while using OAuth [PAPP-26849] diff --git a/release_notes/3.7.0.md b/release_notes/3.7.0.md new file mode 100644 index 0000000..ac0bb26 --- /dev/null +++ b/release_notes/3.7.0.md @@ -0,0 +1 @@ +* Changed the 'body' parameter to optional for the 'post data' action [PAPP-26977] \ No newline at end of file diff --git a/release_notes/3.7.1.md b/release_notes/3.7.1.md new file mode 100644 index 0000000..7549a0c --- /dev/null +++ b/release_notes/3.7.1.md @@ -0,0 +1 @@ +* Updated requests and certifi dependencies in order to use platform packages [PAPP-30822,PAPP-31096] \ No newline at end of file diff --git a/release_notes/3.7.2.md b/release_notes/3.7.2.md new file mode 100644 index 0000000..36310ae --- /dev/null +++ b/release_notes/3.7.2.md @@ -0,0 +1,3 @@ +* Fix an error where OAuth parameters were handled incorrectly [PAPP-34185] +* Fix a bug where expired access tokens cannot be refreshed [PAPP-34185] +* Fix a bug where not all request parameters were honored while refrehsing the access token [PAPP-34185] \ No newline at end of file diff --git a/release_notes/3.7.3.md b/release_notes/3.7.3.md new file mode 100644 index 0000000..46df760 --- /dev/null +++ b/release_notes/3.7.3.md @@ -0,0 +1,5 @@ + +* Update Python dependencies for vulnerabilities, package updates, and platform built-in removals +* Update Python dependencies for Python 3.13 support +* Update NOTICE file with updated dependencies +* Apply pre-commit fixes diff --git a/release_notes/3.7.4.md b/release_notes/3.7.4.md new file mode 100644 index 0000000..62b2fde --- /dev/null +++ b/release_notes/3.7.4.md @@ -0,0 +1 @@ +* PAPP-36204: Update vulnerability with validators wheel \ No newline at end of file diff --git a/release_notes/3.7.5.md b/release_notes/3.7.5.md new file mode 100644 index 0000000..43c84f0 --- /dev/null +++ b/release_notes/3.7.5.md @@ -0,0 +1 @@ +* Update Python version for 3.13 \ No newline at end of file diff --git a/release_notes/3.7.6.md b/release_notes/3.7.6.md new file mode 100644 index 0000000..3febf66 --- /dev/null +++ b/release_notes/3.7.6.md @@ -0,0 +1 @@ +* Update python 3.13 dependencies in the app json \ No newline at end of file diff --git a/release_notes/3.7.7.md b/release_notes/3.7.7.md new file mode 100644 index 0000000..04843ba --- /dev/null +++ b/release_notes/3.7.7.md @@ -0,0 +1 @@ +* Fix python_version 3.13 format \ No newline at end of file diff --git a/release_notes/3.7.8.md b/release_notes/3.7.8.md new file mode 100644 index 0000000..80b66fa --- /dev/null +++ b/release_notes/3.7.8.md @@ -0,0 +1 @@ +* Remove Python 3.13 from Python version \ No newline at end of file diff --git a/release_notes/unreleased.md b/release_notes/unreleased.md new file mode 100644 index 0000000..fbcb2fd --- /dev/null +++ b/release_notes/unreleased.md @@ -0,0 +1 @@ +**Unreleased** From beeb81544df588e536b1aef28d69ad387cf55d86 Mon Sep 17 00:00:00 2001 From: msankows Date: Wed, 20 Aug 2025 13:49:12 +0200 Subject: [PATCH 06/29] refactor: restructure app and improve http_get implementation --- package/logo.svg | 8 +- package/logo_dark.svg | 8 +- package/pyproject.toml | 2 +- .../actions/{http_get.py => action_get.py} | 86 ++++++++++++++----- package/src/app.py | 47 ++-------- package/src/asset.py | 38 ++++++++ package/src/helpers.py | 36 ++++++++ package/uv.lock | 19 +++- 8 files changed, 161 insertions(+), 83 deletions(-) rename package/src/actions/{http_get.py => action_get.py} (57%) create mode 100644 package/src/asset.py create mode 100644 package/src/helpers.py diff --git a/package/logo.svg b/package/logo.svg index 843059c..c67198d 100644 --- a/package/logo.svg +++ b/package/logo.svg @@ -1,7 +1 @@ - - - - Layer 1 - - - +Asset 1 diff --git a/package/logo_dark.svg b/package/logo_dark.svg index 6c702fc..9051579 100644 --- a/package/logo_dark.svg +++ b/package/logo_dark.svg @@ -1,7 +1 @@ - - - - Layer 1 - - - +Asset 2 diff --git a/package/pyproject.toml b/package/pyproject.toml index 1eca952..9a9d078 100644 --- a/package/pyproject.toml +++ b/package/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "http" -version = "1.0.0" +version = "4.0.0" description = "app" license = "Copyright (c) 2025 Splunk Inc." requires-python = ">=3.9, <3.14" diff --git a/package/src/actions/http_get.py b/package/src/actions/action_get.py similarity index 57% rename from package/src/actions/http_get.py rename to package/src/actions/action_get.py index b33ffd3..5e6d2c3 100644 --- a/package/src/actions/http_get.py +++ b/package/src/actions/action_get.py @@ -2,13 +2,16 @@ import json from soar_sdk.abstract import SOARClient from soar_sdk.action_results import ActionOutput, OutputField -from soar_sdk.app import App -from soar_sdk.asset import AssetField, BaseAsset from soar_sdk.exceptions import ActionFailure from soar_sdk.logging import getLogger from soar_sdk.params import Param, Params -from ..app import Asset +from ..asset import Asset +from .. import helpers + + +get_action_type = "investigate" +get_action_description = "This App facilitates making HTTP requests as actions" logger = getLogger() class GetDataOutput(ActionOutput): @@ -33,20 +36,39 @@ class GetDataParams(Params): verify_certificate: bool = Param(description="Verify certificates (if using HTTPS)") headers: str = Param(description="Additional headers (JSON object with headers)", required=False) + +RESPONSE_HANDLERS = { + "json": helpers.process_json_response, + "javascript": helpers.process_json_response, + "xml": helpers.process_xml_response, + "html": helpers.process_html_response, +} + + def http_get(params: GetDataParams, soar: SOARClient, asset: Asset) -> GetDataOutput: + logger.info(f"In action handler for: http_get with location: {params.location}") + + # logic to parsing headers (we need them to sent GET request): + parsed_headers = {} if params.headers: + try: parsed_headers = json.loads(params.headers) - if not isinstance(parsed_headers, dict): - raise ActionFailure("Headers parameter must be a valid JSON object (dictionary).") except json.JSONDecodeError as e: error_message = f"Failed to parse headers. Ensure it's a valid JSON object. Error: {e}" logger.error(error_message) raise ActionFailure(error_message) + if not isinstance(parsed_headers, dict): + raise ActionFailure("Headers parameter must be a valid JSON object (dictionary).") + + + + # sending GET request: + full_url = asset.base_url.rstrip("/") + "/" + params.location.lstrip('/') logger.info(f"Making GET request to: {full_url}") @@ -58,25 +80,43 @@ def http_get(params: GetDataParams, soar: SOARClient, asset: Asset) -> GetDataOu timeout=asset.timeout, ) response.raise_for_status() - - try: - parsed_body_object = response.json() - parsed_body_as_string = json.dumps(parsed_body_object, indent=4) - except requests.exceptions.JSONDecodeError: - parsed_body_as_string = response.text - - logger.info(f"Successfully fetched data. Status: {response.status_code}") - - return GetDataOutput( - location=full_url, - method="GET", - parsed_response_body=parsed_body_as_string, - response_body=response.text, - response_headers=str(dict(response.headers)) - ) - except requests.exceptions.RequestException as e: error_message = f"Failed to get data from {full_url}. Details: {e}" logger.error(error_message) raise ActionFailure(error_message) - \ No newline at end of file + + + + # logic to handle various types of responses (in output we have to have parsed body and raw_body): + + content_type = response.headers.get("Content-Type", "").lower() + + if not response.text.strip(): + parsed_body = helpers.process_empty_response(response) + else: + parser = helpers.process_text_response + for key, handler in RESPONSE_HANDLERS.items(): + if key in content_type: + logger.info(f"Found handler for content type: {key}") + parser = handler + break + + parsed_body = parser(response) + + if isinstance(parsed_body, dict): + raw_body = json.dumps(parsed_body, indent=4) + else: + raw_body = response.text + + logger.info(f"Successfully processed data. Status: {response.status_code}") + + + #output: + + return GetDataOutput( + location=full_url, + method="GET", + parsed_response_body=str(parsed_body), + response_body=raw_body, + response_headers=str(dict(response.headers)) + ) \ No newline at end of file diff --git a/package/src/app.py b/package/src/app.py index a8c16a2..5ba072a 100644 --- a/package/src/app.py +++ b/package/src/app.py @@ -1,48 +1,13 @@ import requests from soar_sdk.abstract import SOARClient from soar_sdk.app import App -from soar_sdk.asset import AssetField, BaseAsset from soar_sdk.exceptions import ActionFailure from soar_sdk.logging import getLogger -logger = getLogger() +from .actions.action_get import http_get, get_action_type, get_action_description +from .asset import Asset -class Asset(BaseAsset): - base_url: str = AssetField( - required=True, - description="Base URL for making queries. (e.g. https://myservice/)", - ) - test_path: str = AssetField( - required=False, - description="Endpoint for test connectivity. (e.g. /some/specific/endpoint , appended to Base URL)", - ) - auth_token_name: str = AssetField( - required=False, - description="Type of authentication token", - default="ph-auth-token", - ) - auth_token: str = AssetField(required=False, description="Value of authentication token") - username: str = AssetField(required=False, description="Username (for HTTP basic auth)") - password: str = AssetField(required=False, description="Password (for HTTP basic auth)") - oauth_token_url: str = AssetField(required=False, description="URL to fetch oauth token from") - client_id: str = AssetField(required=False, description="Client ID (for OAuth)") - client_secret: str = AssetField(required=False, description="Client Secret (for OAuth)") - timeout: float = AssetField(required=False, description="Timeout for HTTP calls") - test_http_method: str = AssetField( - required=False, - description="HTTP Method for Test Connectivity", - default="GET", - value_list=[ - "GET", - "HEAD", - "POST", - "PUT", - "DELETE", - "OPTIONS", - "TRACE", - "PATCH", - ], - ) +logger = getLogger() app = App( @@ -86,12 +51,10 @@ def test_connectivity(soar: SOARClient, asset: Asset) -> None: logger.info("Test connectivity passed!") -from .actions.http_get import http_get app.register_action( http_get, - description="Perform a REST GET call to the server", - action_type="investigate", - + action_type = get_action_type, + description = get_action_description ) if __name__ == "__main__": diff --git a/package/src/asset.py b/package/src/asset.py new file mode 100644 index 0000000..22fe0ea --- /dev/null +++ b/package/src/asset.py @@ -0,0 +1,38 @@ +from soar_sdk.asset import AssetField, BaseAsset + +class Asset(BaseAsset): + base_url: str = AssetField( + required=True, + description="Base URL for making queries. (e.g. https://myservice/)", + ) + test_path: str = AssetField( + required=False, + description="Endpoint for test connectivity. (e.g. /some/specific/endpoint , appended to Base URL)", + ) + auth_token_name: str = AssetField( + required=False, + description="Type of authentication token", + default="ph-auth-token", + ) + auth_token: str = AssetField(required=False, description="Value of authentication token") + username: str = AssetField(required=False, description="Username (for HTTP basic auth)") + password: str = AssetField(required=False, description="Password (for HTTP basic auth)") + oauth_token_url: str = AssetField(required=False, description="URL to fetch oauth token from") + client_id: str = AssetField(required=False, description="Client ID (for OAuth)") + client_secret: str = AssetField(required=False, description="Client Secret (for OAuth)") + timeout: float = AssetField(required=False, description="Timeout for HTTP calls") + test_http_method: str = AssetField( + required=False, + description="HTTP Method for Test Connectivity", + default="GET", + value_list=[ + "GET", + "HEAD", + "POST", + "PUT", + "DELETE", + "OPTIONS", + "TRACE", + "PATCH", + ], + ) \ No newline at end of file diff --git a/package/src/helpers.py b/package/src/helpers.py new file mode 100644 index 0000000..9870f60 --- /dev/null +++ b/package/src/helpers.py @@ -0,0 +1,36 @@ +import json +from bs4 import BeautifulSoup +import xmltodict +from soar_sdk.exceptions import ActionFailure + + + +def process_xml_response(response) -> dict: + try: + return xmltodict.parse(response.text) + except Exception as e: + raise ActionFailure(f"Unable to parse XML response. Error: {e}") + +def process_json_response(response) -> dict: + try: + return response.json() + except json.JSONDecodeError as e: + raise ActionFailure(f"Unable to parse JSON response. Error: {e}") + +def process_html_response(response) -> str: + try: + soup = BeautifulSoup(response.text, "html.parser") + for element in soup(["script", "style", "footer", "nav"]): + element.extract() + error_text_lines = [x.strip() for x in soup.text.split("\n") if x.strip()] + return "\n".join(error_text_lines) + + except Exception as e: + raise ActionFailure(f"Unable to parse HTML response. Error: {e}") + +def process_empty_response(content_type) -> dict: + message = "Response includes a file" if "octet-stream" in content_type else "Empty response body" + return {"message": message} + +def process_text_response(response) -> str: + return response.text diff --git a/package/uv.lock b/package/uv.lock index f32d897..726a450 100644 --- a/package/uv.lock +++ b/package/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.9, <3.14" resolution-markers = [ "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'", @@ -256,10 +256,11 @@ wheels = [ [[package]] name = "http" -version = "1.0.0" +version = "4.0.0" source = { virtual = "." } dependencies = [ { name = "splunk-soar-sdk", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "xmltodict", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] [package.dev-dependencies] @@ -274,7 +275,10 @@ dev = [ ] [package.metadata] -requires-dist = [{ name = "splunk-soar-sdk", specifier = ">=1.0.0b55" }] +requires-dist = [ + { name = "splunk-soar-sdk", specifier = ">=1.0.0b55" }, + { name = "xmltodict" }, +] [package.metadata.requires-dev] dev = [ @@ -920,3 +924,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda5308 wheels = [ { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, ] + +[[package]] +name = "xmltodict" +version = "0.14.2" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/50/05/51dcca9a9bf5e1bce52582683ce50980bcadbc4fa5143b9f2b19ab99958f/xmltodict-0.14.2.tar.gz", hash = "sha256:201e7c28bb210e374999d1dde6382923ab0ed1a8a5faeece48ab525b7810a553", size = 51942, upload-time = "2024-10-16T06:10:29.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/45/fc303eb433e8a2a271739c98e953728422fa61a3c1f36077a49e395c972e/xmltodict-0.14.2-py2.py3-none-any.whl", hash = "sha256:20cc7d723ed729276e808f26fb6b3599f786cbc37e06c65e192ba77c40f20aac", size = 9981, upload-time = "2024-10-16T06:10:27.649Z" }, +] From 29f79d4e51425b84c2ec9f93303dc7223d077e4a Mon Sep 17 00:00:00 2001 From: msankows Date: Wed, 20 Aug 2025 15:21:07 +0200 Subject: [PATCH 07/29] refactor: Improve code architecture and modularity --- package/src/actions/action_get.py | 57 ++++--------------------------- package/src/app.py | 2 +- package/src/common.py | 3 ++ package/src/helpers.py | 46 ++++++++++++++++++++++++- 4 files changed, 55 insertions(+), 53 deletions(-) create mode 100644 package/src/common.py diff --git a/package/src/actions/action_get.py b/package/src/actions/action_get.py index 5e6d2c3..889c864 100644 --- a/package/src/actions/action_get.py +++ b/package/src/actions/action_get.py @@ -7,12 +7,12 @@ from soar_sdk.params import Param, Params from ..asset import Asset from .. import helpers - +from ..common import logger get_action_type = "investigate" get_action_description = "This App facilitates making HTTP requests as actions" -logger = getLogger() + class GetDataOutput(ActionOutput): location: str = OutputField( @@ -37,37 +37,12 @@ class GetDataParams(Params): headers: str = Param(description="Additional headers (JSON object with headers)", required=False) -RESPONSE_HANDLERS = { - "json": helpers.process_json_response, - "javascript": helpers.process_json_response, - "xml": helpers.process_xml_response, - "html": helpers.process_html_response, -} - - def http_get(params: GetDataParams, soar: SOARClient, asset: Asset) -> GetDataOutput: logger.info(f"In action handler for: http_get with location: {params.location}") - - # logic to parsing headers (we need them to sent GET request): - - parsed_headers = {} - if params.headers: - - try: - parsed_headers = json.loads(params.headers) - except json.JSONDecodeError as e: - error_message = f"Failed to parse headers. Ensure it's a valid JSON object. Error: {e}" - logger.error(error_message) - raise ActionFailure(error_message) - - if not isinstance(parsed_headers, dict): - raise ActionFailure("Headers parameter must be a valid JSON object (dictionary).") - - - - # sending GET request: + parsed_headers = helpers.parse_headers(params.headers) + full_url = asset.base_url.rstrip("/") + "/" + params.location.lstrip('/') logger.info(f"Making GET request to: {full_url}") @@ -86,33 +61,13 @@ def http_get(params: GetDataParams, soar: SOARClient, asset: Asset) -> GetDataOu raise ActionFailure(error_message) + parsed_body = helpers.handle_various_response(response) - # logic to handle various types of responses (in output we have to have parsed body and raw_body): - - content_type = response.headers.get("Content-Type", "").lower() - - if not response.text.strip(): - parsed_body = helpers.process_empty_response(response) - else: - parser = helpers.process_text_response - for key, handler in RESPONSE_HANDLERS.items(): - if key in content_type: - logger.info(f"Found handler for content type: {key}") - parser = handler - break - - parsed_body = parser(response) - - if isinstance(parsed_body, dict): - raw_body = json.dumps(parsed_body, indent=4) - else: - raw_body = response.text + raw_body = json.dumps(parsed_body, indent=4) if isinstance(parsed_body, dict) else response.text logger.info(f"Successfully processed data. Status: {response.status_code}") - #output: - return GetDataOutput( location=full_url, method="GET", diff --git a/package/src/app.py b/package/src/app.py index 5ba072a..4e1f095 100644 --- a/package/src/app.py +++ b/package/src/app.py @@ -7,7 +7,7 @@ from .actions.action_get import http_get, get_action_type, get_action_description from .asset import Asset -logger = getLogger() +from .common import logger app = App( diff --git a/package/src/common.py b/package/src/common.py new file mode 100644 index 0000000..e0299aa --- /dev/null +++ b/package/src/common.py @@ -0,0 +1,3 @@ +from soar_sdk.logging import getLogger + +logger = getLogger() \ No newline at end of file diff --git a/package/src/helpers.py b/package/src/helpers.py index 9870f60..34d1b48 100644 --- a/package/src/helpers.py +++ b/package/src/helpers.py @@ -3,7 +3,7 @@ import xmltodict from soar_sdk.exceptions import ActionFailure - +from .common import logger def process_xml_response(response) -> dict: try: @@ -34,3 +34,47 @@ def process_empty_response(content_type) -> dict: def process_text_response(response) -> str: return response.text + +RESPONSE_HANDLERS = { + "json": process_json_response, + "javascript": process_json_response, + "xml": process_xml_response, + "html": process_html_response, +} + + +def parse_headers(headers_str: str | None) -> dict: + + parsed_headers = {} + if headers_str: + + try: + parsed_headers = json.loads(headers_str) + + except json.JSONDecodeError as e: + error_message = f"Failed to parse headers. Ensure it's a valid JSON object. Error: {e}" + logger.error(error_message) + raise ActionFailure(error_message) + + if not isinstance(parsed_headers, dict): + raise ActionFailure("Headers parameter must be a valid JSON object (dictionary).") + + return parsed_headers + + +def handle_various_response(response): + content_type = response.headers.get("Content-Type", "").lower() + + if not response.text.strip(): + parsed_body = process_empty_response(response) + else: + parser = process_text_response + for key, handler in RESPONSE_HANDLERS.items(): + if key in content_type: + logger.info(f"Found handler for content type: {key}") + parser = handler + break + + parsed_body = parser(response) + + return parsed_body \ No newline at end of file From 57773a11fa9cb520c4334d84379e6fdb5eab5cf7 Mon Sep 17 00:00:00 2001 From: msankows Date: Wed, 20 Aug 2025 15:53:05 +0200 Subject: [PATCH 08/29] refactor: improve header parsing --- package/src/helpers.py | 48 ++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/package/src/helpers.py b/package/src/helpers.py index 34d1b48..9c07561 100644 --- a/package/src/helpers.py +++ b/package/src/helpers.py @@ -19,11 +19,11 @@ def process_json_response(response) -> dict: def process_html_response(response) -> str: try: - soup = BeautifulSoup(response.text, "html.parser") - for element in soup(["script", "style", "footer", "nav"]): - element.extract() - error_text_lines = [x.strip() for x in soup.text.split("\n") if x.strip()] - return "\n".join(error_text_lines) + soup = BeautifulSoup(response.text, "html.parser") + for element in soup(["script", "style", "footer", "nav"]): + element.extract() + error_text_lines = [x.strip() for x in soup.text.split("\n") if x.strip()] + return "\n".join(error_text_lines) except Exception as e: raise ActionFailure(f"Unable to parse HTML response. Error: {e}") @@ -45,16 +45,16 @@ def process_text_response(response) -> str: def parse_headers(headers_str: str | None) -> dict: - parsed_headers = {} - if headers_str: + if not headers_str: + return {} - try: - parsed_headers = json.loads(headers_str) + try: + parsed_headers = json.loads(headers_str) - except json.JSONDecodeError as e: - error_message = f"Failed to parse headers. Ensure it's a valid JSON object. Error: {e}" - logger.error(error_message) - raise ActionFailure(error_message) + except json.JSONDecodeError as e: + error_message = f"Failed to parse headers. Ensure it's a valid JSON object. Error: {e}" + logger.error(error_message) + raise ActionFailure(error_message) if not isinstance(parsed_headers, dict): raise ActionFailure("Headers parameter must be a valid JSON object (dictionary).") @@ -66,15 +66,13 @@ def handle_various_response(response): content_type = response.headers.get("Content-Type", "").lower() if not response.text.strip(): - parsed_body = process_empty_response(response) - else: - parser = process_text_response - for key, handler in RESPONSE_HANDLERS.items(): - if key in content_type: - logger.info(f"Found handler for content type: {key}") - parser = handler - break - - parsed_body = parser(response) - - return parsed_body \ No newline at end of file + return process_empty_response(response) + + parser = process_text_response(response) + for key, handler in RESPONSE_HANDLERS.items(): + if key in content_type: + logger.info(f"Found handler for content type: {key}") + parser = handler + break + + return parser(response) \ No newline at end of file From eb027b557576004738cf102738e3ae45526ec568 Mon Sep 17 00:00:00 2001 From: msankows Date: Tue, 26 Aug 2025 15:27:47 +0200 Subject: [PATCH 09/29] feat: POST action --- ...mit-config.yml => .pre-commit-config.yaml} | 0 package/src/actions/action_get.py | 80 +++++------- package/src/actions/action_post.py | 43 ++++++ package/src/app.py | 20 +-- package/src/auth.py | 122 ++++++++++++++++++ package/src/helpers.py | 23 ++-- package/src/request_maker.py | 66 ++++++++++ 7 files changed, 285 insertions(+), 69 deletions(-) rename package/{.pre-commit-config.yml => .pre-commit-config.yaml} (100%) create mode 100644 package/src/actions/action_post.py create mode 100644 package/src/auth.py create mode 100644 package/src/request_maker.py diff --git a/package/.pre-commit-config.yml b/package/.pre-commit-config.yaml similarity index 100% rename from package/.pre-commit-config.yml rename to package/.pre-commit-config.yaml diff --git a/package/src/actions/action_get.py b/package/src/actions/action_get.py index 889c864..538dcc7 100644 --- a/package/src/actions/action_get.py +++ b/package/src/actions/action_get.py @@ -1,32 +1,36 @@ -import requests -import json +# Copyright (c) 2025 Splunk Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. from soar_sdk.abstract import SOARClient from soar_sdk.action_results import ActionOutput, OutputField -from soar_sdk.exceptions import ActionFailure -from soar_sdk.logging import getLogger from soar_sdk.params import Param, Params + from ..asset import Asset -from .. import helpers from ..common import logger - +from ..request_maker import make_request get_action_type = "investigate" get_action_description = "This App facilitates making HTTP requests as actions" class GetDataOutput(ActionOutput): - location: str = OutputField( - cef_types=["url"], example_values=["http://192.168.1.26/rest/cont"] - ) + location: str = OutputField(cef_types=["url"], example_values=["http://192.168.1.26/rest/cont"]) method: str = OutputField(example_values=["GET"]) - parsed_response_body: str = OutputField( - example_values=['{"failed": true, "message": "Requested item not found"}'] - ) - response_body: str = OutputField( - example_values=['{"failed": true, "message": "Requested item not found"}'] - ) + parsed_response_body: str = OutputField(example_values=['{"failed": true, "message": "Requested item not found"}']) + response_body: str = OutputField(example_values=['{"failed": true, "message": "Requested item not found"}']) response_headers: str + class GetDataParams(Params): location: str = Param( description="Location (e.g. path/to/endpoint?query=string)", @@ -38,40 +42,14 @@ class GetDataParams(Params): def http_get(params: GetDataParams, soar: SOARClient, asset: Asset) -> GetDataOutput: - - logger.info(f"In action handler for: http_get with location: {params.location}") - - parsed_headers = helpers.parse_headers(params.headers) - - - full_url = asset.base_url.rstrip("/") + "/" + params.location.lstrip('/') - logger.info(f"Making GET request to: {full_url}") - - try: - response = requests.get( - uri=full_url, - headers=parsed_headers, - verify=params.verify_certificate, - timeout=asset.timeout, - ) - response.raise_for_status() - except requests.exceptions.RequestException as e: - error_message = f"Failed to get data from {full_url}. Details: {e}" - logger.error(error_message) - raise ActionFailure(error_message) - - - parsed_body = helpers.handle_various_response(response) - - raw_body = json.dumps(parsed_body, indent=4) if isinstance(parsed_body, dict) else response.text - - logger.info(f"Successfully processed data. Status: {response.status_code}") - - - return GetDataOutput( - location=full_url, + logger.info("In action handler for: http_get") + return make_request( + asset=asset, + soar=soar, method="GET", - parsed_response_body=str(parsed_body), - response_body=raw_body, - response_headers=str(dict(response.headers)) - ) \ No newline at end of file + location=params.location, + headers=params.headers, + verify=params.verify_certificate, + output=GetDataOutput, + body=None, + ) diff --git a/package/src/actions/action_post.py b/package/src/actions/action_post.py new file mode 100644 index 0000000..b0572c3 --- /dev/null +++ b/package/src/actions/action_post.py @@ -0,0 +1,43 @@ +from soar_sdk.abstract import SOARClient +from soar_sdk.action_results import ActionOutput, OutputField +from soar_sdk.params import Param, Params + +from ..asset import Asset +from ..common import logger +from ..request_maker import make_request + +post_action_type = "generic" +post_action_description = "Perform a REST POST call to the server" + + +class PostDataOutput(ActionOutput): + location: str = OutputField(cef_types=["url"], example_values=["http://192.168.1.26/rest/assets"]) + method: str = OutputField(example_values=["POST"]) + parsed_response_body: str = OutputField(example_values=['{"failed": true, "message": "Requested item not found"}']) + response_body: str = OutputField(example_values=['{"failed": true, "message": "Requested item not found"}']) + response_headers: str + + +class PostDataParams(Params): + location: str = Param( + description="Location (e.g. path/to/endpoint)", + primary=True, + cef_types=["endpoint"], + ) + body: str = Param(description="POST body (query string, JSON, etc.)") + verify_certificate: bool = Param(description="Verify certificates (if using HTTPS)") + headers: str = Param(description="Additional headers (JSON object with headers)") + + +def http_post(params: PostDataParams, soar: SOARClient, asset: Asset) -> PostDataOutput: + logger.info("In action handler for: http_post") + return make_request( + asset=asset, + soar=soar, + method="POST", + location=params.location, + headers=params.headers, + verify=params.verify_certificate, + body=params.body, + output=PostDataOutput, + ) diff --git a/package/src/app.py b/package/src/app.py index 4e1f095..81e346c 100644 --- a/package/src/app.py +++ b/package/src/app.py @@ -2,14 +2,12 @@ from soar_sdk.abstract import SOARClient from soar_sdk.app import App from soar_sdk.exceptions import ActionFailure -from soar_sdk.logging import getLogger -from .actions.action_get import http_get, get_action_type, get_action_description +from .actions.action_get import get_action_description, get_action_type, http_get +from .actions.action_post import http_post, post_action_description, post_action_type from .asset import Asset - from .common import logger - app = App( name="http", app_type="generic", @@ -33,7 +31,6 @@ def test_connectivity(soar: SOARClient, asset: Asset) -> None: full_url = full_url + "/" + asset.test_path.lstrip("/") logger.info(f"Querying base url, {full_url}, to test credentials.") - try: response = requests.request( method=asset.test_http_method, @@ -44,19 +41,22 @@ def test_connectivity(soar: SOARClient, asset: Asset) -> None: logger.info(f"Got status code {response.status_code}.") response.raise_for_status() - except requests.exceptions.RequestException as e: logger.error(f"Test connectivity failed, error: {e}.") raise ActionFailure(f"Test connectivity failed, details: {e}.") logger.info("Test connectivity passed!") + +app.register_action(http_get, action_type=get_action_type, description=get_action_description) + app.register_action( - http_get, - action_type = get_action_type, - description = get_action_description + http_post, + action_type=post_action_type, + description=post_action_description, + read_only=False, ) + if __name__ == "__main__": app.cli() - diff --git a/package/src/auth.py b/package/src/auth.py new file mode 100644 index 0000000..c14126f --- /dev/null +++ b/package/src/auth.py @@ -0,0 +1,122 @@ +# Copyright (c) 2025 Splunk Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import json +from abc import ABC, abstractmethod +from typing import Optional, Tuple + +import requests +from requests.auth import AuthBase, HTTPBasicAuth +from soar_sdk.exceptions import ActionFailure + + +class Authorization(ABC): + @abstractmethod + def create_auth(self, headers) -> Tuple[Optional[AuthBase], dict]: + pass + + +class BasicAuth(Authorization): + def __init__(self, asset): + self.username = asset.username + self.password = asset.password + + def create_auth(self, headers): + return (self.username, self.password), headers + + # tu logger "Using HTTP Basic auth to authenticate" + + +class TokenAuth(Authorization): + def __init__(self, asset): + self.auth_token_name = asset.auth_token_name + self.auth_token = asset.auth_token + + def create_auth(self, headers): + # tu logger "Using provided token to authenticate" + if self.auth_token and self.auth_token_name not in headers: + headers[self.auth_token_name] = self.auth_token + return None, headers + + +class OAuth(Authorization): + def __init__(self, asset, soar_client): + self.asset = asset + self.soar = soar_client + self.state_key = f"oauth_token_{asset.asset_id}" + + def __generate_new_token(self): + token_url = self.asset.oauth_token_url + client_id = self.asset.client_id + client_secret = self.asset.client_secret + + payload = {"grant_type": "client_credentials"} + + try: + response = requests.post( + token_url, + auth=HTTPBasicAuth(client_id, client_secret), + data=payload, + timeout=30, + ) + response.raise_for_status() + + access_token = json.loads(response.text).get("access_token") + + except requests.exceptions.RequestException as e: + raise ActionFailure(f"Error fetching OAuth token from {token_url}. Details: {e}") from e + except json.JSONDecodeError as e: + raise ActionFailure("Error parsing response from server while fetching token") from e + + if not access_token: + raise ActionFailure("Access token not found in response body") + + self.soar.set_state({self.state_key: access_token}) + + return access_token + + def __get_token(self) -> str: + state = self.soar.get_state() + cached_token = state.get(self.state_key) + + if cached_token: + return cached_token + + return self.__generate_new_token() + + def create_auth(self, headers: dict) -> tuple[None, dict]: + # tu logger + access_token = self.__get_token() + + headers["Authorization"] = f"Bearer {access_token}" + + return None, headers + + +class NoAuth(Authorization): + def __init__(self, asset): + pass + + def create_auth(self, headers): + # to logger + return None, headers + + +def get_auth_method(asset, soar_client): + if asset.username and asset.password: + return BasicAuth(asset) + elif asset.auth_token_name and asset.auth_token: + return TokenAuth(asset) + elif asset.oauth_token_url and asset.client_id: + return OAuth(asset, soar_client) + return NoAuth(asset) diff --git a/package/src/helpers.py b/package/src/helpers.py index 9c07561..39377fe 100644 --- a/package/src/helpers.py +++ b/package/src/helpers.py @@ -1,22 +1,27 @@ import json -from bs4 import BeautifulSoup +from typing import Optional + import xmltodict +from bs4 import BeautifulSoup from soar_sdk.exceptions import ActionFailure from .common import logger + def process_xml_response(response) -> dict: try: return xmltodict.parse(response.text) except Exception as e: raise ActionFailure(f"Unable to parse XML response. Error: {e}") - + + def process_json_response(response) -> dict: try: return response.json() except json.JSONDecodeError as e: raise ActionFailure(f"Unable to parse JSON response. Error: {e}") + def process_html_response(response) -> str: try: soup = BeautifulSoup(response.text, "html.parser") @@ -24,17 +29,20 @@ def process_html_response(response) -> str: element.extract() error_text_lines = [x.strip() for x in soup.text.split("\n") if x.strip()] return "\n".join(error_text_lines) - + except Exception as e: raise ActionFailure(f"Unable to parse HTML response. Error: {e}") + def process_empty_response(content_type) -> dict: message = "Response includes a file" if "octet-stream" in content_type else "Empty response body" return {"message": message} + def process_text_response(response) -> str: return response.text + RESPONSE_HANDLERS = { "json": process_json_response, "javascript": process_json_response, @@ -43,8 +51,7 @@ def process_text_response(response) -> str: } -def parse_headers(headers_str: str | None) -> dict: - +def parse_headers(headers_str: Optional[str]) -> dict: if not headers_str: return {} @@ -58,7 +65,7 @@ def parse_headers(headers_str: str | None) -> dict: if not isinstance(parsed_headers, dict): raise ActionFailure("Headers parameter must be a valid JSON object (dictionary).") - + return parsed_headers @@ -74,5 +81,5 @@ def handle_various_response(response): logger.info(f"Found handler for content type: {key}") parser = handler break - - return parser(response) \ No newline at end of file + + return parser(response) diff --git a/package/src/request_maker.py b/package/src/request_maker.py new file mode 100644 index 0000000..4c48481 --- /dev/null +++ b/package/src/request_maker.py @@ -0,0 +1,66 @@ +import json +from typing import Optional + +import requests +from bs4 import UnicodeDammit +from soar_sdk.abstract import SOARClient +from soar_sdk.action_results import ActionOutput +from soar_sdk.exceptions import ActionFailure + +from . import helpers +from .asset import Asset +from .auth import get_auth_method +from .common import logger + + +def make_request( + asset: Asset, + soar: SOARClient, + method: str, + location: str, + output: type[ActionOutput], + verify: bool, + headers: str, + body: Optional[str], +) -> ActionOutput: + + logger.info(f"Preparing to make {method} http request.") + + parsed_headers = helpers.parse_headers(headers) + full_url = asset.base_url.rstrip("/") + "/" + location.lstrip("/") + + logger.info(f"Making {method} request to: {full_url}") + + auth_method = get_auth_method(asset, soar) + auth_object, final_headers = auth_method.create_auth(parsed_headers) + + body = UnicodeDammit(body).unicode_markup.encode("utf-8") if isinstance(body, str) else body + + try: + response = requests.request( + method=method, + url=full_url, + auth=auth_object, + data=body, + verify=verify, + headers=final_headers, + timeout=asset.timeout, + ) + response.raise_for_status + except requests.exceptions.RequestException as e: + logger.error(f"Request failed for {full_url}. Details: {e}") + raise ActionFailure(f"Request failed for {full_url}. Details: {e}") from e + + parsed_body = helpers.handle_various_response(response) + + raw_body = json.dumps(parsed_body, indent=4) if isinstance(parsed_body, dict) else response.text + + logger.info(f"Successfully processed data. Status: {response.status_code}") + + return output( + location=full_url, + method=method, + parsed_response_body=str(parsed_body), + response_body=raw_body, + response_headers=str(dict(response.headers)), + ) From ec918885a6260d5a3b5daefd79c7f77c38df6e88 Mon Sep 17 00:00:00 2001 From: msankows Date: Mon, 1 Sep 2025 10:42:33 +0200 Subject: [PATCH 10/29] fix: tested code - POST,GET,CONNECTIVITY,AUTH --- package/pyproject.toml | 2 +- package/src/actions/action_get.py | 7 +- package/src/actions/action_post.py | 10 +- package/src/app.py | 53 +++++---- package/src/auth.py | 5 - package/src/classes.py | 8 ++ package/src/helpers.py | 27 +++-- package/src/request_maker.py | 20 ++-- package/uv.lock | 167 +++++++++++++++-------------- 9 files changed, 158 insertions(+), 141 deletions(-) create mode 100644 package/src/classes.py diff --git a/package/pyproject.toml b/package/pyproject.toml index 9a9d078..bafafdf 100644 --- a/package/pyproject.toml +++ b/package/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "http" +name = "HTTP" version = "4.0.0" description = "app" license = "Copyright (c) 2025 Splunk Inc." diff --git a/package/src/actions/action_get.py b/package/src/actions/action_get.py index 538dcc7..2d1890d 100644 --- a/package/src/actions/action_get.py +++ b/package/src/actions/action_get.py @@ -16,6 +16,7 @@ from soar_sdk.params import Param, Params from ..asset import Asset +from ..classes import ParsedResponseBody from ..common import logger from ..request_maker import make_request @@ -24,9 +25,11 @@ class GetDataOutput(ActionOutput): + message: str + summary: str location: str = OutputField(cef_types=["url"], example_values=["http://192.168.1.26/rest/cont"]) method: str = OutputField(example_values=["GET"]) - parsed_response_body: str = OutputField(example_values=['{"failed": true, "message": "Requested item not found"}']) + parsed_response_body: ParsedResponseBody = OutputField(example_values=['{"failed": true, "message": "Requested item not found"}']) response_body: str = OutputField(example_values=['{"failed": true, "message": "Requested item not found"}']) response_headers: str @@ -41,7 +44,7 @@ class GetDataParams(Params): headers: str = Param(description="Additional headers (JSON object with headers)", required=False) -def http_get(params: GetDataParams, soar: SOARClient, asset: Asset) -> GetDataOutput: +def get_data(params: GetDataParams, soar: SOARClient, asset: Asset) -> GetDataOutput: logger.info("In action handler for: http_get") return make_request( asset=asset, diff --git a/package/src/actions/action_post.py b/package/src/actions/action_post.py index b0572c3..34213f6 100644 --- a/package/src/actions/action_post.py +++ b/package/src/actions/action_post.py @@ -3,6 +3,7 @@ from soar_sdk.params import Param, Params from ..asset import Asset +from ..classes import ParsedResponseBody from ..common import logger from ..request_maker import make_request @@ -11,9 +12,10 @@ class PostDataOutput(ActionOutput): + message: str location: str = OutputField(cef_types=["url"], example_values=["http://192.168.1.26/rest/assets"]) method: str = OutputField(example_values=["POST"]) - parsed_response_body: str = OutputField(example_values=['{"failed": true, "message": "Requested item not found"}']) + parsed_response_body: ParsedResponseBody = OutputField(example_values=['{"failed": true, "message": "Requested item not found"}']) response_body: str = OutputField(example_values=['{"failed": true, "message": "Requested item not found"}']) response_headers: str @@ -24,12 +26,12 @@ class PostDataParams(Params): primary=True, cef_types=["endpoint"], ) - body: str = Param(description="POST body (query string, JSON, etc.)") + body: str = Param(description="POST body (query string, JSON, etc.)", required=False) verify_certificate: bool = Param(description="Verify certificates (if using HTTPS)") - headers: str = Param(description="Additional headers (JSON object with headers)") + headers: str = Param(description="Additional headers (JSON object with headers)", required=False) -def http_post(params: PostDataParams, soar: SOARClient, asset: Asset) -> PostDataOutput: +def post_data(params: PostDataParams, soar: SOARClient, asset: Asset) -> PostDataOutput: logger.info("In action handler for: http_post") return make_request( asset=asset, diff --git a/package/src/app.py b/package/src/app.py index 81e346c..378d378 100644 --- a/package/src/app.py +++ b/package/src/app.py @@ -1,20 +1,20 @@ -import requests from soar_sdk.abstract import SOARClient +from soar_sdk.action_results import ActionOutput from soar_sdk.app import App -from soar_sdk.exceptions import ActionFailure -from .actions.action_get import get_action_description, get_action_type, http_get -from .actions.action_post import http_post, post_action_description, post_action_type +from .actions.action_get import get_action_description, get_action_type, get_data +from .actions.action_post import post_action_description, post_action_type, post_data from .asset import Asset from .common import logger +from .request_maker import make_request app = App( - name="http", + name="HTTP", app_type="generic", logo="logo.svg", logo_dark="logo_dark.svg", - product_vendor="Splunk Inc.", - product_name="http", + product_vendor="Generic", + product_name="HTTP", publisher="Splunk Inc.", appid="dc312038-005f-470f-badb-8a353ba9bb5b", fips_compliant=False, @@ -22,36 +22,35 @@ ) +class EmptyOutput(ActionOutput): + """An empty output class for actions that do not return data, like test_connectivity.""" + + pass + + @app.test_connectivity() def test_connectivity(soar: SOARClient, asset: Asset) -> None: logger.info("Action 'Test Connectivity' started.") - full_url = asset.base_url.rstrip("/") - if asset.test_path: - full_url = full_url + "/" + asset.test_path.lstrip("/") - logger.info(f"Querying base url, {full_url}, to test credentials.") - - try: - response = requests.request( - method=asset.test_http_method, - url=full_url, - verify=False, - timeout=asset.timeout, - ) - logger.info(f"Got status code {response.status_code}.") - response.raise_for_status() - - except requests.exceptions.RequestException as e: - logger.error(f"Test connectivity failed, error: {e}.") - raise ActionFailure(f"Test connectivity failed, details: {e}.") + + make_request( + asset=asset, + soar=soar, + method=asset.test_http_method, + location=asset.test_path if asset.test_path else "", + output=EmptyOutput, + verify=False, + headers=None, + body=None, + ) logger.info("Test connectivity passed!") -app.register_action(http_get, action_type=get_action_type, description=get_action_description) +app.register_action(get_data, action_type=get_action_type, description=get_action_description) app.register_action( - http_post, + post_data, action_type=post_action_type, description=post_action_description, read_only=False, diff --git a/package/src/auth.py b/package/src/auth.py index c14126f..9339d9e 100644 --- a/package/src/auth.py +++ b/package/src/auth.py @@ -34,8 +34,6 @@ def __init__(self, asset): def create_auth(self, headers): return (self.username, self.password), headers - # tu logger "Using HTTP Basic auth to authenticate" - class TokenAuth(Authorization): def __init__(self, asset): @@ -43,7 +41,6 @@ def __init__(self, asset): self.auth_token = asset.auth_token def create_auth(self, headers): - # tu logger "Using provided token to authenticate" if self.auth_token and self.auth_token_name not in headers: headers[self.auth_token_name] = self.auth_token return None, headers @@ -95,7 +92,6 @@ def __get_token(self) -> str: return self.__generate_new_token() def create_auth(self, headers: dict) -> tuple[None, dict]: - # tu logger access_token = self.__get_token() headers["Authorization"] = f"Bearer {access_token}" @@ -108,7 +104,6 @@ def __init__(self, asset): pass def create_auth(self, headers): - # to logger return None, headers diff --git a/package/src/classes.py b/package/src/classes.py new file mode 100644 index 0000000..21f0557 --- /dev/null +++ b/package/src/classes.py @@ -0,0 +1,8 @@ +from pydantic import Extra +from soar_sdk.action_results import ActionOutput + + +class ParsedResponseBody(ActionOutput): + + class Config: + extra = Extra.allow diff --git a/package/src/helpers.py b/package/src/helpers.py index 39377fe..6579ea7 100644 --- a/package/src/helpers.py +++ b/package/src/helpers.py @@ -3,8 +3,10 @@ import xmltodict from bs4 import BeautifulSoup +from pydantic import ValidationError from soar_sdk.exceptions import ActionFailure +from .classes import ParsedResponseBody from .common import logger @@ -17,9 +19,12 @@ def process_xml_response(response) -> dict: def process_json_response(response) -> dict: try: - return response.json() + data = response.json() + return ParsedResponseBody(**data) except json.JSONDecodeError as e: - raise ActionFailure(f"Unable to parse JSON response. Error: {e}") + raise ActionFailure(f"Server claimed JSON but failed to parse. Error: {e}") + except ValidationError as e: + raise ActionFailure(f"Response JSON did not match expected structure. Details: {e}") def process_html_response(response) -> str: @@ -52,7 +57,7 @@ def process_text_response(response) -> str: def parse_headers(headers_str: Optional[str]) -> dict: - if not headers_str: + if headers_str is None: return {} try: @@ -71,15 +76,19 @@ def parse_headers(headers_str: Optional[str]) -> dict: def handle_various_response(response): content_type = response.headers.get("Content-Type", "").lower() + if not response.text.strip() or ("application/octet-stream" in content_type): + return process_empty_response(content_type), "" - if not response.text.strip(): - return process_empty_response(response) - - parser = process_text_response(response) + parser = process_text_response for key, handler in RESPONSE_HANDLERS.items(): if key in content_type: - logger.info(f"Found handler for content type: {key}") parser = handler break - return parser(response) + parsed_body = parser(response) + + if isinstance(parsed_body, (dict, list)): + raw_body = json.dumps(parsed_body, indent=4) + else: + raw_body = response.text + return parsed_body, raw_body diff --git a/package/src/request_maker.py b/package/src/request_maker.py index 4c48481..760586f 100644 --- a/package/src/request_maker.py +++ b/package/src/request_maker.py @@ -1,4 +1,3 @@ -import json from typing import Optional import requests @@ -20,13 +19,13 @@ def make_request( location: str, output: type[ActionOutput], verify: bool, - headers: str, + headers: Optional[str], body: Optional[str], ) -> ActionOutput: logger.info(f"Preparing to make {method} http request.") - parsed_headers = helpers.parse_headers(headers) + full_url = asset.base_url.rstrip("/") + "/" + location.lstrip("/") logger.info(f"Making {method} request to: {full_url}") @@ -45,22 +44,23 @@ def make_request( verify=verify, headers=final_headers, timeout=asset.timeout, + params=None, ) - response.raise_for_status + response.raise_for_status() + except requests.exceptions.RequestException as e: logger.error(f"Request failed for {full_url}. Details: {e}") raise ActionFailure(f"Request failed for {full_url}. Details: {e}") from e - parsed_body = helpers.handle_various_response(response) - - raw_body = json.dumps(parsed_body, indent=4) if isinstance(parsed_body, dict) else response.text - + parsed_body, raw_body = helpers.handle_various_response(response) logger.info(f"Successfully processed data. Status: {response.status_code}") - + success_message = f"Status code: {response.status_code}" return output( + message=success_message, + summary=success_message, location=full_url, method=method, - parsed_response_body=str(parsed_body), + parsed_response_body=parsed_body, response_body=raw_body, response_headers=str(dict(response.headers)), ) diff --git a/package/uv.lock b/package/uv.lock index 726a450..2993397 100644 --- a/package/uv.lock +++ b/package/uv.lock @@ -157,47 +157,47 @@ wheels = [ [[package]] name = "coverage" -version = "7.10.3" -source = { registry = "https://pypi.python.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f4/2c/253cc41cd0f40b84c1c34c5363e0407d73d4a1cae005fed6db3b823175bd/coverage-7.10.3.tar.gz", hash = "sha256:812ba9250532e4a823b070b0420a36499859542335af3dca8f47fc6aa1a05619", size = 822936, upload-time = "2025-08-10T21:27:39.968Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/44/e14576c34b37764c821866909788ff7463228907ab82bae188dab2b421f1/coverage-7.10.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:53808194afdf948c462215e9403cca27a81cf150d2f9b386aee4dab614ae2ffe", size = 215964, upload-time = "2025-08-10T21:25:22.828Z" }, - { url = "https://files.pythonhosted.org/packages/e6/15/f4f92d9b83100903efe06c9396ee8d8bdba133399d37c186fc5b16d03a87/coverage-7.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f4d1b837d1abf72187a61645dbf799e0d7705aa9232924946e1f57eb09a3bf00", size = 216361, upload-time = "2025-08-10T21:25:25.603Z" }, - { url = "https://files.pythonhosted.org/packages/23/53/c1d8c2778823b1d95ca81701bb8f42c87dc341a2f170acdf716567523490/coverage-7.10.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d52989685ff5bf909c430e6d7f6550937bc6d6f3e6ecb303c97a86100efd4596", size = 244927, upload-time = "2025-08-10T21:25:28.77Z" }, - { url = "https://files.pythonhosted.org/packages/79/41/1e115fd809031f432b4ff8e2ca19999fb6196ab95c35ae7ad5e07c001130/coverage-7.10.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdb558a1d97345bde3a9f4d3e8d11c9e5611f748646e9bb61d7d612a796671b5", size = 246784, upload-time = "2025-08-10T21:25:30.195Z" }, - { url = "https://files.pythonhosted.org/packages/c7/b2/0eba9bdf8f1b327ae2713c74d4b7aa85451bb70622ab4e7b8c000936677c/coverage-7.10.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c9e6331a8f09cb1fc8bda032752af03c366870b48cce908875ba2620d20d0ad4", size = 244828, upload-time = "2025-08-10T21:25:31.785Z" }, - { url = "https://files.pythonhosted.org/packages/b6/7b/ac183fbe19ac5596c223cb47af5737f4437e7566100b7e46cc29b66695a5/coverage-7.10.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c5595fc4ad6a39312c786ec3326d7322d0cf10e3ac6a6df70809910026d67cfb", size = 243721, upload-time = "2025-08-10T21:25:34.939Z" }, - { url = "https://files.pythonhosted.org/packages/87/04/810e506d7a19889c244d35199cbf3239a2f952b55580aa42ca4287409424/coverage-7.10.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f2ff2e2afdf0d51b9b8301e542d9c21a8d084fd23d4c8ea2b3a1b3c96f5f7397", size = 216075, upload-time = "2025-08-10T21:25:39.891Z" }, - { url = "https://files.pythonhosted.org/packages/2e/50/6b3fbab034717b4af3060bdaea6b13dfdc6b1fad44b5082e2a95cd378a9a/coverage-7.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:18ecc5d1b9a8c570f6c9b808fa9a2b16836b3dd5414a6d467ae942208b095f85", size = 216476, upload-time = "2025-08-10T21:25:41.137Z" }, - { url = "https://files.pythonhosted.org/packages/34/12/5608f76070939395c17053bf16e81fd6c06cf362a537ea9d07e281013a27/coverage-7.10.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3966bc9a76b09a40dc6063c8b10375e827ea5dfcaffae402dd65953bef4cba54", size = 248800, upload-time = "2025-08-10T21:25:44.098Z" }, - { url = "https://files.pythonhosted.org/packages/ce/52/7cc90c448a0ad724283cbcdfd66b8d23a598861a6a22ac2b7b8696491798/coverage-7.10.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:205a95b87ef4eb303b7bc5118b47b6b6604a644bcbdb33c336a41cfc0a08c06a", size = 250904, upload-time = "2025-08-10T21:25:45.384Z" }, - { url = "https://files.pythonhosted.org/packages/e6/70/9967b847063c1c393b4f4d6daab1131558ebb6b51f01e7df7150aa99f11d/coverage-7.10.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b3801b79fb2ad61e3c7e2554bab754fc5f105626056980a2b9cf3aef4f13f84", size = 248597, upload-time = "2025-08-10T21:25:47.059Z" }, - { url = "https://files.pythonhosted.org/packages/8e/27/d27af83ad162eba62c4eb7844a1de6cf7d9f6b185df50b0a3514a6f80ddd/coverage-7.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a83d4f134bab2c7ff758e6bb1541dd72b54ba295ced6a63d93efc2e20cb9b124", size = 247290, upload-time = "2025-08-10T21:25:49.945Z" }, - { url = "https://files.pythonhosted.org/packages/b8/62/13c0b66e966c43d7aa64dadc8cd2afa1f5a2bf9bb863bdabc21fb94e8b63/coverage-7.10.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:449c1e2d3a84d18bd204258a897a87bc57380072eb2aded6a5b5226046207b42", size = 216262, upload-time = "2025-08-10T21:25:55.367Z" }, - { url = "https://files.pythonhosted.org/packages/b5/f0/59fdf79be7ac2f0206fc739032f482cfd3f66b18f5248108ff192741beae/coverage-7.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1d4f9ce50b9261ad196dc2b2e9f1fbbee21651b54c3097a25ad783679fd18294", size = 216496, upload-time = "2025-08-10T21:25:56.759Z" }, - { url = "https://files.pythonhosted.org/packages/0c/29/f8bdf88357956c844bd872e87cb16748a37234f7f48c721dc7e981145eb7/coverage-7.10.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ca3c9530ee072b7cb6a6ea7b640bcdff0ad3b334ae9687e521e59f79b1d0437", size = 250738, upload-time = "2025-08-10T21:25:59.406Z" }, - { url = "https://files.pythonhosted.org/packages/ae/df/6396301d332b71e42bbe624670af9376f63f73a455cc24723656afa95796/coverage-7.10.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b6df359e59fa243c9925ae6507e27f29c46698359f45e568fd51b9315dbbe587", size = 251868, upload-time = "2025-08-10T21:26:00.65Z" }, - { url = "https://files.pythonhosted.org/packages/91/21/d760b2df6139b6ef62c9cc03afb9bcdf7d6e36ed4d078baacffa618b4c1c/coverage-7.10.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a181e4c2c896c2ff64c6312db3bda38e9ade2e1aa67f86a5628ae85873786cea", size = 249790, upload-time = "2025-08-10T21:26:02.009Z" }, - { url = "https://files.pythonhosted.org/packages/38/ed/70c0e871cdfef75f27faceada461206c1cc2510c151e1ef8d60a6fedda39/coverage-7.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:daeefff05993e5e8c6e7499a8508e7bd94502b6b9a9159c84fd1fe6bce3151cb", size = 249344, upload-time = "2025-08-10T21:26:05.11Z" }, - { url = "https://files.pythonhosted.org/packages/0a/ff/239e4de9cc149c80e9cc359fab60592365b8c4cbfcad58b8a939d18c6898/coverage-7.10.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b99e87304ffe0eb97c5308447328a584258951853807afdc58b16143a530518a", size = 216298, upload-time = "2025-08-10T21:26:10.973Z" }, - { url = "https://files.pythonhosted.org/packages/56/da/28717da68f8ba68f14b9f558aaa8f3e39ada8b9a1ae4f4977c8f98b286d5/coverage-7.10.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4af09c7574d09afbc1ea7da9dcea23665c01f3bc1b1feb061dac135f98ffc53a", size = 216546, upload-time = "2025-08-10T21:26:12.616Z" }, - { url = "https://files.pythonhosted.org/packages/ea/2f/6ae1db51dc34db499bfe340e89f79a63bd115fc32513a7bacdf17d33cd86/coverage-7.10.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:913ceddb4289cbba3a310704a424e3fb7aac2bc0c3a23ea473193cb290cf17d4", size = 250141, upload-time = "2025-08-10T21:26:15.787Z" }, - { url = "https://files.pythonhosted.org/packages/4f/ed/33efd8819895b10c66348bf26f011dd621e804866c996ea6893d682218df/coverage-7.10.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b1f91cbc78c7112ab84ed2a8defbccd90f888fcae40a97ddd6466b0bec6ae8a", size = 251415, upload-time = "2025-08-10T21:26:17.535Z" }, - { url = "https://files.pythonhosted.org/packages/26/04/cb83826f313d07dc743359c9914d9bc460e0798da9a0e38b4f4fabc207ed/coverage-7.10.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0bac054d45af7cd938834b43a9878b36ea92781bcb009eab040a5b09e9927e3", size = 249575, upload-time = "2025-08-10T21:26:18.921Z" }, - { url = "https://files.pythonhosted.org/packages/99/e8/b68d1487c6af370b8d5ef223c6d7e250d952c3acfbfcdbf1a773aa0da9d2/coverage-7.10.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c1e2e927ab3eadd7c244023927d646e4c15c65bb2ac7ae3c3e9537c013700d21", size = 249084, upload-time = "2025-08-10T21:26:21.638Z" }, - { url = "https://files.pythonhosted.org/packages/fc/26/1c1f450e15a3bf3eaecf053ff64538a2612a23f05b21d79ce03be9ff5903/coverage-7.10.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07009152f497a0464ffdf2634586787aea0e69ddd023eafb23fc38267db94b84", size = 217003, upload-time = "2025-08-10T21:26:27.231Z" }, - { url = "https://files.pythonhosted.org/packages/29/96/4b40036181d8c2948454b458750960956a3c4785f26a3c29418bbbee1666/coverage-7.10.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dd2ba5f0c7e7e8cc418be2f0c14c4d9e3f08b8fb8e4c0f83c2fe87d03eb655e", size = 217238, upload-time = "2025-08-10T21:26:28.83Z" }, - { url = "https://files.pythonhosted.org/packages/59/95/00e7fcbeda3f632232f4c07dde226afe3511a7781a000aa67798feadc535/coverage-7.10.3-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb329f1046888a36b1dc35504d3029e1dd5afe2196d94315d18c45ee380f67d5", size = 260735, upload-time = "2025-08-10T21:26:32.333Z" }, - { url = "https://files.pythonhosted.org/packages/9e/4c/f4666cbc4571804ba2a65b078ff0de600b0b577dc245389e0bc9b69ae7ca/coverage-7.10.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce01048199a91f07f96ca3074b0c14021f4fe7ffd29a3e6a188ac60a5c3a4af8", size = 262960, upload-time = "2025-08-10T21:26:33.701Z" }, - { url = "https://files.pythonhosted.org/packages/c1/a5/8a9e8a7b12a290ed98b60f73d1d3e5e9ced75a4c94a0d1a671ce3ddfff2a/coverage-7.10.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:08b989a06eb9dfacf96d42b7fb4c9a22bafa370d245dc22fa839f2168c6f9fa1", size = 260515, upload-time = "2025-08-10T21:26:35.16Z" }, - { url = "https://files.pythonhosted.org/packages/cc/22/3646f8903743c07b3e53fded0700fed06c580a980482f04bf9536657ac17/coverage-7.10.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3262d19092771c83f3413831d9904b1ccc5f98da5de4ffa4ad67f5b20c7aaf7b", size = 259408, upload-time = "2025-08-10T21:26:37.954Z" }, - { url = "https://files.pythonhosted.org/packages/f1/66/c06f4a93c65b6fc6578ef4f1fe51f83d61fc6f2a74ec0ce434ed288d834a/coverage-7.10.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:da749daa7e141985487e1ff90a68315b0845930ed53dc397f4ae8f8bab25b551", size = 215951, upload-time = "2025-08-10T21:27:19.815Z" }, - { url = "https://files.pythonhosted.org/packages/c2/ea/cc18c70a6f72f8e4def212eaebd8388c64f29608da10b3c38c8ec76f5e49/coverage-7.10.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3126fb6a47d287f461d9b1aa5d1a8c97034d1dffb4f452f2cf211289dae74ef", size = 216335, upload-time = "2025-08-10T21:27:21.737Z" }, - { url = "https://files.pythonhosted.org/packages/5a/e5/4223bdb28b992a19a13ab1410c761e2bfe92ca1e7bba8e85ee2024eeda85/coverage-7.10.3-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4e27bebbd184ef8d1c1e092b74a2b7109dcbe2618dce6e96b1776d53b14b3fe8", size = 244596, upload-time = "2025-08-10T21:27:25.842Z" }, - { url = "https://files.pythonhosted.org/packages/d2/13/d646ba28613669d487c654a760571c10128247d12d9f50e93f69542679a2/coverage-7.10.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8fd4ee2580b9fefbd301b4f8f85b62ac90d1e848bea54f89a5748cf132782118", size = 246370, upload-time = "2025-08-10T21:27:27.503Z" }, - { url = "https://files.pythonhosted.org/packages/02/7c/aff99c67d8c383142b0877ee435caf493765356336211c4899257325d6c7/coverage-7.10.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6999920bdd73259ce11cabfc1307484f071ecc6abdb2ca58d98facbcefc70f16", size = 244254, upload-time = "2025-08-10T21:27:29.357Z" }, - { url = "https://files.pythonhosted.org/packages/d8/4b/6119be0089c89ad49d2e5a508d55a1485c878642b706a7f95b26e299137d/coverage-7.10.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:25b902c5e15dea056485d782e420bb84621cc08ee75d5131ecb3dbef8bd1365f", size = 243281, upload-time = "2025-08-10T21:27:32.815Z" }, - { url = "https://files.pythonhosted.org/packages/84/19/e67f4ae24e232c7f713337f3f4f7c9c58afd0c02866fb07c7b9255a19ed7/coverage-7.10.3-py3-none-any.whl", hash = "sha256:416a8d74dc0adfd33944ba2f405897bab87b7e9e84a391e09d241956bd953ce1", size = 207921, upload-time = "2025-08-10T21:27:38.254Z" }, +version = "7.10.5" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/83/153f54356c7c200013a752ce1ed5448573dca546ce125801afca9e1ac1a4/coverage-7.10.5.tar.gz", hash = "sha256:f2e57716a78bc3ae80b2207be0709a3b2b63b9f2dcf9740ee6ac03588a2015b6", size = 821662, upload-time = "2025-08-23T14:42:44.78Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/70/e77b0061a6c7157bfce645c6b9a715a08d4c86b3360a7b3252818080b817/coverage-7.10.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c6a5c3414bfc7451b879141ce772c546985163cf553f08e0f135f0699a911801", size = 216774, upload-time = "2025-08-23T14:40:26.301Z" }, + { url = "https://files.pythonhosted.org/packages/91/08/2a79de5ecf37ee40f2d898012306f11c161548753391cec763f92647837b/coverage-7.10.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bc8e4d99ce82f1710cc3c125adc30fd1487d3cf6c2cd4994d78d68a47b16989a", size = 217175, upload-time = "2025-08-23T14:40:29.142Z" }, + { url = "https://files.pythonhosted.org/packages/15/06/3a67662c55656702bd398a727a7f35df598eb11104fcb34f1ecbb070291a/coverage-7.10.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:73269df37883e02d460bee0cc16be90509faea1e3bd105d77360b512d5bb9c33", size = 245740, upload-time = "2025-08-23T14:40:32.302Z" }, + { url = "https://files.pythonhosted.org/packages/00/f4/f8763aabf4dc30ef0d0012522d312f0b7f9fede6246a1f27dbcc4a1e523c/coverage-7.10.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f8a81b0614642f91c9effd53eec284f965577591f51f547a1cbeb32035b4c2f", size = 247600, upload-time = "2025-08-23T14:40:33.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/31/6632219a9065e1b83f77eda116fed4c76fb64908a6a9feae41816dab8237/coverage-7.10.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6a29f8e0adb7f8c2b95fa2d4566a1d6e6722e0a637634c6563cb1ab844427dd9", size = 245640, upload-time = "2025-08-23T14:40:35.248Z" }, + { url = "https://files.pythonhosted.org/packages/02/b9/57170bd9f3e333837fc24ecc88bc70fbc2eb7ccfd0876854b0c0407078c3/coverage-7.10.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:90dc3d6fb222b194a5de60af8d190bedeeddcbc7add317e4a3cd333ee6b7c879", size = 244537, upload-time = "2025-08-23T14:40:38.737Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f2/336d34d2fc1291ca7c18eeb46f64985e6cef5a1a7ef6d9c23720c6527289/coverage-7.10.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c177e6ffe2ebc7c410785307758ee21258aa8e8092b44d09a2da767834f075f2", size = 216890, upload-time = "2025-08-23T14:40:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/92448b07cc1cf2b429d0ce635f59cf0c626a5d8de21358f11e92174ff2a6/coverage-7.10.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:14d6071c51ad0f703d6440827eaa46386169b5fdced42631d5a5ac419616046f", size = 217287, upload-time = "2025-08-23T14:40:45.214Z" }, + { url = "https://files.pythonhosted.org/packages/28/e5/fe3bbc8d097029d284b5fb305b38bb3404895da48495f05bff025df62770/coverage-7.10.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f39071caa126f69d63f99b324fb08c7b1da2ec28cbb1fe7b5b1799926492f65c", size = 249614, upload-time = "2025-08-23T14:40:48.082Z" }, + { url = "https://files.pythonhosted.org/packages/69/9c/a1c89a8c8712799efccb32cd0a1ee88e452f0c13a006b65bb2271f1ac767/coverage-7.10.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343a023193f04d46edc46b2616cdbee68c94dd10208ecd3adc56fcc54ef2baa1", size = 251719, upload-time = "2025-08-23T14:40:49.349Z" }, + { url = "https://files.pythonhosted.org/packages/e9/be/5576b5625865aa95b5633315f8f4142b003a70c3d96e76f04487c3b5cc95/coverage-7.10.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:585ffe93ae5894d1ebdee69fc0b0d4b7c75d8007983692fb300ac98eed146f78", size = 249411, upload-time = "2025-08-23T14:40:50.624Z" }, + { url = "https://files.pythonhosted.org/packages/40/cb/aebb2d8c9e3533ee340bea19b71c5b76605a0268aa49808e26fe96ec0a07/coverage-7.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eb7b0bbf7cc1d0453b843eca7b5fa017874735bef9bfdfa4121373d2cc885ed6", size = 248104, upload-time = "2025-08-23T14:40:54.064Z" }, + { url = "https://files.pythonhosted.org/packages/27/8e/40d75c7128f871ea0fd829d3e7e4a14460cad7c3826e3b472e6471ad05bd/coverage-7.10.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c2d05c7e73c60a4cecc7d9b60dbfd603b4ebc0adafaef371445b47d0f805c8a9", size = 217077, upload-time = "2025-08-23T14:40:59.329Z" }, + { url = "https://files.pythonhosted.org/packages/18/a8/f333f4cf3fb5477a7f727b4d603a2eb5c3c5611c7fe01329c2e13b23b678/coverage-7.10.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:32ddaa3b2c509778ed5373b177eb2bf5662405493baeff52278a0b4f9415188b", size = 217310, upload-time = "2025-08-23T14:41:00.628Z" }, + { url = "https://files.pythonhosted.org/packages/3f/bc/1011da599b414fb6c9c0f34086736126f9ff71f841755786a6b87601b088/coverage-7.10.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7fa22800f3908df31cea6fb230f20ac49e343515d968cc3a42b30d5c3ebf9b5a", size = 251550, upload-time = "2025-08-23T14:41:03.438Z" }, + { url = "https://files.pythonhosted.org/packages/4c/6f/b5c03c0c721c067d21bc697accc3642f3cef9f087dac429c918c37a37437/coverage-7.10.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f366a57ac81f5e12797136552f5b7502fa053c861a009b91b80ed51f2ce651c6", size = 252684, upload-time = "2025-08-23T14:41:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/f9/50/d474bc300ebcb6a38a1047d5c465a227605d6473e49b4e0d793102312bc5/coverage-7.10.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5f1dc8f1980a272ad4a6c84cba7981792344dad33bf5869361576b7aef42733a", size = 250602, upload-time = "2025-08-23T14:41:06.719Z" }, + { url = "https://files.pythonhosted.org/packages/e2/96/a7c3c0562266ac39dcad271d0eec8fc20ab576e3e2f64130a845ad2a557b/coverage-7.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c2492e4dd9daab63f5f56286f8a04c51323d237631eb98505d87e4c4ff19ec34", size = 250158, upload-time = "2025-08-23T14:41:09.749Z" }, + { url = "https://files.pythonhosted.org/packages/9f/08/4166ecfb60ba011444f38a5a6107814b80c34c717bc7a23be0d22e92ca09/coverage-7.10.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ef3b83594d933020f54cf65ea1f4405d1f4e41a009c46df629dd964fcb6e907c", size = 217106, upload-time = "2025-08-23T14:41:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/25/d7/b71022408adbf040a680b8c64bf6ead3be37b553e5844f7465643979f7ca/coverage-7.10.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b96bfdf7c0ea9faebce088a3ecb2382819da4fbc05c7b80040dbc428df6af44", size = 217353, upload-time = "2025-08-23T14:41:16.656Z" }, + { url = "https://files.pythonhosted.org/packages/90/65/28752c3a896566ec93e0219fc4f47ff71bd2b745f51554c93e8dcb659796/coverage-7.10.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8002dc6a049aac0e81ecec97abfb08c01ef0c1fbf962d0c98da3950ace89b869", size = 250955, upload-time = "2025-08-23T14:41:19.577Z" }, + { url = "https://files.pythonhosted.org/packages/a5/eb/ca6b7967f57f6fef31da8749ea20417790bb6723593c8cd98a987be20423/coverage-7.10.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:63d4bb2966d6f5f705a6b0c6784c8969c468dbc4bcf9d9ded8bff1c7e092451f", size = 252230, upload-time = "2025-08-23T14:41:20.959Z" }, + { url = "https://files.pythonhosted.org/packages/bc/29/17a411b2a2a18f8b8c952aa01c00f9284a1fbc677c68a0003b772ea89104/coverage-7.10.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1f672efc0731a6846b157389b6e6d5d5e9e59d1d1a23a5c66a99fd58339914d5", size = 250387, upload-time = "2025-08-23T14:41:22.644Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c6/0ad7d0137257553eb4706b4ad6180bec0a1b6a648b092c5bbda48d0e5b2c/coverage-7.10.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2968647e3ed5a6c019a419264386b013979ff1fb67dd11f5c9886c43d6a31fc2", size = 249894, upload-time = "2025-08-23T14:41:26.165Z" }, + { url = "https://files.pythonhosted.org/packages/d8/a1/590154e6eae07beee3b111cc1f907c30da6fc8ce0a83ef756c72f3c7c748/coverage-7.10.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0520dff502da5e09d0d20781df74d8189ab334a1e40d5bafe2efaa4158e2d9e7", size = 217819, upload-time = "2025-08-23T14:41:31.962Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ff/436ffa3cfc7741f0973c5c89405307fe39b78dcf201565b934e6616fc4ad/coverage-7.10.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d9cd64aca68f503ed3f1f18c7c9174cbb797baba02ca8ab5112f9d1c0328cd4b", size = 218040, upload-time = "2025-08-23T14:41:33.472Z" }, + { url = "https://files.pythonhosted.org/packages/b5/89/21af956843896adc2e64fc075eae3c1cadb97ee0a6960733e65e696f32dd/coverage-7.10.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1b7181c0feeb06ed8a02da02792f42f829a7b29990fef52eff257fef0885d760", size = 261551, upload-time = "2025-08-23T14:41:36.333Z" }, + { url = "https://files.pythonhosted.org/packages/e1/96/390a69244ab837e0ac137989277879a084c786cf036c3c4a3b9637d43a89/coverage-7.10.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36d42b7396b605f774d4372dd9c49bed71cbabce4ae1ccd074d155709dd8f235", size = 263776, upload-time = "2025-08-23T14:41:38.25Z" }, + { url = "https://files.pythonhosted.org/packages/00/32/cfd6ae1da0a521723349f3129b2455832fc27d3f8882c07e5b6fefdd0da2/coverage-7.10.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b4fdc777e05c4940b297bf47bf7eedd56a39a61dc23ba798e4b830d585486ca5", size = 261326, upload-time = "2025-08-23T14:41:40.343Z" }, + { url = "https://files.pythonhosted.org/packages/f4/5d/a234f7409896468e5539d42234016045e4015e857488b0b5b5f3f3fa5f2b/coverage-7.10.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:66c644cbd7aed8fe266d5917e2c9f65458a51cfe5eeff9c05f15b335f697066e", size = 260217, upload-time = "2025-08-23T14:41:43.591Z" }, + { url = "https://files.pythonhosted.org/packages/3b/21/05248e8bc74683488cb7477e6b6b878decadd15af0ec96f56381d3d7ff2d/coverage-7.10.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:62835c1b00c4a4ace24c1a88561a5a59b612fbb83a525d1c70ff5720c97c0610", size = 216763, upload-time = "2025-08-23T14:42:26.75Z" }, + { url = "https://files.pythonhosted.org/packages/a9/7f/161a0ad40cb1c7e19dc1aae106d3430cc88dac3d651796d6cf3f3730c800/coverage-7.10.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5255b3bbcc1d32a4069d6403820ac8e6dbcc1d68cb28a60a1ebf17e47028e898", size = 217154, upload-time = "2025-08-23T14:42:28.238Z" }, + { url = "https://files.pythonhosted.org/packages/6e/4e/2649344e33eeb3567041e8255a1942173cae81817fe06b60f3fafaafe111/coverage-7.10.5-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8048ce4b149c93447a55d279078c8ae98b08a6951a3c4d2d7e87f4efc7bfe100", size = 245412, upload-time = "2025-08-23T14:42:31.296Z" }, + { url = "https://files.pythonhosted.org/packages/ac/b1/b21e1e69986ad89b051dd42c3ef06d9326e03ac3c0c844fc33385d1d9e35/coverage-7.10.5-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4028e7558e268dd8bcf4d9484aad393cafa654c24b4885f6f9474bf53183a82a", size = 247182, upload-time = "2025-08-23T14:42:33.155Z" }, + { url = "https://files.pythonhosted.org/packages/4c/b5/80837be411ae092e03fcc2a7877bd9a659c531eff50453e463057a9eee44/coverage-7.10.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:03f47dc870eec0367fcdd603ca6a01517d2504e83dc18dbfafae37faec66129a", size = 245066, upload-time = "2025-08-23T14:42:34.754Z" }, + { url = "https://files.pythonhosted.org/packages/75/0f/505c6af24a9ae5d8919d209b9c31b7092815f468fa43bec3b1118232c62a/coverage-7.10.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b3dcf2ead47fa8be14224ee817dfc1df98043af568fe120a22f81c0eb3c34ad2", size = 244095, upload-time = "2025-08-23T14:42:38.227Z" }, + { url = "https://files.pythonhosted.org/packages/08/b6/fff6609354deba9aeec466e4bcaeb9d1ed3e5d60b14b57df2a36fb2273f2/coverage-7.10.5-py3-none-any.whl", hash = "sha256:0be24d35e4db1d23d0db5c0f6a74a962e2ec83c426b5cac09f4234aadef38e4a", size = 208736, upload-time = "2025-08-23T14:42:43.145Z" }, ] [[package]] @@ -238,11 +238,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.18.0" +version = "3.19.1" source = { registry = "https://pypi.python.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687, upload-time = "2025-08-14T16:56:03.016Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, + { url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" }, ] [[package]] @@ -277,7 +277,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "splunk-soar-sdk", specifier = ">=1.0.0b55" }, - { name = "xmltodict" }, + { name = "xmltodict", specifier = "==0.13.0" }, ] [package.metadata.requires-dev] @@ -321,11 +321,11 @@ wheels = [ [[package]] name = "humanize" -version = "4.12.3" +version = "4.13.0" source = { registry = "https://pypi.python.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/22/d1/bbc4d251187a43f69844f7fd8941426549bbe4723e8ff0a7441796b0789f/humanize-4.12.3.tar.gz", hash = "sha256:8430be3a615106fdfceb0b2c1b41c4c98c6b0fc5cc59663a5539b111dd325fb0", size = 80514, upload-time = "2025-04-30T11:51:07.98Z" } +sdist = { url = "https://files.pythonhosted.org/packages/98/1d/3062fcc89ee05a715c0b9bfe6490c00c576314f27ffee3a704122c6fd259/humanize-4.13.0.tar.gz", hash = "sha256:78f79e68f76f0b04d711c4e55d32bebef5be387148862cb1ef83d2b58e7935a0", size = 81884, upload-time = "2025-08-25T09:39:20.04Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/1e/62a2ec3104394a2975a2629eec89276ede9dbe717092f6966fcf963e1bf0/humanize-4.12.3-py3-none-any.whl", hash = "sha256:2cbf6370af06568fa6d2da77c86edb7886f3160ecd19ee1ffef07979efc597f6", size = 128487, upload-time = "2025-04-30T11:51:06.468Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/316e7ca04d26695ef0635dc81683d628350810eb8e9b2299fc08ba49f366/humanize-4.13.0-py3-none-any.whl", hash = "sha256:b810820b31891813b1673e8fec7f1ed3312061eab2f26e3fa192c393d11ed25f", size = 128869, upload-time = "2025-08-25T09:39:18.54Z" }, ] [[package]] @@ -510,11 +510,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.3.8" +version = "4.4.0" source = { registry = "https://pypi.python.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } +sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, + { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, ] [[package]] @@ -668,7 +668,7 @@ wheels = [ [[package]] name = "requests" -version = "2.32.4" +version = "2.32.5" source = { registry = "https://pypi.python.org/simple" } dependencies = [ { name = "certifi", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, @@ -676,9 +676,9 @@ dependencies = [ { name = "idna", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "urllib3", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] [[package]] @@ -711,22 +711,23 @@ wheels = [ [[package]] name = "ruff" -version = "0.12.8" +version = "0.12.10" source = { registry = "https://pypi.python.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4b/da/5bd7565be729e86e1442dad2c9a364ceeff82227c2dece7c29697a9795eb/ruff-0.12.8.tar.gz", hash = "sha256:4cb3a45525176e1009b2b64126acf5f9444ea59066262791febf55e40493a033", size = 5242373, upload-time = "2025-08-07T19:05:47.268Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/eb/8c073deb376e46ae767f4961390d17545e8535921d2f65101720ed8bd434/ruff-0.12.10.tar.gz", hash = "sha256:189ab65149d11ea69a2d775343adf5f49bb2426fc4780f65ee33b423ad2e47f9", size = 5310076, upload-time = "2025-08-21T18:23:22.595Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/1e/c843bfa8ad1114fab3eb2b78235dda76acd66384c663a4e0415ecc13aa1e/ruff-0.12.8-py3-none-linux_armv6l.whl", hash = "sha256:63cb5a5e933fc913e5823a0dfdc3c99add73f52d139d6cd5cc8639d0e0465513", size = 11675315, upload-time = "2025-08-07T19:05:06.15Z" }, - { url = "https://files.pythonhosted.org/packages/24/ee/af6e5c2a8ca3a81676d5480a1025494fd104b8896266502bb4de2a0e8388/ruff-0.12.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a9bbe28f9f551accf84a24c366c1aa8774d6748438b47174f8e8565ab9dedbc", size = 12456653, upload-time = "2025-08-07T19:05:09.759Z" }, - { url = "https://files.pythonhosted.org/packages/99/9d/e91f84dfe3866fa648c10512904991ecc326fd0b66578b324ee6ecb8f725/ruff-0.12.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2fae54e752a3150f7ee0e09bce2e133caf10ce9d971510a9b925392dc98d2fec", size = 11659690, upload-time = "2025-08-07T19:05:12.551Z" }, - { url = "https://files.pythonhosted.org/packages/fe/ac/a363d25ec53040408ebdd4efcee929d48547665858ede0505d1d8041b2e5/ruff-0.12.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0acbcf01206df963d9331b5838fb31f3b44fa979ee7fa368b9b9057d89f4a53", size = 11896923, upload-time = "2025-08-07T19:05:14.821Z" }, - { url = "https://files.pythonhosted.org/packages/58/9f/ea356cd87c395f6ade9bb81365bd909ff60860975ca1bc39f0e59de3da37/ruff-0.12.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae3e7504666ad4c62f9ac8eedb52a93f9ebdeb34742b8b71cd3cccd24912719f", size = 11477612, upload-time = "2025-08-07T19:05:16.712Z" }, - { url = "https://files.pythonhosted.org/packages/5e/c4/f2176a310f26e6160deaf661ef60db6c3bb62b7a35e57ae28f27a09a7d63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dbea798fc0065ad0b84a2947b0aff4233f0cb30f226f00a2c5850ca4393de609", size = 14206885, upload-time = "2025-08-07T19:05:21.025Z" }, - { url = "https://files.pythonhosted.org/packages/87/9d/98e162f3eeeb6689acbedbae5050b4b3220754554526c50c292b611d3a63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:49ebcaccc2bdad86fd51b7864e3d808aad404aab8df33d469b6e65584656263a", size = 13639381, upload-time = "2025-08-07T19:05:23.423Z" }, - { url = "https://files.pythonhosted.org/packages/81/4e/1b7478b072fcde5161b48f64774d6edd59d6d198e4ba8918d9f4702b8043/ruff-0.12.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ac9c570634b98c71c88cb17badd90f13fc076a472ba6ef1d113d8ed3df109fb", size = 12613271, upload-time = "2025-08-07T19:05:25.507Z" }, - { url = "https://files.pythonhosted.org/packages/e8/67/0c3c9179a3ad19791ef1b8f7138aa27d4578c78700551c60d9260b2c660d/ruff-0.12.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:560e0cd641e45591a3e42cb50ef61ce07162b9c233786663fdce2d8557d99818", size = 12847783, upload-time = "2025-08-07T19:05:28.14Z" }, - { url = "https://files.pythonhosted.org/packages/4e/2a/0b6ac3dd045acf8aa229b12c9c17bb35508191b71a14904baf99573a21bd/ruff-0.12.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:71c83121512e7743fba5a8848c261dcc454cafb3ef2934a43f1b7a4eb5a447ea", size = 11702672, upload-time = "2025-08-07T19:05:30.413Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ee/f9fdc9f341b0430110de8b39a6ee5fa68c5706dc7c0aa940817947d6937e/ruff-0.12.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:de4429ef2ba091ecddedd300f4c3f24bca875d3d8b23340728c3cb0da81072c3", size = 11440626, upload-time = "2025-08-07T19:05:32.492Z" }, - { url = "https://files.pythonhosted.org/packages/18/9f/5c5d93e1d00d854d5013c96e1a92c33b703a0332707a7cdbd0a4880a84fb/ruff-0.12.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:45c32487e14f60b88aad6be9fd5da5093dbefb0e3e1224131cb1d441d7cb7d46", size = 12913212, upload-time = "2025-08-07T19:05:36.541Z" }, + { url = "https://files.pythonhosted.org/packages/24/e7/560d049d15585d6c201f9eeacd2fd130def3741323e5ccf123786e0e3c95/ruff-0.12.10-py3-none-linux_armv6l.whl", hash = "sha256:8b593cb0fb55cc8692dac7b06deb29afda78c721c7ccfed22db941201b7b8f7b", size = 11935161, upload-time = "2025-08-21T18:22:26.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b0/ad2464922a1113c365d12b8f80ed70fcfb39764288ac77c995156080488d/ruff-0.12.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ebb7333a45d56efc7c110a46a69a1b32365d5c5161e7244aaf3aa20ce62399c1", size = 12660884, upload-time = "2025-08-21T18:22:30.925Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f1/97f509b4108d7bae16c48389f54f005b62ce86712120fd8b2d8e88a7cb49/ruff-0.12.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d59e58586829f8e4a9920788f6efba97a13d1fa320b047814e8afede381c6839", size = 11872754, upload-time = "2025-08-21T18:22:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/12/ad/44f606d243f744a75adc432275217296095101f83f966842063d78eee2d3/ruff-0.12.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:822d9677b560f1fdeab69b89d1f444bf5459da4aa04e06e766cf0121771ab844", size = 12092276, upload-time = "2025-08-21T18:22:36.764Z" }, + { url = "https://files.pythonhosted.org/packages/06/1f/ed6c265e199568010197909b25c896d66e4ef2c5e1c3808caf461f6f3579/ruff-0.12.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:37b4a64f4062a50c75019c61c7017ff598cb444984b638511f48539d3a1c98db", size = 11734700, upload-time = "2025-08-21T18:22:39.822Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/39369e6ac7f2a1848f22fb0b00b690492f20811a1ac5c1fd1d2798329263/ruff-0.12.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:059e863ea3a9ade41407ad71c1de2badfbe01539117f38f763ba42a1206f7559", size = 14436642, upload-time = "2025-08-21T18:22:45.612Z" }, + { url = "https://files.pythonhosted.org/packages/e3/03/5da8cad4b0d5242a936eb203b58318016db44f5c5d351b07e3f5e211bb89/ruff-0.12.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bef6161e297c68908b7218fa6e0e93e99a286e5ed9653d4be71e687dff101cf", size = 13859107, upload-time = "2025-08-21T18:22:48.886Z" }, + { url = "https://files.pythonhosted.org/packages/19/19/dd7273b69bf7f93a070c9cec9494a94048325ad18fdcf50114f07e6bf417/ruff-0.12.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4f1345fbf8fb0531cd722285b5f15af49b2932742fc96b633e883da8d841896b", size = 12886521, upload-time = "2025-08-21T18:22:51.567Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1d/b4207ec35e7babaee62c462769e77457e26eb853fbdc877af29417033333/ruff-0.12.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f68433c4fbc63efbfa3ba5db31727db229fa4e61000f452c540474b03de52a9", size = 13097528, upload-time = "2025-08-21T18:22:54.609Z" }, + { url = "https://files.pythonhosted.org/packages/ff/00/58f7b873b21114456e880b75176af3490d7a2836033779ca42f50de3b47a/ruff-0.12.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:141ce3d88803c625257b8a6debf4a0473eb6eed9643a6189b68838b43e78165a", size = 13080443, upload-time = "2025-08-21T18:22:57.413Z" }, + { url = "https://files.pythonhosted.org/packages/12/8c/9e6660007fb10189ccb78a02b41691288038e51e4788bf49b0a60f740604/ruff-0.12.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f3fc21178cd44c98142ae7590f42ddcb587b8e09a3b849cbc84edb62ee95de60", size = 11896759, upload-time = "2025-08-21T18:23:00.473Z" }, + { url = "https://files.pythonhosted.org/packages/67/4c/6d092bb99ea9ea6ebda817a0e7ad886f42a58b4501a7e27cd97371d0ba54/ruff-0.12.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7d1a4e0bdfafcd2e3e235ecf50bf0176f74dd37902f241588ae1f6c827a36c56", size = 11701463, upload-time = "2025-08-21T18:23:03.211Z" }, + { url = "https://files.pythonhosted.org/packages/ad/37/63a9c788bbe0b0850611669ec6b8589838faf2f4f959647f2d3e320383ae/ruff-0.12.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ae479e1a18b439c59138f066ae79cc0f3ee250712a873d00dbafadaad9481e5b", size = 13164356, upload-time = "2025-08-21T18:23:10.225Z" }, ] [[package]] @@ -749,16 +750,16 @@ wheels = [ [[package]] name = "soupsieve" -version = "2.7" +version = "2.8" source = { registry = "https://pypi.python.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418, upload-time = "2025-04-20T18:50:08.518Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/e6/21ccce3262dd4889aa3332e5a119a3491a95e8f60939870a3a035aabac0d/soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f", size = 103472, upload-time = "2025-08-27T15:39:51.78Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677, upload-time = "2025-04-20T18:50:07.196Z" }, + { url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" }, ] [[package]] name = "splunk-soar-sdk" -version = "1.0.0b55" +version = "1.1.0" source = { registry = "https://pypi.python.org/simple" } dependencies = [ { name = "beautifulsoup4", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, @@ -776,9 +777,9 @@ dependencies = [ { name = "tqdm", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "typer", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c3/48/6639d36ff8184c6729de0ef87ff64bec8a6a45fe821907c8c2aa05e84442/splunk_soar_sdk-1.0.0b55.tar.gz", hash = "sha256:375be2f77736427b86d940f2245fbee58a600e1d68ac95c3bb9d6419c82479cd", size = 370757, upload-time = "2025-08-06T20:23:45.368Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/47/262353a2ab093f18ce7b5475814f3df7687800d205cb55c7a28001f0ea35/splunk_soar_sdk-1.1.0.tar.gz", hash = "sha256:20105071af535ce051ee536b849ef10a9934ccafcdb64ea9e5a1d6b3c6e2ed0f", size = 376008, upload-time = "2025-08-22T22:52:56.496Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/0a/c65d959cfebdb6201511fc9f0c8766311b3abeff0aab060efe261d1fec03/splunk_soar_sdk-1.0.0b55-py3-none-any.whl", hash = "sha256:68a3846fb0aa9822d9c7b8552e0ca533c4589212d7ea32fdf576bd3e8be65e20", size = 146305, upload-time = "2025-08-06T20:23:41.924Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e0/db0e833e31d77567eb253847b5b0c08a17bb44bd79ac9a49b88d6690581b/splunk_soar_sdk-1.1.0-py3-none-any.whl", hash = "sha256:f730236fbf3076ec755f14c56d585ded74718c205df6fef3b92b262650ef850b", size = 150002, upload-time = "2025-08-22T22:52:55.15Z" }, ] [[package]] @@ -828,7 +829,7 @@ wheels = [ [[package]] name = "typer" -version = "0.16.0" +version = "0.16.1" source = { registry = "https://pypi.python.org/simple" } dependencies = [ { name = "click", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, @@ -836,9 +837,9 @@ dependencies = [ { name = "shellingham", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "typing-extensions", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c5/8c/7d682431efca5fd290017663ea4588bf6f2c6aad085c7f108c5dbc316e70/typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b", size = 102625, upload-time = "2025-05-26T14:30:31.824Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/78/d90f616bf5f88f8710ad067c1f8705bf7618059836ca084e5bb2a0855d75/typer-0.16.1.tar.gz", hash = "sha256:d358c65a464a7a90f338e3bb7ff0c74ac081449e53884b12ba658cbd72990614", size = 102836, upload-time = "2025-08-18T19:18:22.898Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/42/3efaf858001d2c2913de7f354563e3a3a2f0decae3efe98427125a8f441e/typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855", size = 46317, upload-time = "2025-05-26T14:30:30.523Z" }, + { url = "https://files.pythonhosted.org/packages/2d/76/06dbe78f39b2203d2a47d5facc5df5102d0561e2807396471b5f7c5a30a1/typer-0.16.1-py3-none-any.whl", hash = "sha256:90ee01cb02d9b8395ae21ee3368421faf21fa138cb2a541ed369c08cec5237c9", size = 46397, upload-time = "2025-08-18T19:18:21.663Z" }, ] [[package]] @@ -852,11 +853,11 @@ wheels = [ [[package]] name = "typing-extensions" -version = "4.14.1" +version = "4.15.0" source = { registry = "https://pypi.python.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] [[package]] @@ -927,9 +928,9 @@ wheels = [ [[package]] name = "xmltodict" -version = "0.14.2" +version = "0.13.0" source = { registry = "https://pypi.python.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/50/05/51dcca9a9bf5e1bce52582683ce50980bcadbc4fa5143b9f2b19ab99958f/xmltodict-0.14.2.tar.gz", hash = "sha256:201e7c28bb210e374999d1dde6382923ab0ed1a8a5faeece48ab525b7810a553", size = 51942, upload-time = "2024-10-16T06:10:29.683Z" } +sdist = { url = "https://files.pythonhosted.org/packages/39/0d/40df5be1e684bbaecdb9d1e0e40d5d482465de6b00cbb92b84ee5d243c7f/xmltodict-0.13.0.tar.gz", hash = "sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56", size = 33813, upload-time = "2022-05-08T07:00:04.916Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/45/fc303eb433e8a2a271739c98e953728422fa61a3c1f36077a49e395c972e/xmltodict-0.14.2-py2.py3-none-any.whl", hash = "sha256:20cc7d723ed729276e808f26fb6b3599f786cbc37e06c65e192ba77c40f20aac", size = 9981, upload-time = "2024-10-16T06:10:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/94/db/fd0326e331726f07ff7f40675cd86aa804bfd2e5016c727fa761c934990e/xmltodict-0.13.0-py2.py3-none-any.whl", hash = "sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852", size = 9971, upload-time = "2022-05-08T07:00:02.898Z" }, ] From 78ca07f5c28d285b8535facfce206f540ee6cc14 Mon Sep 17 00:00:00 2001 From: msankows Date: Mon, 1 Sep 2025 15:36:29 +0200 Subject: [PATCH 11/29] feat: PUT request --- package/src/actions/action_get.py | 2 +- package/src/actions/action_put.py | 44 +++++++++++++++++++++++++++++++ package/src/app.py | 3 ++- package/src/helpers.py | 3 +-- 4 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 package/src/actions/action_put.py diff --git a/package/src/actions/action_get.py b/package/src/actions/action_get.py index 2d1890d..07c9c0b 100644 --- a/package/src/actions/action_get.py +++ b/package/src/actions/action_get.py @@ -45,7 +45,7 @@ class GetDataParams(Params): def get_data(params: GetDataParams, soar: SOARClient, asset: Asset) -> GetDataOutput: - logger.info("In action handler for: http_get") + logger.info("In action handler for: get_data") return make_request( asset=asset, soar=soar, diff --git a/package/src/actions/action_put.py b/package/src/actions/action_put.py new file mode 100644 index 0000000..f01c774 --- /dev/null +++ b/package/src/actions/action_put.py @@ -0,0 +1,44 @@ +from soar_sdk.abstract import SOARClient +from soar_sdk.action_results import ActionOutput, OutputField +from soar_sdk.params import Param, Params + +from ..asset import Asset +from ..classes import ParsedResponseBody +from ..common import logger +from ..request_maker import make_request + +put_action_description = "Perform a REST PUT call to the server" +put_action_type = "generic" + + +class PutDataOutput(ActionOutput): + location: str = OutputField(cef_types=["url"], example_values=["http://192.168.1.26/rest/assets"]) + method: str = OutputField(example_values=["PUT"]) + parsed_response_body: ParsedResponseBody = OutputField(example_values=['{"failed": true, "message": "Requested item not found"}']) + response_body: str = OutputField(example_values=['{"failed": true, "message": "Requested item not found"}']) + response_headers: str + + +class PutDataParams(Params): + location: str = Param( + description="Location (e.g. path/to/endpoint?query=string)", + primary=True, + cef_types=["endpoint"], + ) + body: str = Param(description="PATCH body (query string, JSON, etc.)") + verify_certificate: bool = Param(description="Verify certificates (if using HTTPS)") + headers: str = Param(description="Additional headers (JSON object with headers)") + + +def put_data(params: PutDataParams, soar: SOARClient, asset: Asset) -> PutDataOutput: + logger.info("In action handler for: put_data") + return make_request( + asset=asset, + soar=soar, + method="PUT", + location=params.location, + headers=params.headers, + verify=params.verify_certificate, + body=params.body, + output=PutDataOutput, + ) diff --git a/package/src/app.py b/package/src/app.py index 378d378..8a65442 100644 --- a/package/src/app.py +++ b/package/src/app.py @@ -4,6 +4,7 @@ from .actions.action_get import get_action_description, get_action_type, get_data from .actions.action_post import post_action_description, post_action_type, post_data +from .actions.action_put import put_action_description, put_action_type, put_data from .asset import Asset from .common import logger from .request_maker import make_request @@ -56,6 +57,6 @@ def test_connectivity(soar: SOARClient, asset: Asset) -> None: read_only=False, ) - +app.register_action(put_data, action_type=put_action_type, description=put_action_description, read_only=False) if __name__ == "__main__": app.cli() diff --git a/package/src/helpers.py b/package/src/helpers.py index 6579ea7..03c130f 100644 --- a/package/src/helpers.py +++ b/package/src/helpers.py @@ -19,8 +19,7 @@ def process_xml_response(response) -> dict: def process_json_response(response) -> dict: try: - data = response.json() - return ParsedResponseBody(**data) + return ParsedResponseBody(**response.json()) except json.JSONDecodeError as e: raise ActionFailure(f"Server claimed JSON but failed to parse. Error: {e}") except ValidationError as e: From 6f946c63f5e2ac6c45b6a6b3a7a14e57ab7d5c08 Mon Sep 17 00:00:00 2001 From: msankows Date: Tue, 2 Sep 2025 12:17:01 +0200 Subject: [PATCH 12/29] fix: code refactoring --- package/src/actions/action_get.py | 4 ++-- package/src/actions/action_post.py | 4 ++-- package/src/actions/action_put.py | 4 ++-- package/src/app.py | 15 +++++++-------- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/package/src/actions/action_get.py b/package/src/actions/action_get.py index 07c9c0b..9986148 100644 --- a/package/src/actions/action_get.py +++ b/package/src/actions/action_get.py @@ -20,8 +20,8 @@ from ..common import logger from ..request_maker import make_request -get_action_type = "investigate" -get_action_description = "This App facilitates making HTTP requests as actions" +action_type = "investigate" +action_description = "This App facilitates making HTTP requests as actions" class GetDataOutput(ActionOutput): diff --git a/package/src/actions/action_post.py b/package/src/actions/action_post.py index 34213f6..d9f579f 100644 --- a/package/src/actions/action_post.py +++ b/package/src/actions/action_post.py @@ -7,8 +7,8 @@ from ..common import logger from ..request_maker import make_request -post_action_type = "generic" -post_action_description = "Perform a REST POST call to the server" +action_type = "generic" +action_description = "Perform a REST POST call to the server" class PostDataOutput(ActionOutput): diff --git a/package/src/actions/action_put.py b/package/src/actions/action_put.py index f01c774..208b3db 100644 --- a/package/src/actions/action_put.py +++ b/package/src/actions/action_put.py @@ -7,8 +7,8 @@ from ..common import logger from ..request_maker import make_request -put_action_description = "Perform a REST PUT call to the server" -put_action_type = "generic" +action_description = "Perform a REST PUT call to the server" +action_type = "generic" class PutDataOutput(ActionOutput): diff --git a/package/src/app.py b/package/src/app.py index 8a65442..7471e2a 100644 --- a/package/src/app.py +++ b/package/src/app.py @@ -2,9 +2,7 @@ from soar_sdk.action_results import ActionOutput from soar_sdk.app import App -from .actions.action_get import get_action_description, get_action_type, get_data -from .actions.action_post import post_action_description, post_action_type, post_data -from .actions.action_put import put_action_description, put_action_type, put_data +from .actions import action_get, action_post, action_put from .asset import Asset from .common import logger from .request_maker import make_request @@ -48,15 +46,16 @@ def test_connectivity(soar: SOARClient, asset: Asset) -> None: logger.info("Test connectivity passed!") -app.register_action(get_data, action_type=get_action_type, description=get_action_description) +app.register_action(action_get.get_data, action_type=action_get.action_type, description=action_get.action_description) app.register_action( - post_data, - action_type=post_action_type, - description=post_action_description, + action_post.post_data, + action_type=action_post.action_type, + description=action_post.action_description, read_only=False, ) -app.register_action(put_data, action_type=put_action_type, description=put_action_description, read_only=False) +app.register_action(action_put.put_data, action_type=action_put.action_type, description=action_put.action_description, read_only=False) + if __name__ == "__main__": app.cli() From 325306560230ccb3900289dc2576b813c38a257e Mon Sep 17 00:00:00 2001 From: msankows Date: Thu, 4 Sep 2025 11:09:00 +0200 Subject: [PATCH 13/29] feat: PATCH, HEAD, OPTIONS, DELETE implemented --- package/src/actions/action_delete.py | 39 +++++++++++++++++++++++++++ package/src/actions/action_get.py | 13 +++------ package/src/actions/action_head.py | 38 ++++++++++++++++++++++++++ package/src/actions/action_options.py | 38 ++++++++++++++++++++++++++ package/src/actions/action_patch.py | 39 +++++++++++++++++++++++++++ package/src/actions/action_post.py | 12 +++------ package/src/actions/action_put.py | 11 +++----- package/src/app.py | 15 +++++------ package/src/classes.py | 13 ++++++++- package/src/common.py | 2 +- package/src/helpers.py | 11 +++++--- package/src/request_maker.py | 5 ++-- 12 files changed, 191 insertions(+), 45 deletions(-) create mode 100644 package/src/actions/action_delete.py create mode 100644 package/src/actions/action_head.py create mode 100644 package/src/actions/action_options.py create mode 100644 package/src/actions/action_patch.py diff --git a/package/src/actions/action_delete.py b/package/src/actions/action_delete.py new file mode 100644 index 0000000..25e2c2a --- /dev/null +++ b/package/src/actions/action_delete.py @@ -0,0 +1,39 @@ +from soar_sdk.abstract import SOARClient +from soar_sdk.params import Param, Params + +from ..asset import Asset +from ..classes import BaseHttpOutput +from ..common import logger +from ..request_maker import make_request + +action_description = "Perform a REST DELETE call to the server" +action_type = "generic" + + +class DeleteDataOutput(BaseHttpOutput): + pass + + +class DeleteDataParams(Params): + location: str = Param( + description="Location (e.g. path/to/endpoint?query=string)", + primary=True, + cef_types=["endpoint"], + ) + body: str = Param(description="DELETE body (query string, JSON, etc.)") + verify_certificate: bool = Param(description="Verify certificates (if using HTTPS)") + headers: str = Param(description="Additional headers (JSON object with headers)") + + +def delete_data(params: DeleteDataParams, soar: SOARClient, asset: Asset) -> DeleteDataOutput: + logger.info("In action handler for: head_data") + return make_request( + asset=asset, + soar=soar, + method="DELETE", + location=params.location, + headers=params.headers, + verify=params.verify_certificate, + output=DeleteDataOutput, + body=None, + ) diff --git a/package/src/actions/action_get.py b/package/src/actions/action_get.py index 9986148..482d482 100644 --- a/package/src/actions/action_get.py +++ b/package/src/actions/action_get.py @@ -12,11 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. from soar_sdk.abstract import SOARClient -from soar_sdk.action_results import ActionOutput, OutputField from soar_sdk.params import Param, Params from ..asset import Asset -from ..classes import ParsedResponseBody +from ..classes import BaseHttpOutput from ..common import logger from ..request_maker import make_request @@ -24,14 +23,8 @@ action_description = "This App facilitates making HTTP requests as actions" -class GetDataOutput(ActionOutput): - message: str - summary: str - location: str = OutputField(cef_types=["url"], example_values=["http://192.168.1.26/rest/cont"]) - method: str = OutputField(example_values=["GET"]) - parsed_response_body: ParsedResponseBody = OutputField(example_values=['{"failed": true, "message": "Requested item not found"}']) - response_body: str = OutputField(example_values=['{"failed": true, "message": "Requested item not found"}']) - response_headers: str +class GetDataOutput(BaseHttpOutput): + pass class GetDataParams(Params): diff --git a/package/src/actions/action_head.py b/package/src/actions/action_head.py new file mode 100644 index 0000000..966d250 --- /dev/null +++ b/package/src/actions/action_head.py @@ -0,0 +1,38 @@ +from soar_sdk.abstract import SOARClient +from soar_sdk.params import Param, Params + +from ..asset import Asset +from ..classes import BaseHttpOutput +from ..common import logger +from ..request_maker import make_request + +action_description = "Perform a REST HEAD call to the server" +action_type = "investigate" + + +class GetHeadersOutput(BaseHttpOutput): + pass + + +class GetHeadersParams(Params): + location: str = Param( + description="Location (e.g. path/to/endpoint?query=string)", + primary=True, + cef_types=["endpoint"], + ) + verify_certificate: bool = Param(description="Verify certificates (if using HTTPS)") + headers: str = Param(description="Additional headers (JSON object with headers)") + + +def get_headers(params: GetHeadersParams, soar: SOARClient, asset: Asset) -> GetHeadersOutput: + logger.info("In action handler for: head_data") + return make_request( + asset=asset, + soar=soar, + method="HEAD", + location=params.location, + headers=params.headers, + verify=params.verify_certificate, + output=GetHeadersOutput, + body=None, + ) diff --git a/package/src/actions/action_options.py b/package/src/actions/action_options.py new file mode 100644 index 0000000..11bed7e --- /dev/null +++ b/package/src/actions/action_options.py @@ -0,0 +1,38 @@ +from soar_sdk.abstract import SOARClient +from soar_sdk.params import Param, Params + +from ..asset import Asset +from ..classes import BaseHttpOutput +from ..common import logger +from ..request_maker import make_request + +action_description = "Perform a REST OPTIONS call to the server" +action_type = "investigate" + + +class GetOptionsParams(Params): + location: str = Param( + description="Location (e.g. path/to/endpoint?query=string)", + primary=True, + cef_types=["endpoint"], + ) + verify_certificate: bool = Param(description="Verify certificates (if using HTTPS)") + headers: str = Param(description="Additional headers (JSON object with headers)") + + +class GetOptionsOutput(BaseHttpOutput): + pass + + +def get_options(params: GetOptionsParams, soar: SOARClient, asset: Asset) -> GetOptionsOutput: + logger.info("In action handler for: get_options") + return make_request( + asset=asset, + soar=soar, + method="OPTIONS", + location=params.location, + headers=params.headers, + verify=params.verify_certificate, + output=GetOptionsOutput, + body=None, + ) diff --git a/package/src/actions/action_patch.py b/package/src/actions/action_patch.py new file mode 100644 index 0000000..d0d799d --- /dev/null +++ b/package/src/actions/action_patch.py @@ -0,0 +1,39 @@ +from soar_sdk.abstract import SOARClient +from soar_sdk.params import Param, Params + +from ..asset import Asset +from ..classes import BaseHttpOutput +from ..common import logger +from ..request_maker import make_request + +action_description = "Perform a REST PATCH call to the server" +action_type = "generic" + + +class PatchDataParams(Params): + location: str = Param( + description="Location (e.g. path/to/endpoint?query=string)", + primary=True, + cef_types=["endpoint"], + ) + body: str = Param(description="PATCH body (query string, JSON, etc.)") + verify_certificate: bool = Param(description="Verify certificates (if using HTTPS)") + headers: str = Param(description="Additional headers (JSON object with headers)") + + +class PatchDataOutput(BaseHttpOutput): + pass + + +def patch_data(params: PatchDataParams, soar: SOARClient, asset: Asset) -> PatchDataOutput: + logger.info("In action handler for: patch_data") + return make_request( + asset=asset, + soar=soar, + method="PATCH", + location=params.location, + headers=params.headers, + verify=params.verify_certificate, + output=PatchDataOutput, + body=None, + ) diff --git a/package/src/actions/action_post.py b/package/src/actions/action_post.py index d9f579f..305e189 100644 --- a/package/src/actions/action_post.py +++ b/package/src/actions/action_post.py @@ -1,9 +1,8 @@ from soar_sdk.abstract import SOARClient -from soar_sdk.action_results import ActionOutput, OutputField from soar_sdk.params import Param, Params from ..asset import Asset -from ..classes import ParsedResponseBody +from ..classes import BaseHttpOutput from ..common import logger from ..request_maker import make_request @@ -11,13 +10,8 @@ action_description = "Perform a REST POST call to the server" -class PostDataOutput(ActionOutput): - message: str - location: str = OutputField(cef_types=["url"], example_values=["http://192.168.1.26/rest/assets"]) - method: str = OutputField(example_values=["POST"]) - parsed_response_body: ParsedResponseBody = OutputField(example_values=['{"failed": true, "message": "Requested item not found"}']) - response_body: str = OutputField(example_values=['{"failed": true, "message": "Requested item not found"}']) - response_headers: str +class PostDataOutput(BaseHttpOutput): + pass class PostDataParams(Params): diff --git a/package/src/actions/action_put.py b/package/src/actions/action_put.py index 208b3db..fe13f86 100644 --- a/package/src/actions/action_put.py +++ b/package/src/actions/action_put.py @@ -1,9 +1,8 @@ from soar_sdk.abstract import SOARClient -from soar_sdk.action_results import ActionOutput, OutputField from soar_sdk.params import Param, Params from ..asset import Asset -from ..classes import ParsedResponseBody +from ..classes import BaseHttpOutput from ..common import logger from ..request_maker import make_request @@ -11,12 +10,8 @@ action_type = "generic" -class PutDataOutput(ActionOutput): - location: str = OutputField(cef_types=["url"], example_values=["http://192.168.1.26/rest/assets"]) - method: str = OutputField(example_values=["PUT"]) - parsed_response_body: ParsedResponseBody = OutputField(example_values=['{"failed": true, "message": "Requested item not found"}']) - response_body: str = OutputField(example_values=['{"failed": true, "message": "Requested item not found"}']) - response_headers: str +class PutDataOutput(BaseHttpOutput): + pass class PutDataParams(Params): diff --git a/package/src/app.py b/package/src/app.py index 7471e2a..8eb0190 100644 --- a/package/src/app.py +++ b/package/src/app.py @@ -2,7 +2,7 @@ from soar_sdk.action_results import ActionOutput from soar_sdk.app import App -from .actions import action_get, action_post, action_put +from .actions import action_delete, action_get, action_head, action_options, action_patch, action_post, action_put from .asset import Asset from .common import logger from .request_maker import make_request @@ -47,15 +47,12 @@ def test_connectivity(soar: SOARClient, asset: Asset) -> None: app.register_action(action_get.get_data, action_type=action_get.action_type, description=action_get.action_description) - -app.register_action( - action_post.post_data, - action_type=action_post.action_type, - description=action_post.action_description, - read_only=False, -) - +app.register_action(action_post.post_data, action_type=action_post.action_type, description=action_post.action_description, read_only=False) app.register_action(action_put.put_data, action_type=action_put.action_type, description=action_put.action_description, read_only=False) +app.register_action(action_patch.patch_data, action_type=action_patch.action_type, description=action_patch.action_description) +app.register_action(action_delete.delete_data, action_type=action_delete.action_type, description=action_delete.action_description) +app.register_action(action_head.get_headers, action_type=action_head.action_type, description=action_head.action_description) +app.register_action(action_options.get_options, action_type=action_options.action_type, description=action_options.action_description) if __name__ == "__main__": app.cli() diff --git a/package/src/classes.py b/package/src/classes.py index 21f0557..9062e37 100644 --- a/package/src/classes.py +++ b/package/src/classes.py @@ -1,8 +1,19 @@ from pydantic import Extra -from soar_sdk.action_results import ActionOutput +from soar_sdk.action_results import ActionOutput, GenericActionOutput, OutputField class ParsedResponseBody(ActionOutput): class Config: extra = Extra.allow + + +class BaseHttpOutput(GenericActionOutput): + location: str = OutputField(cef_types=["url"], example_values=["http://192.168.1.26/rest/assets"]) + method: str = OutputField(example_values=["POST"]) + parsed_response_body: ParsedResponseBody = OutputField(example_values=['{"failed": true, "message": "Requested item not found"}']) + response_body: str = OutputField(example_values=['{"failed": true, "message": "Requested item not found"}']) + response_headers: str + + def generate_action_summary_message(self) -> str: + return f"Status code: {self.status_code}" diff --git a/package/src/common.py b/package/src/common.py index e0299aa..fd13347 100644 --- a/package/src/common.py +++ b/package/src/common.py @@ -1,3 +1,3 @@ from soar_sdk.logging import getLogger -logger = getLogger() \ No newline at end of file +logger = getLogger() diff --git a/package/src/helpers.py b/package/src/helpers.py index 03c130f..fc929da 100644 --- a/package/src/helpers.py +++ b/package/src/helpers.py @@ -26,13 +26,15 @@ def process_json_response(response) -> dict: raise ActionFailure(f"Response JSON did not match expected structure. Details: {e}") -def process_html_response(response) -> str: +def process_html_response(response) -> ParsedResponseBody: try: soup = BeautifulSoup(response.text, "html.parser") for element in soup(["script", "style", "footer", "nav"]): element.extract() error_text_lines = [x.strip() for x in soup.text.split("\n") if x.strip()] - return "\n".join(error_text_lines) + cleaned_text = "\n".join(error_text_lines) + data_dict = {"cleaned_html_text": cleaned_text} + return ParsedResponseBody(**data_dict) except Exception as e: raise ActionFailure(f"Unable to parse HTML response. Error: {e}") @@ -43,8 +45,9 @@ def process_empty_response(content_type) -> dict: return {"message": message} -def process_text_response(response) -> str: - return response.text +def process_text_response(response) -> ParsedResponseBody: + data_dict = {"raw_text": response.text} + return ParsedResponseBody(**data_dict) RESPONSE_HANDLERS = { diff --git a/package/src/request_maker.py b/package/src/request_maker.py index 760586f..d990669 100644 --- a/package/src/request_maker.py +++ b/package/src/request_maker.py @@ -54,10 +54,9 @@ def make_request( parsed_body, raw_body = helpers.handle_various_response(response) logger.info(f"Successfully processed data. Status: {response.status_code}") - success_message = f"Status code: {response.status_code}" + return output( - message=success_message, - summary=success_message, + status_code=response.status_code, location=full_url, method=method, parsed_response_body=parsed_body, From ea480310277fe1b943c7cb8c96dcc909023db6fd Mon Sep 17 00:00:00 2001 From: NiemySpiewak-splunk Date: Thu, 4 Sep 2025 11:11:18 +0200 Subject: [PATCH 14/29] small logger fix --- package/src/actions/action_delete.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/src/actions/action_delete.py b/package/src/actions/action_delete.py index 25e2c2a..1038f54 100644 --- a/package/src/actions/action_delete.py +++ b/package/src/actions/action_delete.py @@ -26,7 +26,7 @@ class DeleteDataParams(Params): def delete_data(params: DeleteDataParams, soar: SOARClient, asset: Asset) -> DeleteDataOutput: - logger.info("In action handler for: head_data") + logger.info("In action handler for: delete_data") return make_request( asset=asset, soar=soar, From dc566790a93e68323fc1635fb32271bfb23fbb7f Mon Sep 17 00:00:00 2001 From: msankows Date: Thu, 4 Sep 2025 12:09:59 +0200 Subject: [PATCH 15/29] code refactoring --- package/src/actions/action_delete.py | 13 +++---------- package/src/actions/action_get.py | 13 +++---------- package/src/actions/action_head.py | 13 +++---------- package/src/actions/action_options.py | 13 +++---------- package/src/actions/action_patch.py | 13 +++---------- package/src/actions/action_post.py | 13 +++---------- package/src/actions/action_put.py | 13 +++---------- package/src/classes.py | 11 +++++++++++ 8 files changed, 32 insertions(+), 70 deletions(-) diff --git a/package/src/actions/action_delete.py b/package/src/actions/action_delete.py index 25e2c2a..8d03c7b 100644 --- a/package/src/actions/action_delete.py +++ b/package/src/actions/action_delete.py @@ -1,8 +1,8 @@ from soar_sdk.abstract import SOARClient -from soar_sdk.params import Param, Params +from soar_sdk.params import Param from ..asset import Asset -from ..classes import BaseHttpOutput +from ..classes import BaseHttpOutput, BaseHttpParams from ..common import logger from ..request_maker import make_request @@ -14,15 +14,8 @@ class DeleteDataOutput(BaseHttpOutput): pass -class DeleteDataParams(Params): - location: str = Param( - description="Location (e.g. path/to/endpoint?query=string)", - primary=True, - cef_types=["endpoint"], - ) +class DeleteDataParams(BaseHttpParams): body: str = Param(description="DELETE body (query string, JSON, etc.)") - verify_certificate: bool = Param(description="Verify certificates (if using HTTPS)") - headers: str = Param(description="Additional headers (JSON object with headers)") def delete_data(params: DeleteDataParams, soar: SOARClient, asset: Asset) -> DeleteDataOutput: diff --git a/package/src/actions/action_get.py b/package/src/actions/action_get.py index 482d482..eca23e1 100644 --- a/package/src/actions/action_get.py +++ b/package/src/actions/action_get.py @@ -12,10 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. from soar_sdk.abstract import SOARClient -from soar_sdk.params import Param, Params from ..asset import Asset -from ..classes import BaseHttpOutput +from ..classes import BaseHttpOutput, BaseHttpParams from ..common import logger from ..request_maker import make_request @@ -27,14 +26,8 @@ class GetDataOutput(BaseHttpOutput): pass -class GetDataParams(Params): - location: str = Param( - description="Location (e.g. path/to/endpoint?query=string)", - primary=True, - cef_types=["endpoint"], - ) - verify_certificate: bool = Param(description="Verify certificates (if using HTTPS)") - headers: str = Param(description="Additional headers (JSON object with headers)", required=False) +class GetDataParams(BaseHttpParams): + pass def get_data(params: GetDataParams, soar: SOARClient, asset: Asset) -> GetDataOutput: diff --git a/package/src/actions/action_head.py b/package/src/actions/action_head.py index 966d250..315ecfb 100644 --- a/package/src/actions/action_head.py +++ b/package/src/actions/action_head.py @@ -1,8 +1,7 @@ from soar_sdk.abstract import SOARClient -from soar_sdk.params import Param, Params from ..asset import Asset -from ..classes import BaseHttpOutput +from ..classes import BaseHttpOutput, BaseHttpParams from ..common import logger from ..request_maker import make_request @@ -14,14 +13,8 @@ class GetHeadersOutput(BaseHttpOutput): pass -class GetHeadersParams(Params): - location: str = Param( - description="Location (e.g. path/to/endpoint?query=string)", - primary=True, - cef_types=["endpoint"], - ) - verify_certificate: bool = Param(description="Verify certificates (if using HTTPS)") - headers: str = Param(description="Additional headers (JSON object with headers)") +class GetHeadersParams(BaseHttpParams): + pass def get_headers(params: GetHeadersParams, soar: SOARClient, asset: Asset) -> GetHeadersOutput: diff --git a/package/src/actions/action_options.py b/package/src/actions/action_options.py index 11bed7e..ff6b508 100644 --- a/package/src/actions/action_options.py +++ b/package/src/actions/action_options.py @@ -1,8 +1,7 @@ from soar_sdk.abstract import SOARClient -from soar_sdk.params import Param, Params from ..asset import Asset -from ..classes import BaseHttpOutput +from ..classes import BaseHttpOutput, BaseHttpParams from ..common import logger from ..request_maker import make_request @@ -10,14 +9,8 @@ action_type = "investigate" -class GetOptionsParams(Params): - location: str = Param( - description="Location (e.g. path/to/endpoint?query=string)", - primary=True, - cef_types=["endpoint"], - ) - verify_certificate: bool = Param(description="Verify certificates (if using HTTPS)") - headers: str = Param(description="Additional headers (JSON object with headers)") +class GetOptionsParams(BaseHttpParams): + pass class GetOptionsOutput(BaseHttpOutput): diff --git a/package/src/actions/action_patch.py b/package/src/actions/action_patch.py index d0d799d..b516d12 100644 --- a/package/src/actions/action_patch.py +++ b/package/src/actions/action_patch.py @@ -1,8 +1,8 @@ from soar_sdk.abstract import SOARClient -from soar_sdk.params import Param, Params +from soar_sdk.params import Param from ..asset import Asset -from ..classes import BaseHttpOutput +from ..classes import BaseHttpOutput, BaseHttpParams from ..common import logger from ..request_maker import make_request @@ -10,15 +10,8 @@ action_type = "generic" -class PatchDataParams(Params): - location: str = Param( - description="Location (e.g. path/to/endpoint?query=string)", - primary=True, - cef_types=["endpoint"], - ) +class PatchDataParams(BaseHttpParams): body: str = Param(description="PATCH body (query string, JSON, etc.)") - verify_certificate: bool = Param(description="Verify certificates (if using HTTPS)") - headers: str = Param(description="Additional headers (JSON object with headers)") class PatchDataOutput(BaseHttpOutput): diff --git a/package/src/actions/action_post.py b/package/src/actions/action_post.py index 305e189..d9e47fa 100644 --- a/package/src/actions/action_post.py +++ b/package/src/actions/action_post.py @@ -1,8 +1,8 @@ from soar_sdk.abstract import SOARClient -from soar_sdk.params import Param, Params +from soar_sdk.params import Param from ..asset import Asset -from ..classes import BaseHttpOutput +from ..classes import BaseHttpOutput, BaseHttpParams from ..common import logger from ..request_maker import make_request @@ -14,15 +14,8 @@ class PostDataOutput(BaseHttpOutput): pass -class PostDataParams(Params): - location: str = Param( - description="Location (e.g. path/to/endpoint)", - primary=True, - cef_types=["endpoint"], - ) +class PostDataParams(BaseHttpParams): body: str = Param(description="POST body (query string, JSON, etc.)", required=False) - verify_certificate: bool = Param(description="Verify certificates (if using HTTPS)") - headers: str = Param(description="Additional headers (JSON object with headers)", required=False) def post_data(params: PostDataParams, soar: SOARClient, asset: Asset) -> PostDataOutput: diff --git a/package/src/actions/action_put.py b/package/src/actions/action_put.py index fe13f86..a43df19 100644 --- a/package/src/actions/action_put.py +++ b/package/src/actions/action_put.py @@ -1,8 +1,8 @@ from soar_sdk.abstract import SOARClient -from soar_sdk.params import Param, Params +from soar_sdk.params import Param from ..asset import Asset -from ..classes import BaseHttpOutput +from ..classes import BaseHttpOutput, BaseHttpParams from ..common import logger from ..request_maker import make_request @@ -14,15 +14,8 @@ class PutDataOutput(BaseHttpOutput): pass -class PutDataParams(Params): - location: str = Param( - description="Location (e.g. path/to/endpoint?query=string)", - primary=True, - cef_types=["endpoint"], - ) +class PutDataParams(BaseHttpParams): body: str = Param(description="PATCH body (query string, JSON, etc.)") - verify_certificate: bool = Param(description="Verify certificates (if using HTTPS)") - headers: str = Param(description="Additional headers (JSON object with headers)") def put_data(params: PutDataParams, soar: SOARClient, asset: Asset) -> PutDataOutput: diff --git a/package/src/classes.py b/package/src/classes.py index 9062e37..def4fec 100644 --- a/package/src/classes.py +++ b/package/src/classes.py @@ -1,5 +1,6 @@ from pydantic import Extra from soar_sdk.action_results import ActionOutput, GenericActionOutput, OutputField +from soar_sdk.params import Param, Params class ParsedResponseBody(ActionOutput): @@ -17,3 +18,13 @@ class BaseHttpOutput(GenericActionOutput): def generate_action_summary_message(self) -> str: return f"Status code: {self.status_code}" + + +class BaseHttpParams(Params): + location: str = Param( + description="Location (e.g. path/to/endpoint)", + primary=True, + cef_types=["endpoint"], + ) + verify_certificate: bool = Param(description="Verify certificates (if using HTTPS)") + headers: str = Param(description="Additional headers (JSON object with headers)", required=False) From f1e80c8233ae6cc1dbf7ad89c445f42a00c07030 Mon Sep 17 00:00:00 2001 From: msankows Date: Tue, 9 Sep 2025 18:53:11 +0200 Subject: [PATCH 16/29] feat: put file and get file implementation --- package/pyproject.toml | 2 + package/src/actions/get_file.py | 84 +++++++++++++ package/src/actions/put_file.py | 77 ++++++++++++ package/src/app.py | 7 +- package/src/auth.py | 13 +- package/src/request_maker.py | 48 +++++--- package/uv.lock | 211 +++++++++++++++++++++----------- 7 files changed, 343 insertions(+), 99 deletions(-) create mode 100644 package/src/actions/get_file.py create mode 100644 package/src/actions/put_file.py diff --git a/package/pyproject.toml b/package/pyproject.toml index bafafdf..21d1db0 100644 --- a/package/pyproject.toml +++ b/package/pyproject.toml @@ -8,6 +8,8 @@ authors = [ ] dependencies = [ "splunk-soar-sdk>=1.0.0b55", + "validators", + "xmltodict", ] [dependency-groups] diff --git a/package/src/actions/get_file.py b/package/src/actions/get_file.py new file mode 100644 index 0000000..ee6c5fb --- /dev/null +++ b/package/src/actions/get_file.py @@ -0,0 +1,84 @@ +from urllib.parse import quote, unquote_plus + +import requests +import validators +from soar_sdk.abstract import SOARClient +from soar_sdk.action_results import ActionOutput +from soar_sdk.exceptions import ActionFailure +from soar_sdk.params import Param, Params + +from ..asset import Asset +from ..auth import get_auth_method + + +class GetFileParams(Params): + hostname: str = Param( + description="Hostname to execute command on", + primary=True, + cef_types=["host name"], + ) + file_path: str = Param( + description="Path of the file to download (include filename)", + primary=True, + cef_types=["file path"], + ) + verify_certificate: bool = Param(description="Verify certificates (if using HTTPS)") + + +class GetFileOutput(ActionOutput): + vault_id: str + file_name: str + + +action_description = "Retrieve a file from the endpoint and save it to the vault" +action_type = "investigate" +verbose = "Provide the file path and file name to download into the vault. For example, /web_storage/file.tgz." + + +def get_file(params: GetFileParams, soar: SOARClient, asset: Asset) -> GetFileOutput: + hostname = params.hostname.strip(" ").strip("/") or asset.base_url + file_path = params.file_path.strip() + if not file_path: + raise ActionFailure("Parameter 'file_path' cannot be empty.") + encoded_file_path = quote(file_path) + validate_url = f"{hostname}/{encoded_file_path}" + + if not validators.url(validate_url): + raise ActionFailure(f"Invalid URL constructed based on hostname and file_path: {validate_url}") + full_url = f"{hostname}/{file_path}" + + file_name = unquote_plus(file_path.split("/")[-1]) + + auth_strategy = get_auth_method(asset, soar) + auth_object, final_headers = auth_strategy.create_auth({}) + + try: + response = requests.get( + url=full_url, + auth=auth_object, + headers=final_headers, + verify=params.verify_certificate, + timeout=asset.timeout, + ) + response.raise_for_status() + except requests.exceptions.RequestException as e: + raise ActionFailure(f"Failed to download file from {full_url}. Details: {e}") + + try: + file_content = response.content + if not file_content: + raise ActionFailure("Downloaded file is empty.") + + vault_add_result = soar.vault.create_attachment( + file_content=file_content, file_name=file_name, container_id=soar.get_container_id, metadata=None + ) + + if not vault_add_result.success: + raise ActionFailure(f"Failed to add file to vault. Reason: {vault_add_result.message}") + + return GetFileOutput( + vault_id=vault_add_result.vault_id, + file_name=file_name, + ) + except Exception as e: + raise ActionFailure(f"An error occurred while saving the file to the vault. Details: {e}") diff --git a/package/src/actions/put_file.py b/package/src/actions/put_file.py new file mode 100644 index 0000000..6910c4d --- /dev/null +++ b/package/src/actions/put_file.py @@ -0,0 +1,77 @@ +from urllib.parse import quote + +import requests +from soar_sdk.abstract import SOARClient +from soar_sdk.action_results import ActionOutput +from soar_sdk.exceptions import ActionFailure +from soar_sdk.params import Param, Params + +from ..asset import Asset +from ..auth import get_auth_method +from ..common import logger + + +class PutFileParams(Params): + host: str = Param( + description="Hostname/IP with port number to execute command on", + primary=True, + cef_types=["host name"], + ) + vault_id: str = Param(description="Vault ID of file", primary=True, cef_types=["vault id"]) + file_destination: str = Param( + description="File destination path (exclude filename)", + primary=True, + cef_types=["file path"], + ) + file_name: str = Param(description="Name of the file to be put on endpoint") + verify_certificate: bool = Param(description="Verify certificates (if using HTTPS)") + + +class PutFileOutput(ActionOutput): + file_sent: str + + +action_description = "Put a file from the vault to another location" +action_type = "generic" +verbose = "Provide the path to store the file on the file server. For example, /web_storage/test_repo/." + + +def put_file(params: PutFileParams, soar: SOARClient, asset: Asset) -> PutFileOutput: + try: + attachments = soar.vault.get_attachment(vault_id=params.vault_id) + if not attachments: + raise ActionFailure(f"File with vault_id '{params.vault_id}' not found in vault.") + vault_attachment = attachments[0] + file_name_to_send = params.file_name or vault_attachment.name + if params.file_name: + if vault_attachment.name != params.file_name: + logger.warning( + f"Provided file_name '{params.file_name}' does not match the name in vault '{vault_attachment.name}'. Using provided name." + ) + with vault_attachment.open() as f: + base_url = params.host or asset.base_url + full_url = f"{base_url.rstrip('/')}/{params.file_destination.lstrip('/')}/{quote(file_name_to_send)}" + auth_strategy = get_auth_method(asset, soar) + auth_object, final_headers = auth_strategy.create_auth({}) + files_payload = {"file": f} + query_params = {"file_path": params.file_destination} + + response = requests.post( + uri=full_url, + auth=auth_object, + headers=final_headers, + params=query_params, + files=files_payload, + verify=params.verify_certificate, + timeout=asset.timeout, + ) + response.raise_for_status() + + except requests.exceptions.RequestException as e: + raise ActionFailure(f"Failed to upload file to {full_url}. Details: {e}") + except Exception as e: + raise ActionFailure(f"An unexpected error occurred. Details: {e}") + + return PutFileOutput( + file_sent=full_url, + ) diff --git a/package/src/app.py b/package/src/app.py index 8eb0190..4babd4f 100644 --- a/package/src/app.py +++ b/package/src/app.py @@ -2,7 +2,7 @@ from soar_sdk.action_results import ActionOutput from soar_sdk.app import App -from .actions import action_delete, action_get, action_head, action_options, action_patch, action_post, action_put +from .actions import action_delete, action_get, action_head, action_options, action_patch, action_post, action_put, get_file, put_file from .asset import Asset from .common import logger from .request_maker import make_request @@ -53,6 +53,11 @@ def test_connectivity(soar: SOARClient, asset: Asset) -> None: app.register_action(action_delete.delete_data, action_type=action_delete.action_type, description=action_delete.action_description) app.register_action(action_head.get_headers, action_type=action_head.action_type, description=action_head.action_description) app.register_action(action_options.get_options, action_type=action_options.action_type, description=action_options.action_description) +app.register_action( + put_file.put_file, action_type=put_file.action_type, description=put_file.action_description, read_only=False, verbose=put_file.verbose +) +app.register_action(get_file.get_file, action_type=get_file.action_type, description=get_file.action_description, verbose=get_file.verbose) + if __name__ == "__main__": app.cli() diff --git a/package/src/auth.py b/package/src/auth.py index 9339d9e..e95e527 100644 --- a/package/src/auth.py +++ b/package/src/auth.py @@ -52,7 +52,7 @@ def __init__(self, asset, soar_client): self.soar = soar_client self.state_key = f"oauth_token_{asset.asset_id}" - def __generate_new_token(self): + def _generate_new_token(self): token_url = self.asset.oauth_token_url client_id = self.asset.client_id client_secret = self.asset.client_secret @@ -78,18 +78,17 @@ def __generate_new_token(self): if not access_token: raise ActionFailure("Access token not found in response body") - self.soar.set_state({self.state_key: access_token}) + self.soar.auth_state[self.state_key] = access_token return access_token - def __get_token(self) -> str: - state = self.soar.get_state() - cached_token = state.get(self.state_key) + def get_token(self, force_new: bool = False) -> str: + cached_token = self.soar.auth_state.get(self.state_key) - if cached_token: + if cached_token and not force_new: return cached_token - return self.__generate_new_token() + return self._generate_new_token() def create_auth(self, headers: dict) -> tuple[None, dict]: access_token = self.__get_token() diff --git a/package/src/request_maker.py b/package/src/request_maker.py index d990669..1368898 100644 --- a/package/src/request_maker.py +++ b/package/src/request_maker.py @@ -8,7 +8,7 @@ from . import helpers from .asset import Asset -from .auth import get_auth_method +from .auth import OAuth, get_auth_method from .common import logger @@ -30,27 +30,37 @@ def make_request( logger.info(f"Making {method} request to: {full_url}") - auth_method = get_auth_method(asset, soar) - auth_object, final_headers = auth_method.create_auth(parsed_headers) - body = UnicodeDammit(body).unicode_markup.encode("utf-8") if isinstance(body, str) else body - try: - response = requests.request( - method=method, - url=full_url, - auth=auth_object, - data=body, - verify=verify, - headers=final_headers, - timeout=asset.timeout, - params=None, - ) - response.raise_for_status() + retries = 1 + response = None + + while retries >= 0: + auth_method = get_auth_method(asset, soar) + auth_object, final_headers = auth_method.create_auth(parsed_headers) + + try: + response = requests.request( + method=method, + url=full_url, + auth=auth_object, + data=body, + verify=verify, + headers=final_headers, + timeout=asset.timeout, + ) + response.raise_for_status() + + break - except requests.exceptions.RequestException as e: - logger.error(f"Request failed for {full_url}. Details: {e}") - raise ActionFailure(f"Request failed for {full_url}. Details: {e}") from e + except requests.exceptions.RequestException as e: + if isinstance(auth_method, OAuth) and e.response and e.response.status_code == 401 and retries > 0: + logger.warning("Request failed with 401, token might be expired. Forcing a refresh.") + auth_method.get_token(force_new=True) + retries -= 1 + continue + else: + raise ActionFailure(f"Request failed for {full_url}. Details: {e}") parsed_body, raw_body = helpers.handle_various_response(response) logger.info(f"Successfully processed data. Status: {response.status_code}") diff --git a/package/uv.lock b/package/uv.lock index 2993397..0e99677 100644 --- a/package/uv.lock +++ b/package/uv.lock @@ -65,6 +65,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fc/55/96142937f66150805c25c4d0f31ee4132fd33497753400734f9dfdcbdc66/bleach-6.2.0-py3-none-any.whl", hash = "sha256:117d9c6097a7c3d22fd578fcd8d35ff1e125df6736f554da4e432fdd63f31e5e", size = 163406, upload-time = "2024-10-29T18:30:38.186Z" }, ] +[[package]] +name = "build" +version = "1.3.0" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "packaging", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pyproject-hooks", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "tomli", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/1c/23e33405a7c9eac261dff640926b8b5adaed6a6eb3e1767d441ed611d0c0/build-1.3.0.tar.gz", hash = "sha256:698edd0ea270bde950f53aed21f3a0135672206f3911e0176261a31e0e07b397", size = 48544, upload-time = "2025-08-01T21:27:09.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/8c/2b30c12155ad8de0cf641d76a8b396a16d2c36bc6d50b621a62b7c4567c1/build-1.3.0-py3-none-any.whl", hash = "sha256:7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4", size = 23382, upload-time = "2025-08-01T21:27:07.844Z" }, +] + [[package]] name = "certifi" version = "2025.8.3" @@ -157,47 +172,47 @@ wheels = [ [[package]] name = "coverage" -version = "7.10.5" -source = { registry = "https://pypi.python.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/61/83/153f54356c7c200013a752ce1ed5448573dca546ce125801afca9e1ac1a4/coverage-7.10.5.tar.gz", hash = "sha256:f2e57716a78bc3ae80b2207be0709a3b2b63b9f2dcf9740ee6ac03588a2015b6", size = 821662, upload-time = "2025-08-23T14:42:44.78Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/af/70/e77b0061a6c7157bfce645c6b9a715a08d4c86b3360a7b3252818080b817/coverage-7.10.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c6a5c3414bfc7451b879141ce772c546985163cf553f08e0f135f0699a911801", size = 216774, upload-time = "2025-08-23T14:40:26.301Z" }, - { url = "https://files.pythonhosted.org/packages/91/08/2a79de5ecf37ee40f2d898012306f11c161548753391cec763f92647837b/coverage-7.10.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bc8e4d99ce82f1710cc3c125adc30fd1487d3cf6c2cd4994d78d68a47b16989a", size = 217175, upload-time = "2025-08-23T14:40:29.142Z" }, - { url = "https://files.pythonhosted.org/packages/15/06/3a67662c55656702bd398a727a7f35df598eb11104fcb34f1ecbb070291a/coverage-7.10.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:73269df37883e02d460bee0cc16be90509faea1e3bd105d77360b512d5bb9c33", size = 245740, upload-time = "2025-08-23T14:40:32.302Z" }, - { url = "https://files.pythonhosted.org/packages/00/f4/f8763aabf4dc30ef0d0012522d312f0b7f9fede6246a1f27dbcc4a1e523c/coverage-7.10.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f8a81b0614642f91c9effd53eec284f965577591f51f547a1cbeb32035b4c2f", size = 247600, upload-time = "2025-08-23T14:40:33.66Z" }, - { url = "https://files.pythonhosted.org/packages/9c/31/6632219a9065e1b83f77eda116fed4c76fb64908a6a9feae41816dab8237/coverage-7.10.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6a29f8e0adb7f8c2b95fa2d4566a1d6e6722e0a637634c6563cb1ab844427dd9", size = 245640, upload-time = "2025-08-23T14:40:35.248Z" }, - { url = "https://files.pythonhosted.org/packages/02/b9/57170bd9f3e333837fc24ecc88bc70fbc2eb7ccfd0876854b0c0407078c3/coverage-7.10.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:90dc3d6fb222b194a5de60af8d190bedeeddcbc7add317e4a3cd333ee6b7c879", size = 244537, upload-time = "2025-08-23T14:40:38.737Z" }, - { url = "https://files.pythonhosted.org/packages/cb/f2/336d34d2fc1291ca7c18eeb46f64985e6cef5a1a7ef6d9c23720c6527289/coverage-7.10.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c177e6ffe2ebc7c410785307758ee21258aa8e8092b44d09a2da767834f075f2", size = 216890, upload-time = "2025-08-23T14:40:43.627Z" }, - { url = "https://files.pythonhosted.org/packages/39/ea/92448b07cc1cf2b429d0ce635f59cf0c626a5d8de21358f11e92174ff2a6/coverage-7.10.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:14d6071c51ad0f703d6440827eaa46386169b5fdced42631d5a5ac419616046f", size = 217287, upload-time = "2025-08-23T14:40:45.214Z" }, - { url = "https://files.pythonhosted.org/packages/28/e5/fe3bbc8d097029d284b5fb305b38bb3404895da48495f05bff025df62770/coverage-7.10.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f39071caa126f69d63f99b324fb08c7b1da2ec28cbb1fe7b5b1799926492f65c", size = 249614, upload-time = "2025-08-23T14:40:48.082Z" }, - { url = "https://files.pythonhosted.org/packages/69/9c/a1c89a8c8712799efccb32cd0a1ee88e452f0c13a006b65bb2271f1ac767/coverage-7.10.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343a023193f04d46edc46b2616cdbee68c94dd10208ecd3adc56fcc54ef2baa1", size = 251719, upload-time = "2025-08-23T14:40:49.349Z" }, - { url = "https://files.pythonhosted.org/packages/e9/be/5576b5625865aa95b5633315f8f4142b003a70c3d96e76f04487c3b5cc95/coverage-7.10.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:585ffe93ae5894d1ebdee69fc0b0d4b7c75d8007983692fb300ac98eed146f78", size = 249411, upload-time = "2025-08-23T14:40:50.624Z" }, - { url = "https://files.pythonhosted.org/packages/40/cb/aebb2d8c9e3533ee340bea19b71c5b76605a0268aa49808e26fe96ec0a07/coverage-7.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eb7b0bbf7cc1d0453b843eca7b5fa017874735bef9bfdfa4121373d2cc885ed6", size = 248104, upload-time = "2025-08-23T14:40:54.064Z" }, - { url = "https://files.pythonhosted.org/packages/27/8e/40d75c7128f871ea0fd829d3e7e4a14460cad7c3826e3b472e6471ad05bd/coverage-7.10.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c2d05c7e73c60a4cecc7d9b60dbfd603b4ebc0adafaef371445b47d0f805c8a9", size = 217077, upload-time = "2025-08-23T14:40:59.329Z" }, - { url = "https://files.pythonhosted.org/packages/18/a8/f333f4cf3fb5477a7f727b4d603a2eb5c3c5611c7fe01329c2e13b23b678/coverage-7.10.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:32ddaa3b2c509778ed5373b177eb2bf5662405493baeff52278a0b4f9415188b", size = 217310, upload-time = "2025-08-23T14:41:00.628Z" }, - { url = "https://files.pythonhosted.org/packages/3f/bc/1011da599b414fb6c9c0f34086736126f9ff71f841755786a6b87601b088/coverage-7.10.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7fa22800f3908df31cea6fb230f20ac49e343515d968cc3a42b30d5c3ebf9b5a", size = 251550, upload-time = "2025-08-23T14:41:03.438Z" }, - { url = "https://files.pythonhosted.org/packages/4c/6f/b5c03c0c721c067d21bc697accc3642f3cef9f087dac429c918c37a37437/coverage-7.10.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f366a57ac81f5e12797136552f5b7502fa053c861a009b91b80ed51f2ce651c6", size = 252684, upload-time = "2025-08-23T14:41:04.85Z" }, - { url = "https://files.pythonhosted.org/packages/f9/50/d474bc300ebcb6a38a1047d5c465a227605d6473e49b4e0d793102312bc5/coverage-7.10.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5f1dc8f1980a272ad4a6c84cba7981792344dad33bf5869361576b7aef42733a", size = 250602, upload-time = "2025-08-23T14:41:06.719Z" }, - { url = "https://files.pythonhosted.org/packages/e2/96/a7c3c0562266ac39dcad271d0eec8fc20ab576e3e2f64130a845ad2a557b/coverage-7.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c2492e4dd9daab63f5f56286f8a04c51323d237631eb98505d87e4c4ff19ec34", size = 250158, upload-time = "2025-08-23T14:41:09.749Z" }, - { url = "https://files.pythonhosted.org/packages/9f/08/4166ecfb60ba011444f38a5a6107814b80c34c717bc7a23be0d22e92ca09/coverage-7.10.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ef3b83594d933020f54cf65ea1f4405d1f4e41a009c46df629dd964fcb6e907c", size = 217106, upload-time = "2025-08-23T14:41:15.268Z" }, - { url = "https://files.pythonhosted.org/packages/25/d7/b71022408adbf040a680b8c64bf6ead3be37b553e5844f7465643979f7ca/coverage-7.10.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b96bfdf7c0ea9faebce088a3ecb2382819da4fbc05c7b80040dbc428df6af44", size = 217353, upload-time = "2025-08-23T14:41:16.656Z" }, - { url = "https://files.pythonhosted.org/packages/90/65/28752c3a896566ec93e0219fc4f47ff71bd2b745f51554c93e8dcb659796/coverage-7.10.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8002dc6a049aac0e81ecec97abfb08c01ef0c1fbf962d0c98da3950ace89b869", size = 250955, upload-time = "2025-08-23T14:41:19.577Z" }, - { url = "https://files.pythonhosted.org/packages/a5/eb/ca6b7967f57f6fef31da8749ea20417790bb6723593c8cd98a987be20423/coverage-7.10.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:63d4bb2966d6f5f705a6b0c6784c8969c468dbc4bcf9d9ded8bff1c7e092451f", size = 252230, upload-time = "2025-08-23T14:41:20.959Z" }, - { url = "https://files.pythonhosted.org/packages/bc/29/17a411b2a2a18f8b8c952aa01c00f9284a1fbc677c68a0003b772ea89104/coverage-7.10.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1f672efc0731a6846b157389b6e6d5d5e9e59d1d1a23a5c66a99fd58339914d5", size = 250387, upload-time = "2025-08-23T14:41:22.644Z" }, - { url = "https://files.pythonhosted.org/packages/d1/c6/0ad7d0137257553eb4706b4ad6180bec0a1b6a648b092c5bbda48d0e5b2c/coverage-7.10.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2968647e3ed5a6c019a419264386b013979ff1fb67dd11f5c9886c43d6a31fc2", size = 249894, upload-time = "2025-08-23T14:41:26.165Z" }, - { url = "https://files.pythonhosted.org/packages/d8/a1/590154e6eae07beee3b111cc1f907c30da6fc8ce0a83ef756c72f3c7c748/coverage-7.10.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0520dff502da5e09d0d20781df74d8189ab334a1e40d5bafe2efaa4158e2d9e7", size = 217819, upload-time = "2025-08-23T14:41:31.962Z" }, - { url = "https://files.pythonhosted.org/packages/0d/ff/436ffa3cfc7741f0973c5c89405307fe39b78dcf201565b934e6616fc4ad/coverage-7.10.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d9cd64aca68f503ed3f1f18c7c9174cbb797baba02ca8ab5112f9d1c0328cd4b", size = 218040, upload-time = "2025-08-23T14:41:33.472Z" }, - { url = "https://files.pythonhosted.org/packages/b5/89/21af956843896adc2e64fc075eae3c1cadb97ee0a6960733e65e696f32dd/coverage-7.10.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1b7181c0feeb06ed8a02da02792f42f829a7b29990fef52eff257fef0885d760", size = 261551, upload-time = "2025-08-23T14:41:36.333Z" }, - { url = "https://files.pythonhosted.org/packages/e1/96/390a69244ab837e0ac137989277879a084c786cf036c3c4a3b9637d43a89/coverage-7.10.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36d42b7396b605f774d4372dd9c49bed71cbabce4ae1ccd074d155709dd8f235", size = 263776, upload-time = "2025-08-23T14:41:38.25Z" }, - { url = "https://files.pythonhosted.org/packages/00/32/cfd6ae1da0a521723349f3129b2455832fc27d3f8882c07e5b6fefdd0da2/coverage-7.10.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b4fdc777e05c4940b297bf47bf7eedd56a39a61dc23ba798e4b830d585486ca5", size = 261326, upload-time = "2025-08-23T14:41:40.343Z" }, - { url = "https://files.pythonhosted.org/packages/f4/5d/a234f7409896468e5539d42234016045e4015e857488b0b5b5f3f3fa5f2b/coverage-7.10.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:66c644cbd7aed8fe266d5917e2c9f65458a51cfe5eeff9c05f15b335f697066e", size = 260217, upload-time = "2025-08-23T14:41:43.591Z" }, - { url = "https://files.pythonhosted.org/packages/3b/21/05248e8bc74683488cb7477e6b6b878decadd15af0ec96f56381d3d7ff2d/coverage-7.10.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:62835c1b00c4a4ace24c1a88561a5a59b612fbb83a525d1c70ff5720c97c0610", size = 216763, upload-time = "2025-08-23T14:42:26.75Z" }, - { url = "https://files.pythonhosted.org/packages/a9/7f/161a0ad40cb1c7e19dc1aae106d3430cc88dac3d651796d6cf3f3730c800/coverage-7.10.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5255b3bbcc1d32a4069d6403820ac8e6dbcc1d68cb28a60a1ebf17e47028e898", size = 217154, upload-time = "2025-08-23T14:42:28.238Z" }, - { url = "https://files.pythonhosted.org/packages/6e/4e/2649344e33eeb3567041e8255a1942173cae81817fe06b60f3fafaafe111/coverage-7.10.5-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8048ce4b149c93447a55d279078c8ae98b08a6951a3c4d2d7e87f4efc7bfe100", size = 245412, upload-time = "2025-08-23T14:42:31.296Z" }, - { url = "https://files.pythonhosted.org/packages/ac/b1/b21e1e69986ad89b051dd42c3ef06d9326e03ac3c0c844fc33385d1d9e35/coverage-7.10.5-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4028e7558e268dd8bcf4d9484aad393cafa654c24b4885f6f9474bf53183a82a", size = 247182, upload-time = "2025-08-23T14:42:33.155Z" }, - { url = "https://files.pythonhosted.org/packages/4c/b5/80837be411ae092e03fcc2a7877bd9a659c531eff50453e463057a9eee44/coverage-7.10.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:03f47dc870eec0367fcdd603ca6a01517d2504e83dc18dbfafae37faec66129a", size = 245066, upload-time = "2025-08-23T14:42:34.754Z" }, - { url = "https://files.pythonhosted.org/packages/75/0f/505c6af24a9ae5d8919d209b9c31b7092815f468fa43bec3b1118232c62a/coverage-7.10.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b3dcf2ead47fa8be14224ee817dfc1df98043af568fe120a22f81c0eb3c34ad2", size = 244095, upload-time = "2025-08-23T14:42:38.227Z" }, - { url = "https://files.pythonhosted.org/packages/08/b6/fff6609354deba9aeec466e4bcaeb9d1ed3e5d60b14b57df2a36fb2273f2/coverage-7.10.5-py3-none-any.whl", hash = "sha256:0be24d35e4db1d23d0db5c0f6a74a962e2ec83c426b5cac09f4234aadef38e4a", size = 208736, upload-time = "2025-08-23T14:42:43.145Z" }, +version = "7.10.6" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/14/70/025b179c993f019105b79575ac6edb5e084fb0f0e63f15cdebef4e454fb5/coverage-7.10.6.tar.gz", hash = "sha256:f644a3ae5933a552a29dbb9aa2f90c677a875f80ebea028e5a52a4f429044b90", size = 823736, upload-time = "2025-08-29T15:35:16.668Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/1d/2e64b43d978b5bd184e0756a41415597dfef30fcbd90b747474bd749d45f/coverage-7.10.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:70e7bfbd57126b5554aa482691145f798d7df77489a177a6bef80de78860a356", size = 217025, upload-time = "2025-08-29T15:32:57.169Z" }, + { url = "https://files.pythonhosted.org/packages/23/62/b1e0f513417c02cc10ef735c3ee5186df55f190f70498b3702d516aad06f/coverage-7.10.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e41be6f0f19da64af13403e52f2dec38bbc2937af54df8ecef10850ff8d35301", size = 217419, upload-time = "2025-08-29T15:32:59.908Z" }, + { url = "https://files.pythonhosted.org/packages/fb/6f/5e03631c3305cad187eaf76af0b559fff88af9a0b0c180d006fb02413d7a/coverage-7.10.6-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10356fdd33a7cc06e8051413140bbdc6f972137508a3572e3f59f805cd2832fd", size = 245992, upload-time = "2025-08-29T15:33:03.239Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a1/f30ea0fb400b080730125b490771ec62b3375789f90af0bb68bfb8a921d7/coverage-7.10.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80b1695cf7c5ebe7b44bf2521221b9bb8cdf69b1f24231149a7e3eb1ae5fa2fb", size = 247851, upload-time = "2025-08-29T15:33:04.603Z" }, + { url = "https://files.pythonhosted.org/packages/02/8e/cfa8fee8e8ef9a6bb76c7bef039f3302f44e615d2194161a21d3d83ac2e9/coverage-7.10.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2e4c33e6378b9d52d3454bd08847a8651f4ed23ddbb4a0520227bd346382bbc6", size = 245891, upload-time = "2025-08-29T15:33:06.176Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a6/ba188b376529ce36483b2d585ca7bdac64aacbe5aa10da5978029a9c94db/coverage-7.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7d79dabc0a56f5af990cc6da9ad1e40766e82773c075f09cc571e2076fef882e", size = 244786, upload-time = "2025-08-29T15:33:08.965Z" }, + { url = "https://files.pythonhosted.org/packages/d4/16/2bea27e212c4980753d6d563a0803c150edeaaddb0771a50d2afc410a261/coverage-7.10.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c706db3cabb7ceef779de68270150665e710b46d56372455cd741184f3868d8f", size = 217129, upload-time = "2025-08-29T15:33:13.575Z" }, + { url = "https://files.pythonhosted.org/packages/2a/51/e7159e068831ab37e31aac0969d47b8c5ee25b7d307b51e310ec34869315/coverage-7.10.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e0c38dc289e0508ef68ec95834cb5d2e96fdbe792eaccaa1bccac3966bbadcc", size = 217532, upload-time = "2025-08-29T15:33:14.872Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fb/7435ef8ab9b2594a6e3f58505cc30e98ae8b33265d844007737946c59389/coverage-7.10.6-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:689920ecfd60f992cafca4f5477d55720466ad2c7fa29bb56ac8d44a1ac2b47a", size = 249864, upload-time = "2025-08-29T15:33:17.434Z" }, + { url = "https://files.pythonhosted.org/packages/51/f8/d9d64e8da7bcddb094d511154824038833c81e3a039020a9d6539bf303e9/coverage-7.10.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec98435796d2624d6905820a42f82149ee9fc4f2d45c2c5bc5a44481cc50db62", size = 251969, upload-time = "2025-08-29T15:33:18.822Z" }, + { url = "https://files.pythonhosted.org/packages/43/28/c43ba0ef19f446d6463c751315140d8f2a521e04c3e79e5c5fe211bfa430/coverage-7.10.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b37201ce4a458c7a758ecc4efa92fa8ed783c66e0fa3c42ae19fc454a0792153", size = 249659, upload-time = "2025-08-29T15:33:20.407Z" }, + { url = "https://files.pythonhosted.org/packages/4c/55/0964aa87126624e8c159e32b0bc4e84edef78c89a1a4b924d28dd8265625/coverage-7.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5aea98383463d6e1fa4e95416d8de66f2d0cb588774ee20ae1b28df826bcb619", size = 248351, upload-time = "2025-08-29T15:33:23.105Z" }, + { url = "https://files.pythonhosted.org/packages/26/06/263f3305c97ad78aab066d116b52250dd316e74fcc20c197b61e07eb391a/coverage-7.10.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5b2dd6059938063a2c9fee1af729d4f2af28fd1a545e9b7652861f0d752ebcea", size = 217324, upload-time = "2025-08-29T15:33:29.06Z" }, + { url = "https://files.pythonhosted.org/packages/e9/60/1e1ded9a4fe80d843d7d53b3e395c1db3ff32d6c301e501f393b2e6c1c1f/coverage-7.10.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:388d80e56191bf846c485c14ae2bc8898aa3124d9d35903fef7d907780477634", size = 217560, upload-time = "2025-08-29T15:33:30.748Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1d/ae25a7dc58fcce8b172d42ffe5313fc267afe61c97fa872b80ee72d9515a/coverage-7.10.6-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:961834e2f2b863a0e14260a9a273aff07ff7818ab6e66d2addf5628590c628f9", size = 251802, upload-time = "2025-08-29T15:33:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7a/1f561d47743710fe996957ed7c124b421320f150f1d38523d8d9102d3e2a/coverage-7.10.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf9a19f5012dab774628491659646335b1928cfc931bf8d97b0d5918dd58033c", size = 252935, upload-time = "2025-08-29T15:33:34.909Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ad/8b97cd5d28aecdfde792dcbf646bac141167a5cacae2cd775998b45fabb5/coverage-7.10.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:99c4283e2a0e147b9c9cc6bc9c96124de9419d6044837e9799763a0e29a7321a", size = 250855, upload-time = "2025-08-29T15:33:36.922Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9c/8ce95dee640a38e760d5b747c10913e7a06554704d60b41e73fdea6a1ffd/coverage-7.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cdbe264f11afd69841bd8c0d83ca10b5b32853263ee62e6ac6a0ab63895f972", size = 250409, upload-time = "2025-08-29T15:33:39.447Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e7/917e5953ea29a28c1057729c1d5af9084ab6d9c66217523fd0e10f14d8f6/coverage-7.10.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ffea0575345e9ee0144dfe5701aa17f3ba546f8c3bb48db62ae101afb740e7d6", size = 217351, upload-time = "2025-08-29T15:33:45.438Z" }, + { url = "https://files.pythonhosted.org/packages/eb/86/2e161b93a4f11d0ea93f9bebb6a53f113d5d6e416d7561ca41bb0a29996b/coverage-7.10.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:95d91d7317cde40a1c249d6b7382750b7e6d86fad9d8eaf4fa3f8f44cf171e80", size = 217600, upload-time = "2025-08-29T15:33:47.269Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/508420fb47d09d904d962f123221bc249f64b5e56aa93d5f5f7603be475f/coverage-7.10.6-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f3f56e4cb573755e96a16501a98bf211f100463d70275759e73f3cbc00d4f27", size = 251206, upload-time = "2025-08-29T15:33:50.697Z" }, + { url = "https://files.pythonhosted.org/packages/e9/1f/9020135734184f439da85c70ea78194c2730e56c2d18aee6e8ff1719d50d/coverage-7.10.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db4a1d897bbbe7339946ffa2fe60c10cc81c43fab8b062d3fcb84188688174a4", size = 252478, upload-time = "2025-08-29T15:33:52.303Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a4/3d228f3942bb5a2051fde28c136eea23a761177dc4ff4ef54533164ce255/coverage-7.10.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d8fd7879082953c156d5b13c74aa6cca37f6a6f4747b39538504c3f9c63d043d", size = 250637, upload-time = "2025-08-29T15:33:53.67Z" }, + { url = "https://files.pythonhosted.org/packages/90/26/64eecfa214e80dd1d101e420cab2901827de0e49631d666543d0e53cf597/coverage-7.10.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:61c950fc33d29c91b9e18540e1aed7d9f6787cc870a3e4032493bbbe641d12fc", size = 250143, upload-time = "2025-08-29T15:33:56.386Z" }, + { url = "https://files.pythonhosted.org/packages/d7/1c/ccccf4bf116f9517275fa85047495515add43e41dfe8e0bef6e333c6b344/coverage-7.10.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c9a8b7a34a4de3ed987f636f71881cd3b8339f61118b1aa311fbda12741bff0b", size = 218059, upload-time = "2025-08-29T15:34:02.91Z" }, + { url = "https://files.pythonhosted.org/packages/92/97/8a3ceff833d27c7492af4f39d5da6761e9ff624831db9e9f25b3886ddbca/coverage-7.10.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dd5af36092430c2b075cee966719898f2ae87b636cefb85a653f1d0ba5d5393", size = 218287, upload-time = "2025-08-29T15:34:05.106Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7e/6a7df5a6fb440a0179d94a348eb6616ed4745e7df26bf2a02bc4db72c421/coverage-7.10.6-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d6b9ae13d5d3e8aeca9ca94198aa7b3ebbc5acfada557d724f2a1f03d2c0b0df", size = 261801, upload-time = "2025-08-29T15:34:08.006Z" }, + { url = "https://files.pythonhosted.org/packages/3a/4c/a270a414f4ed5d196b9d3d67922968e768cd971d1b251e1b4f75e9362f75/coverage-7.10.6-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:675824a363cc05781b1527b39dc2587b8984965834a748177ee3c37b64ffeafb", size = 264027, upload-time = "2025-08-29T15:34:09.806Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8b/3210d663d594926c12f373c5370bf1e7c5c3a427519a8afa65b561b9a55c/coverage-7.10.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:692d70ea725f471a547c305f0d0fc6a73480c62fb0da726370c088ab21aed282", size = 261576, upload-time = "2025-08-29T15:34:11.585Z" }, + { url = "https://files.pythonhosted.org/packages/3a/06/d6478d152cd189b33eac691cba27a40704990ba95de49771285f34a5861e/coverage-7.10.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d9369a23186d189b2fc95cc08b8160ba242057e887d766864f7adf3c46b2df21", size = 260468, upload-time = "2025-08-29T15:34:14.571Z" }, + { url = "https://files.pythonhosted.org/packages/91/70/f73ad83b1d2fd2d5825ac58c8f551193433a7deaf9b0d00a8b69ef61cd9a/coverage-7.10.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90558c35af64971d65fbd935c32010f9a2f52776103a259f1dee865fe8259352", size = 217009, upload-time = "2025-08-29T15:34:57.381Z" }, + { url = "https://files.pythonhosted.org/packages/01/e8/099b55cd48922abbd4b01ddd9ffa352408614413ebfc965501e981aced6b/coverage-7.10.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8953746d371e5695405806c46d705a3cd170b9cc2b9f93953ad838f6c1e58612", size = 217400, upload-time = "2025-08-29T15:34:58.985Z" }, + { url = "https://files.pythonhosted.org/packages/01/f9/82c6c061838afbd2172e773156c0aa84a901d59211b4975a4e93accf5c89/coverage-7.10.6-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7eb68d356ba0cc158ca535ce1381dbf2037fa8cb5b1ae5ddfc302e7317d04144", size = 245658, upload-time = "2025-08-29T15:35:02.135Z" }, + { url = "https://files.pythonhosted.org/packages/81/6a/35674445b1d38161148558a3ff51b0aa7f0b54b1def3abe3fbd34efe05bc/coverage-7.10.6-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b15a87265e96307482746d86995f4bff282f14b027db75469c446da6127433b", size = 247433, upload-time = "2025-08-29T15:35:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/98c99e7cafb288730a93535092eb433b5503d529869791681c4f2e2012a8/coverage-7.10.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fc53ba868875bfbb66ee447d64d6413c2db91fddcfca57025a0e7ab5b07d5862", size = 245315, upload-time = "2025-08-29T15:35:05.629Z" }, + { url = "https://files.pythonhosted.org/packages/67/52/d57a42502aef05c6325f28e2e81216c2d9b489040132c18725b7a04d1448/coverage-7.10.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9702b59d582ff1e184945d8b501ffdd08d2cee38d93a2206aa5f1365ce0b8d78", size = 244343, upload-time = "2025-08-29T15:35:09.55Z" }, + { url = "https://files.pythonhosted.org/packages/44/0c/50db5379b615854b5cf89146f8f5bd1d5a9693d7f3a987e269693521c404/coverage-7.10.6-py3-none-any.whl", hash = "sha256:92c4ecf6bf11b2e85fd4d8204814dc26e6a19f0c9d938c207c5cb0eadfcabbe3", size = 208986, upload-time = "2025-08-29T15:35:14.506Z" }, ] [[package]] @@ -260,6 +275,7 @@ version = "4.0.0" source = { virtual = "." } dependencies = [ { name = "splunk-soar-sdk", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "validators", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "xmltodict", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] @@ -277,7 +293,8 @@ dev = [ [package.metadata] requires-dist = [ { name = "splunk-soar-sdk", specifier = ">=1.0.0b55" }, - { name = "xmltodict", specifier = "==0.13.0" }, + { name = "validators" }, + { name = "xmltodict" }, ] [package.metadata.requires-dev] @@ -330,11 +347,11 @@ wheels = [ [[package]] name = "identify" -version = "2.6.13" +version = "2.6.14" source = { registry = "https://pypi.python.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/ca/ffbabe3635bb839aa36b3a893c91a9b0d368cb4d8073e03a12896970af82/identify-2.6.13.tar.gz", hash = "sha256:da8d6c828e773620e13bfa86ea601c5a5310ba4bcd65edf378198b56a1f9fb32", size = 99243, upload-time = "2025-08-09T19:35:00.6Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/c4/62963f25a678f6a050fb0505a65e9e726996171e6dbe1547f79619eefb15/identify-2.6.14.tar.gz", hash = "sha256:663494103b4f717cb26921c52f8751363dc89db64364cd836a9bf1535f53cd6a", size = 99283, upload-time = "2025-09-06T19:30:52.938Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/ce/461b60a3ee109518c055953729bf9ed089a04db895d47e95444071dcdef2/identify-2.6.13-py2.py3-none-any.whl", hash = "sha256:60381139b3ae39447482ecc406944190f690d4a2997f2584062089848361b33b", size = 99153, upload-time = "2025-08-09T19:34:59.1Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ae/2ad30f4652712c82f1c23423d79136fbce338932ad166d70c1efb86a5998/identify-2.6.14-py2.py3-none-any.whl", hash = "sha256:11a073da82212c6646b1f39bb20d4483bfb9543bd5566fec60053c4bb309bf2e", size = 99172, upload-time = "2025-09-06T19:30:51.759Z" }, ] [[package]] @@ -346,6 +363,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "zipp", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, +] + [[package]] name = "iniconfig" version = "2.1.0" @@ -583,6 +612,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] +[[package]] +name = "pyproject-hooks" +version = "1.2.0" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, +] + [[package]] name = "pytest" version = "7.4.4" @@ -601,14 +639,14 @@ wheels = [ [[package]] name = "pytest-mock" -version = "3.14.1" +version = "3.15.0" source = { registry = "https://pypi.python.org/simple" } dependencies = [ { name = "pytest", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/28/67172c96ba684058a4d24ffe144d64783d2a270d0af0d9e792737bddc75c/pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e", size = 33241, upload-time = "2025-05-26T13:58:45.167Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/99/3323ee5c16b3637b4d941c362182d3e749c11e400bea31018c42219f3a98/pytest_mock-3.15.0.tar.gz", hash = "sha256:ab896bd190316b9d5d87b277569dfcdf718b2d049a2ccff5f7aca279c002a1cf", size = 33838, upload-time = "2025-09-04T20:57:48.679Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/05/77b60e520511c53d1c1ca75f1930c7dd8e971d0c4379b7f4b3f9644685ba/pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0", size = 9923, upload-time = "2025-05-26T13:58:43.487Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b3/7fefc43fb706380144bcd293cc6e446e6f637ddfa8b83f48d1734156b529/pytest_mock-3.15.0-py3-none-any.whl", hash = "sha256:ef2219485fb1bd256b00e7ad7466ce26729b30eadfc7cbcdb4fa9a92ca68db6f", size = 10050, upload-time = "2025-09-04T20:57:47.274Z" }, ] [[package]] @@ -711,23 +749,32 @@ wheels = [ [[package]] name = "ruff" -version = "0.12.10" +version = "0.12.12" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/f0/e0965dd709b8cabe6356811c0ee8c096806bb57d20b5019eb4e48a117410/ruff-0.12.12.tar.gz", hash = "sha256:b86cd3415dbe31b3b46a71c598f4c4b2f550346d1ccf6326b347cc0c8fd063d6", size = 5359915, upload-time = "2025-09-04T16:50:18.273Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/79/8d3d687224d88367b51c7974cec1040c4b015772bfbeffac95face14c04a/ruff-0.12.12-py3-none-linux_armv6l.whl", hash = "sha256:de1c4b916d98ab289818e55ce481e2cacfaad7710b01d1f990c497edf217dafc", size = 12116602, upload-time = "2025-09-04T16:49:18.892Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c3/6e599657fe192462f94861a09aae935b869aea8a1da07f47d6eae471397c/ruff-0.12.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7acd6045e87fac75a0b0cdedacf9ab3e1ad9d929d149785903cff9bb69ad9727", size = 12868393, upload-time = "2025-09-04T16:49:23.043Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d2/9e3e40d399abc95336b1843f52fc0daaceb672d0e3c9290a28ff1a96f79d/ruff-0.12.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:abf4073688d7d6da16611f2f126be86523a8ec4343d15d276c614bda8ec44edb", size = 12036967, upload-time = "2025-09-04T16:49:26.04Z" }, + { url = "https://files.pythonhosted.org/packages/e9/03/6816b2ed08836be272e87107d905f0908be5b4a40c14bfc91043e76631b8/ruff-0.12.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:968e77094b1d7a576992ac078557d1439df678a34c6fe02fd979f973af167577", size = 12276038, upload-time = "2025-09-04T16:49:29.056Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d5/707b92a61310edf358a389477eabd8af68f375c0ef858194be97ca5b6069/ruff-0.12.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42a67d16e5b1ffc6d21c5f67851e0e769517fb57a8ebad1d0781b30888aa704e", size = 11901110, upload-time = "2025-09-04T16:49:32.07Z" }, + { url = "https://files.pythonhosted.org/packages/98/0e/91421368ae6c4f3765dd41a150f760c5f725516028a6be30e58255e3c668/ruff-0.12.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:59f909c0fdd8f1dcdbfed0b9569b8bf428cf144bec87d9de298dcd4723f5bee8", size = 14638365, upload-time = "2025-09-04T16:49:38.892Z" }, + { url = "https://files.pythonhosted.org/packages/74/5d/88f3f06a142f58ecc8ecb0c2fe0b82343e2a2b04dcd098809f717cf74b6c/ruff-0.12.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ac93d87047e765336f0c18eacad51dad0c1c33c9df7484c40f98e1d773876f5", size = 14060812, upload-time = "2025-09-04T16:49:42.732Z" }, + { url = "https://files.pythonhosted.org/packages/13/fc/8962e7ddd2e81863d5c92400820f650b86f97ff919c59836fbc4c1a6d84c/ruff-0.12.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01543c137fd3650d322922e8b14cc133b8ea734617c4891c5a9fccf4bfc9aa92", size = 13050208, upload-time = "2025-09-04T16:49:46.434Z" }, + { url = "https://files.pythonhosted.org/packages/53/06/8deb52d48a9a624fd37390555d9589e719eac568c020b27e96eed671f25f/ruff-0.12.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afc2fa864197634e549d87fb1e7b6feb01df0a80fd510d6489e1ce8c0b1cc45", size = 13311444, upload-time = "2025-09-04T16:49:49.931Z" }, + { url = "https://files.pythonhosted.org/packages/2a/81/de5a29af7eb8f341f8140867ffb93f82e4fde7256dadee79016ac87c2716/ruff-0.12.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0c0945246f5ad776cb8925e36af2438e66188d2b57d9cf2eed2c382c58b371e5", size = 13279474, upload-time = "2025-09-04T16:49:53.465Z" }, + { url = "https://files.pythonhosted.org/packages/7f/14/d9577fdeaf791737ada1b4f5c6b59c21c3326f3f683229096cccd7674e0c/ruff-0.12.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a0fbafe8c58e37aae28b84a80ba1817f2ea552e9450156018a478bf1fa80f4e4", size = 12070204, upload-time = "2025-09-04T16:49:56.882Z" }, + { url = "https://files.pythonhosted.org/packages/77/04/a910078284b47fad54506dc0af13839c418ff704e341c176f64e1127e461/ruff-0.12.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b9c456fb2fc8e1282affa932c9e40f5ec31ec9cbb66751a316bd131273b57c23", size = 11880347, upload-time = "2025-09-04T16:49:59.729Z" }, + { url = "https://files.pythonhosted.org/packages/21/9c/28a8dacce4855e6703dcb8cdf6c1705d0b23dd01d60150786cd55aa93b16/ruff-0.12.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:26a1b5a2bf7dd2c47e3b46d077cd9c0fc3b93e6c6cc9ed750bd312ae9dc302ee", size = 13360687, upload-time = "2025-09-04T16:50:05.8Z" }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" source = { registry = "https://pypi.python.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3b/eb/8c073deb376e46ae767f4961390d17545e8535921d2f65101720ed8bd434/ruff-0.12.10.tar.gz", hash = "sha256:189ab65149d11ea69a2d775343adf5f49bb2426fc4780f65ee33b423ad2e47f9", size = 5310076, upload-time = "2025-08-21T18:23:22.595Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/24/e7/560d049d15585d6c201f9eeacd2fd130def3741323e5ccf123786e0e3c95/ruff-0.12.10-py3-none-linux_armv6l.whl", hash = "sha256:8b593cb0fb55cc8692dac7b06deb29afda78c721c7ccfed22db941201b7b8f7b", size = 11935161, upload-time = "2025-08-21T18:22:26.965Z" }, - { url = "https://files.pythonhosted.org/packages/d1/b0/ad2464922a1113c365d12b8f80ed70fcfb39764288ac77c995156080488d/ruff-0.12.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ebb7333a45d56efc7c110a46a69a1b32365d5c5161e7244aaf3aa20ce62399c1", size = 12660884, upload-time = "2025-08-21T18:22:30.925Z" }, - { url = "https://files.pythonhosted.org/packages/d7/f1/97f509b4108d7bae16c48389f54f005b62ce86712120fd8b2d8e88a7cb49/ruff-0.12.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d59e58586829f8e4a9920788f6efba97a13d1fa320b047814e8afede381c6839", size = 11872754, upload-time = "2025-08-21T18:22:34.035Z" }, - { url = "https://files.pythonhosted.org/packages/12/ad/44f606d243f744a75adc432275217296095101f83f966842063d78eee2d3/ruff-0.12.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:822d9677b560f1fdeab69b89d1f444bf5459da4aa04e06e766cf0121771ab844", size = 12092276, upload-time = "2025-08-21T18:22:36.764Z" }, - { url = "https://files.pythonhosted.org/packages/06/1f/ed6c265e199568010197909b25c896d66e4ef2c5e1c3808caf461f6f3579/ruff-0.12.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:37b4a64f4062a50c75019c61c7017ff598cb444984b638511f48539d3a1c98db", size = 11734700, upload-time = "2025-08-21T18:22:39.822Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/39369e6ac7f2a1848f22fb0b00b690492f20811a1ac5c1fd1d2798329263/ruff-0.12.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:059e863ea3a9ade41407ad71c1de2badfbe01539117f38f763ba42a1206f7559", size = 14436642, upload-time = "2025-08-21T18:22:45.612Z" }, - { url = "https://files.pythonhosted.org/packages/e3/03/5da8cad4b0d5242a936eb203b58318016db44f5c5d351b07e3f5e211bb89/ruff-0.12.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bef6161e297c68908b7218fa6e0e93e99a286e5ed9653d4be71e687dff101cf", size = 13859107, upload-time = "2025-08-21T18:22:48.886Z" }, - { url = "https://files.pythonhosted.org/packages/19/19/dd7273b69bf7f93a070c9cec9494a94048325ad18fdcf50114f07e6bf417/ruff-0.12.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4f1345fbf8fb0531cd722285b5f15af49b2932742fc96b633e883da8d841896b", size = 12886521, upload-time = "2025-08-21T18:22:51.567Z" }, - { url = "https://files.pythonhosted.org/packages/c0/1d/b4207ec35e7babaee62c462769e77457e26eb853fbdc877af29417033333/ruff-0.12.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f68433c4fbc63efbfa3ba5db31727db229fa4e61000f452c540474b03de52a9", size = 13097528, upload-time = "2025-08-21T18:22:54.609Z" }, - { url = "https://files.pythonhosted.org/packages/ff/00/58f7b873b21114456e880b75176af3490d7a2836033779ca42f50de3b47a/ruff-0.12.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:141ce3d88803c625257b8a6debf4a0473eb6eed9643a6189b68838b43e78165a", size = 13080443, upload-time = "2025-08-21T18:22:57.413Z" }, - { url = "https://files.pythonhosted.org/packages/12/8c/9e6660007fb10189ccb78a02b41691288038e51e4788bf49b0a60f740604/ruff-0.12.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f3fc21178cd44c98142ae7590f42ddcb587b8e09a3b849cbc84edb62ee95de60", size = 11896759, upload-time = "2025-08-21T18:23:00.473Z" }, - { url = "https://files.pythonhosted.org/packages/67/4c/6d092bb99ea9ea6ebda817a0e7ad886f42a58b4501a7e27cd97371d0ba54/ruff-0.12.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7d1a4e0bdfafcd2e3e235ecf50bf0176f74dd37902f241588ae1f6c827a36c56", size = 11701463, upload-time = "2025-08-21T18:23:03.211Z" }, - { url = "https://files.pythonhosted.org/packages/ad/37/63a9c788bbe0b0850611669ec6b8589838faf2f4f959647f2d3e320383ae/ruff-0.12.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ae479e1a18b439c59138f066ae79cc0f3ee250712a873d00dbafadaad9481e5b", size = 13164356, upload-time = "2025-08-21T18:23:10.225Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, ] [[package]] @@ -759,11 +806,12 @@ wheels = [ [[package]] name = "splunk-soar-sdk" -version = "1.1.0" +version = "1.3.3" source = { registry = "https://pypi.python.org/simple" } dependencies = [ { name = "beautifulsoup4", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "bleach", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "build", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "click", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "distro", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "httpx", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, @@ -773,13 +821,14 @@ dependencies = [ { name = "pydantic", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "requests", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "responses", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "setuptools", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "toml", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "tqdm", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "typer", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d8/47/262353a2ab093f18ce7b5475814f3df7687800d205cb55c7a28001f0ea35/splunk_soar_sdk-1.1.0.tar.gz", hash = "sha256:20105071af535ce051ee536b849ef10a9934ccafcdb64ea9e5a1d6b3c6e2ed0f", size = 376008, upload-time = "2025-08-22T22:52:56.496Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/8f/a01e44b57f871d4830a2bc53e4d8c825e0ba72435ccbcd5e8bb668013317/splunk_soar_sdk-1.3.3.tar.gz", hash = "sha256:57f8e28c725b737eb91c574480f59207e4020b37273051991dea582da5c3076a", size = 508969, upload-time = "2025-09-08T03:48:32.871Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/e0/db0e833e31d77567eb253847b5b0c08a17bb44bd79ac9a49b88d6690581b/splunk_soar_sdk-1.1.0-py3-none-any.whl", hash = "sha256:f730236fbf3076ec755f14c56d585ded74718c205df6fef3b92b262650ef850b", size = 150002, upload-time = "2025-08-22T22:52:55.15Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d2/85ad8db77caae830e7fb5feec75a809de97fbd1f5eb28cdbad14f4da42d4/splunk_soar_sdk-1.3.3-py3-none-any.whl", hash = "sha256:4654db5e3b3c9dad4f1b7060921d605687bdc1c195b75d3af7ecdab007f20caf", size = 154969, upload-time = "2025-09-08T03:48:31.474Z" }, ] [[package]] @@ -829,7 +878,7 @@ wheels = [ [[package]] name = "typer" -version = "0.16.1" +version = "0.17.4" source = { registry = "https://pypi.python.org/simple" } dependencies = [ { name = "click", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, @@ -837,9 +886,9 @@ dependencies = [ { name = "shellingham", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "typing-extensions", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/78/d90f616bf5f88f8710ad067c1f8705bf7618059836ca084e5bb2a0855d75/typer-0.16.1.tar.gz", hash = "sha256:d358c65a464a7a90f338e3bb7ff0c74ac081449e53884b12ba658cbd72990614", size = 102836, upload-time = "2025-08-18T19:18:22.898Z" } +sdist = { url = "https://files.pythonhosted.org/packages/92/e8/2a73ccf9874ec4c7638f172efc8972ceab13a0e3480b389d6ed822f7a822/typer-0.17.4.tar.gz", hash = "sha256:b77dc07d849312fd2bb5e7f20a7af8985c7ec360c45b051ed5412f64d8dc1580", size = 103734, upload-time = "2025-09-05T18:14:40.746Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/76/06dbe78f39b2203d2a47d5facc5df5102d0561e2807396471b5f7c5a30a1/typer-0.16.1-py3-none-any.whl", hash = "sha256:90ee01cb02d9b8395ae21ee3368421faf21fa138cb2a541ed369c08cec5237c9", size = 46397, upload-time = "2025-08-18T19:18:21.663Z" }, + { url = "https://files.pythonhosted.org/packages/93/72/6b3e70d32e89a5cbb6a4513726c1ae8762165b027af569289e19ec08edd8/typer-0.17.4-py3-none-any.whl", hash = "sha256:015534a6edaa450e7007eba705d5c18c3349dcea50a6ad79a5ed530967575824", size = 46643, upload-time = "2025-09-05T18:14:39.166Z" }, ] [[package]] @@ -869,6 +918,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, ] +[[package]] +name = "validators" +version = "0.35.0" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/66/a435d9ae49850b2f071f7ebd8119dd4e84872b01630d6736761e6e7fd847/validators-0.35.0.tar.gz", hash = "sha256:992d6c48a4e77c81f1b4daba10d16c3a9bb0dbb79b3a19ea847ff0928e70497a", size = 73399, upload-time = "2025-05-01T05:42:06.7Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/6e/3e955517e22cbdd565f2f8b2e73d52528b14b8bcfdb04f62466b071de847/validators-0.35.0-py3-none-any.whl", hash = "sha256:e8c947097eae7892cb3d26868d637f79f47b4a0554bc6b80065dfe5aac3705dd", size = 44712, upload-time = "2025-05-01T05:42:04.203Z" }, +] + [[package]] name = "virtualenv" version = "20.34.0" @@ -928,9 +986,18 @@ wheels = [ [[package]] name = "xmltodict" -version = "0.13.0" +version = "0.15.0" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/ee/b30fdb281b39da57053bd7012870989de6f066d6ef1476d78de8fc427324/xmltodict-0.15.0.tar.gz", hash = "sha256:c6d46b4e3413d1e4fc3e5016f0f1c7a5c10f8ce39efaa0cb099af986ecfc9a53", size = 60285, upload-time = "2025-09-05T00:35:45.947Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/56/507a207b96e3aa7365c28bb6702011e7c76c899c1737966b25852eaef3e8/xmltodict-0.15.0-py2.py3-none-any.whl", hash = "sha256:8887783bf1faba1754fc45fdf3fe03fbb3629c811ae57f91c018aace4c58d4ed", size = 10965, upload-time = "2025-09-05T00:35:44.583Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" source = { registry = "https://pypi.python.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/39/0d/40df5be1e684bbaecdb9d1e0e40d5d482465de6b00cbb92b84ee5d243c7f/xmltodict-0.13.0.tar.gz", hash = "sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56", size = 33813, upload-time = "2022-05-08T07:00:04.916Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/db/fd0326e331726f07ff7f40675cd86aa804bfd2e5016c727fa761c934990e/xmltodict-0.13.0-py2.py3-none-any.whl", hash = "sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852", size = 9971, upload-time = "2022-05-08T07:00:02.898Z" }, + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, ] From 12db9d564809894a01726ed5612c09755571c385 Mon Sep 17 00:00:00 2001 From: msankows Date: Wed, 10 Sep 2025 11:16:02 +0200 Subject: [PATCH 17/29] fix: code refactoring --- package/src/actions/get_file.py | 23 +++++++++++------------ package/src/actions/put_file.py | 12 +++++------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/package/src/actions/get_file.py b/package/src/actions/get_file.py index ee6c5fb..6c551f2 100644 --- a/package/src/actions/get_file.py +++ b/package/src/actions/get_file.py @@ -15,9 +15,10 @@ class GetFileParams(Params): hostname: str = Param( description="Hostname to execute command on", primary=True, - cef_types=["host name"], + cef_types=["hostname"], ) file_path: str = Param( + required=True, description="Path of the file to download (include filename)", primary=True, cef_types=["file path"], @@ -38,8 +39,6 @@ class GetFileOutput(ActionOutput): def get_file(params: GetFileParams, soar: SOARClient, asset: Asset) -> GetFileOutput: hostname = params.hostname.strip(" ").strip("/") or asset.base_url file_path = params.file_path.strip() - if not file_path: - raise ActionFailure("Parameter 'file_path' cannot be empty.") encoded_file_path = quote(file_path) validate_url = f"{hostname}/{encoded_file_path}" @@ -69,16 +68,16 @@ def get_file(params: GetFileParams, soar: SOARClient, asset: Asset) -> GetFileOu if not file_content: raise ActionFailure("Downloaded file is empty.") - vault_add_result = soar.vault.create_attachment( - file_content=file_content, file_name=file_name, container_id=soar.get_container_id, metadata=None - ) + container_id_to_use = soar.get_container_id() - if not vault_add_result.success: - raise ActionFailure(f"Failed to add file to vault. Reason: {vault_add_result.message}") + if not ( + new_vault_id := soar.vault.create_attachment( + file_content=file_content, file_name=file_name, container_id=container_id_to_use, metadata=None + ) + ): + raise ActionFailure("Failed to add file to vault.") - return GetFileOutput( - vault_id=vault_add_result.vault_id, - file_name=file_name, - ) except Exception as e: raise ActionFailure(f"An error occurred while saving the file to the vault. Details: {e}") + + return GetFileOutput(vault_id=new_vault_id, file_name=file_name) diff --git a/package/src/actions/put_file.py b/package/src/actions/put_file.py index 6910c4d..c7493e1 100644 --- a/package/src/actions/put_file.py +++ b/package/src/actions/put_file.py @@ -38,16 +38,14 @@ class PutFileOutput(ActionOutput): def put_file(params: PutFileParams, soar: SOARClient, asset: Asset) -> PutFileOutput: try: - attachments = soar.vault.get_attachment(vault_id=params.vault_id) - if not attachments: + if not (attachments := soar.vault.get_attachment(vault_id=params.vault_id)): raise ActionFailure(f"File with vault_id '{params.vault_id}' not found in vault.") vault_attachment = attachments[0] file_name_to_send = params.file_name or vault_attachment.name - if params.file_name: - if vault_attachment.name != params.file_name: - logger.warning( - f"Provided file_name '{params.file_name}' does not match the name in vault '{vault_attachment.name}'. Using provided name." - ) + if params.file_name and vault_attachment.name != params.file_name: + logger.warning( + f"Provided file_name '{params.file_name}' does not match the name in vault '{vault_attachment.name}'. Using provided name." + ) with vault_attachment.open() as f: base_url = params.host or asset.base_url full_url = f"{base_url.rstrip('/')}/{params.file_destination.lstrip('/')}/{quote(file_name_to_send)}" From 58e74538c9deccd50a6ba199bb088e5510f46c1e Mon Sep 17 00:00:00 2001 From: msankows Date: Thu, 11 Sep 2025 11:07:15 +0200 Subject: [PATCH 18/29] fix: body and verify_certificate not required - all tests passed --- package/src/actions/action_delete.py | 2 +- package/src/actions/action_patch.py | 2 +- package/src/actions/action_put.py | 2 +- package/src/actions/get_file.py | 2 +- package/src/actions/put_file.py | 2 +- package/src/app.py | 2 -- package/src/classes.py | 2 +- 7 files changed, 6 insertions(+), 8 deletions(-) diff --git a/package/src/actions/action_delete.py b/package/src/actions/action_delete.py index 76a9d2f..422c3b6 100644 --- a/package/src/actions/action_delete.py +++ b/package/src/actions/action_delete.py @@ -15,7 +15,7 @@ class DeleteDataOutput(BaseHttpOutput): class DeleteDataParams(BaseHttpParams): - body: str = Param(description="DELETE body (query string, JSON, etc.)") + body: str = Param(description="DELETE body (query string, JSON, etc.)", required=False) def delete_data(params: DeleteDataParams, soar: SOARClient, asset: Asset) -> DeleteDataOutput: diff --git a/package/src/actions/action_patch.py b/package/src/actions/action_patch.py index b516d12..cc4a2d1 100644 --- a/package/src/actions/action_patch.py +++ b/package/src/actions/action_patch.py @@ -11,7 +11,7 @@ class PatchDataParams(BaseHttpParams): - body: str = Param(description="PATCH body (query string, JSON, etc.)") + body: str = Param(description="PATCH body (query string, JSON, etc.)", required=False) class PatchDataOutput(BaseHttpOutput): diff --git a/package/src/actions/action_put.py b/package/src/actions/action_put.py index a43df19..bdff05b 100644 --- a/package/src/actions/action_put.py +++ b/package/src/actions/action_put.py @@ -15,7 +15,7 @@ class PutDataOutput(BaseHttpOutput): class PutDataParams(BaseHttpParams): - body: str = Param(description="PATCH body (query string, JSON, etc.)") + body: str = Param(description="PATCH body (query string, JSON, etc.)", required=False) def put_data(params: PutDataParams, soar: SOARClient, asset: Asset) -> PutDataOutput: diff --git a/package/src/actions/get_file.py b/package/src/actions/get_file.py index 6c551f2..ff1bd16 100644 --- a/package/src/actions/get_file.py +++ b/package/src/actions/get_file.py @@ -23,7 +23,7 @@ class GetFileParams(Params): primary=True, cef_types=["file path"], ) - verify_certificate: bool = Param(description="Verify certificates (if using HTTPS)") + verify_certificate: bool = Param(description="Verify certificates (if using HTTPS)", default=False) class GetFileOutput(ActionOutput): diff --git a/package/src/actions/put_file.py b/package/src/actions/put_file.py index c7493e1..faf1193 100644 --- a/package/src/actions/put_file.py +++ b/package/src/actions/put_file.py @@ -24,7 +24,7 @@ class PutFileParams(Params): cef_types=["file path"], ) file_name: str = Param(description="Name of the file to be put on endpoint") - verify_certificate: bool = Param(description="Verify certificates (if using HTTPS)") + verify_certificate: bool = Param(description="Verify certificates (if using HTTPS)", default=False) class PutFileOutput(ActionOutput): diff --git a/package/src/app.py b/package/src/app.py index 4babd4f..e63c84c 100644 --- a/package/src/app.py +++ b/package/src/app.py @@ -22,8 +22,6 @@ class EmptyOutput(ActionOutput): - """An empty output class for actions that do not return data, like test_connectivity.""" - pass diff --git a/package/src/classes.py b/package/src/classes.py index def4fec..eb6c4be 100644 --- a/package/src/classes.py +++ b/package/src/classes.py @@ -26,5 +26,5 @@ class BaseHttpParams(Params): primary=True, cef_types=["endpoint"], ) - verify_certificate: bool = Param(description="Verify certificates (if using HTTPS)") + verify_certificate: bool = Param(description="Verify certificates (if using HTTPS)", default=False, required=False) headers: str = Param(description="Additional headers (JSON object with headers)", required=False) From 516b996d37cf8ed4ad2d69b7666287208c95679d Mon Sep 17 00:00:00 2001 From: msankows Date: Thu, 11 Sep 2025 14:30:26 +0200 Subject: [PATCH 19/29] fix: added descriptions --- package/src/actions/action_delete.py | 12 ++++-- package/src/actions/action_get.py | 12 ++++-- package/src/actions/action_head.py | 12 ++++-- package/src/actions/action_options.py | 12 ++++-- package/src/actions/action_patch.py | 10 ++++- package/src/actions/action_post.py | 12 ++++-- package/src/actions/action_put.py | 10 ++++- package/src/actions/get_file.py | 7 ++- package/src/actions/put_file.py | 7 ++- package/src/app.py | 30 ++++++------- package/src/auth.py | 61 +++++++++++++++++++++++++++ package/src/classes.py | 11 +++++ package/src/helpers.py | 9 ++++ package/src/request_maker.py | 6 +++ 14 files changed, 172 insertions(+), 39 deletions(-) diff --git a/package/src/actions/action_delete.py b/package/src/actions/action_delete.py index 422c3b6..b25052f 100644 --- a/package/src/actions/action_delete.py +++ b/package/src/actions/action_delete.py @@ -6,19 +6,25 @@ from ..common import logger from ..request_maker import make_request -action_description = "Perform a REST DELETE call to the server" -action_type = "generic" - class DeleteDataOutput(BaseHttpOutput): + """ + Defines the structured output for the 'DELETE Request' action. + """ + pass class DeleteDataParams(BaseHttpParams): + """ + Defines the input parameters for the 'DELETE Request' action. + """ + body: str = Param(description="DELETE body (query string, JSON, etc.)", required=False) def delete_data(params: DeleteDataParams, soar: SOARClient, asset: Asset) -> DeleteDataOutput: + """Perform a REST DELETE call to the server.""" logger.info("In action handler for: delete_data") return make_request( asset=asset, diff --git a/package/src/actions/action_get.py b/package/src/actions/action_get.py index eca23e1..d023df7 100644 --- a/package/src/actions/action_get.py +++ b/package/src/actions/action_get.py @@ -18,19 +18,25 @@ from ..common import logger from ..request_maker import make_request -action_type = "investigate" -action_description = "This App facilitates making HTTP requests as actions" - class GetDataOutput(BaseHttpOutput): + """ + Defines the structured output for the 'GET Request' action. + """ + pass class GetDataParams(BaseHttpParams): + """ + Defines the input parameters for the 'GET Request' action. + """ + pass def get_data(params: GetDataParams, soar: SOARClient, asset: Asset) -> GetDataOutput: + """Perform a REST GET call to the server.""" logger.info("In action handler for: get_data") return make_request( asset=asset, diff --git a/package/src/actions/action_head.py b/package/src/actions/action_head.py index 315ecfb..1245d4b 100644 --- a/package/src/actions/action_head.py +++ b/package/src/actions/action_head.py @@ -5,19 +5,25 @@ from ..common import logger from ..request_maker import make_request -action_description = "Perform a REST HEAD call to the server" -action_type = "investigate" - class GetHeadersOutput(BaseHttpOutput): + """ + Defines the structured output for the 'HEAD Request' action. + """ + pass class GetHeadersParams(BaseHttpParams): + """ + Defines the input parameters for the 'HEAD Request' action. + """ + pass def get_headers(params: GetHeadersParams, soar: SOARClient, asset: Asset) -> GetHeadersOutput: + """Perform a REST HEAD call to the server.""" logger.info("In action handler for: head_data") return make_request( asset=asset, diff --git a/package/src/actions/action_options.py b/package/src/actions/action_options.py index ff6b508..47d9af6 100644 --- a/package/src/actions/action_options.py +++ b/package/src/actions/action_options.py @@ -5,19 +5,25 @@ from ..common import logger from ..request_maker import make_request -action_description = "Perform a REST OPTIONS call to the server" -action_type = "investigate" - class GetOptionsParams(BaseHttpParams): + """ + Defines the input parameters for the 'OPTIONS Request' action. + """ + pass class GetOptionsOutput(BaseHttpOutput): + """ + Defines the structured output for the 'OPTIONS Request' action. + """ + pass def get_options(params: GetOptionsParams, soar: SOARClient, asset: Asset) -> GetOptionsOutput: + """Perform a REST OPTIONS call to the server.""" logger.info("In action handler for: get_options") return make_request( asset=asset, diff --git a/package/src/actions/action_patch.py b/package/src/actions/action_patch.py index cc4a2d1..608dad5 100644 --- a/package/src/actions/action_patch.py +++ b/package/src/actions/action_patch.py @@ -6,19 +6,27 @@ from ..common import logger from ..request_maker import make_request -action_description = "Perform a REST PATCH call to the server" action_type = "generic" class PatchDataParams(BaseHttpParams): + """ + Defines the input parameters for the 'PATCH Request' action. + """ + body: str = Param(description="PATCH body (query string, JSON, etc.)", required=False) class PatchDataOutput(BaseHttpOutput): + """ + Defines the structured output for the 'PATCH Request' action. + """ + pass def patch_data(params: PatchDataParams, soar: SOARClient, asset: Asset) -> PatchDataOutput: + """Perform a REST PATCH call to the server.""" logger.info("In action handler for: patch_data") return make_request( asset=asset, diff --git a/package/src/actions/action_post.py b/package/src/actions/action_post.py index d9e47fa..796b781 100644 --- a/package/src/actions/action_post.py +++ b/package/src/actions/action_post.py @@ -6,19 +6,25 @@ from ..common import logger from ..request_maker import make_request -action_type = "generic" -action_description = "Perform a REST POST call to the server" - class PostDataOutput(BaseHttpOutput): + """ + Defines the structured output for the 'POST Request' action. + """ + pass class PostDataParams(BaseHttpParams): + """ + Defines the input parameters for the 'POST Request' action. + """ + body: str = Param(description="POST body (query string, JSON, etc.)", required=False) def post_data(params: PostDataParams, soar: SOARClient, asset: Asset) -> PostDataOutput: + """Perform a REST POST call to the server.""" logger.info("In action handler for: http_post") return make_request( asset=asset, diff --git a/package/src/actions/action_put.py b/package/src/actions/action_put.py index bdff05b..b6c0518 100644 --- a/package/src/actions/action_put.py +++ b/package/src/actions/action_put.py @@ -6,19 +6,27 @@ from ..common import logger from ..request_maker import make_request -action_description = "Perform a REST PUT call to the server" action_type = "generic" class PutDataOutput(BaseHttpOutput): + """ + Defines the structured output for the 'PUT Request' action. + """ + pass class PutDataParams(BaseHttpParams): + """ + Defines the input parameters for the 'PUT Request' action. + """ + body: str = Param(description="PATCH body (query string, JSON, etc.)", required=False) def put_data(params: PutDataParams, soar: SOARClient, asset: Asset) -> PutDataOutput: + """Perform a REST PUT call to the server.""" logger.info("In action handler for: put_data") return make_request( asset=asset, diff --git a/package/src/actions/get_file.py b/package/src/actions/get_file.py index ff1bd16..9d8f3b5 100644 --- a/package/src/actions/get_file.py +++ b/package/src/actions/get_file.py @@ -12,6 +12,8 @@ class GetFileParams(Params): + """Defines the input parameters for the 'Get File' action.""" + hostname: str = Param( description="Hostname to execute command on", primary=True, @@ -27,16 +29,17 @@ class GetFileParams(Params): class GetFileOutput(ActionOutput): + """Defines the structured output for the 'Get File' action.""" + vault_id: str file_name: str -action_description = "Retrieve a file from the endpoint and save it to the vault" -action_type = "investigate" verbose = "Provide the file path and file name to download into the vault. For example, /web_storage/file.tgz." def get_file(params: GetFileParams, soar: SOARClient, asset: Asset) -> GetFileOutput: + """Retrieve a file from the endpoint and save it to the vault.""" hostname = params.hostname.strip(" ").strip("/") or asset.base_url file_path = params.file_path.strip() encoded_file_path = quote(file_path) diff --git a/package/src/actions/put_file.py b/package/src/actions/put_file.py index faf1193..e50a20c 100644 --- a/package/src/actions/put_file.py +++ b/package/src/actions/put_file.py @@ -12,6 +12,8 @@ class PutFileParams(Params): + """Defines the input parameters for the 'Put File' action.""" + host: str = Param( description="Hostname/IP with port number to execute command on", primary=True, @@ -28,15 +30,16 @@ class PutFileParams(Params): class PutFileOutput(ActionOutput): + """Defines the structured output for the 'Put File' action.""" + file_sent: str -action_description = "Put a file from the vault to another location" -action_type = "generic" verbose = "Provide the path to store the file on the file server. For example, /web_storage/test_repo/." def put_file(params: PutFileParams, soar: SOARClient, asset: Asset) -> PutFileOutput: + """Put a file from the vault to another location.""" try: if not (attachments := soar.vault.get_attachment(vault_id=params.vault_id)): raise ActionFailure(f"File with vault_id '{params.vault_id}' not found in vault.") diff --git a/package/src/app.py b/package/src/app.py index e63c84c..e68e5f2 100644 --- a/package/src/app.py +++ b/package/src/app.py @@ -1,9 +1,9 @@ from soar_sdk.abstract import SOARClient -from soar_sdk.action_results import ActionOutput from soar_sdk.app import App from .actions import action_delete, action_get, action_head, action_options, action_patch, action_post, action_put, get_file, put_file from .asset import Asset +from .classes import EmptyOutput from .common import logger from .request_maker import make_request @@ -21,14 +21,10 @@ ) -class EmptyOutput(ActionOutput): - pass - - @app.test_connectivity() def test_connectivity(soar: SOARClient, asset: Asset) -> None: - - logger.info("Action 'Test Connectivity' started.") + """Validate connection using the configured credentials.""" + logger.info("In action handler for: test_connectivity") make_request( asset=asset, @@ -44,17 +40,15 @@ def test_connectivity(soar: SOARClient, asset: Asset) -> None: logger.info("Test connectivity passed!") -app.register_action(action_get.get_data, action_type=action_get.action_type, description=action_get.action_description) -app.register_action(action_post.post_data, action_type=action_post.action_type, description=action_post.action_description, read_only=False) -app.register_action(action_put.put_data, action_type=action_put.action_type, description=action_put.action_description, read_only=False) -app.register_action(action_patch.patch_data, action_type=action_patch.action_type, description=action_patch.action_description) -app.register_action(action_delete.delete_data, action_type=action_delete.action_type, description=action_delete.action_description) -app.register_action(action_head.get_headers, action_type=action_head.action_type, description=action_head.action_description) -app.register_action(action_options.get_options, action_type=action_options.action_type, description=action_options.action_description) -app.register_action( - put_file.put_file, action_type=put_file.action_type, description=put_file.action_description, read_only=False, verbose=put_file.verbose -) -app.register_action(get_file.get_file, action_type=get_file.action_type, description=get_file.action_description, verbose=get_file.verbose) +app.register_action(action_get.get_data, action_type="investigate") +app.register_action(action_post.post_data, action_type="generic", read_only=False) +app.register_action(action_put.put_data, action_type="generic", read_only=False) +app.register_action(action_patch.patch_data, action_type="generic", read_only=False) +app.register_action(action_delete.delete_data, action_type="generic", read_only=False) +app.register_action(action_head.get_headers, action_type="investigate") +app.register_action(action_options.get_options, action_type="investigate") +app.register_action(put_file.put_file, action_type="generic", read_only=False, verbose=put_file.verbose) +app.register_action(get_file.get_file, action_type="investigate", verbose=get_file.verbose) if __name__ == "__main__": diff --git a/package/src/auth.py b/package/src/auth.py index e95e527..762dd29 100644 --- a/package/src/auth.py +++ b/package/src/auth.py @@ -21,12 +21,34 @@ class Authorization(ABC): + """ + Abstract base class for defining an authentication strategy. + + Each strategy must implement the `create_auth` method, which is responsible + for preparing the necessary authentication objects and headers for a request. + """ + @abstractmethod def create_auth(self, headers) -> Tuple[Optional[AuthBase], dict]: + """ + Prepares authentication components for an HTTP request. + + Args: + headers (dict): The initial dictionary of headers for the request. + + Returns: + Tuple[Optional[AuthBase], dict]: A tuple containing: + - An optional `requests.auth.AuthBase` object. + - The updated headers dictionary. + """ pass class BasicAuth(Authorization): + """ + Implements HTTP Basic Authentication using username and password. + """ + def __init__(self, asset): self.username = asset.username self.password = asset.password @@ -36,6 +58,10 @@ def create_auth(self, headers): class TokenAuth(Authorization): + """ + Implements authentication using a static token in a specified header. + """ + def __init__(self, asset): self.auth_token_name = asset.auth_token_name self.auth_token = asset.auth_token @@ -47,12 +73,22 @@ def create_auth(self, headers): class OAuth(Authorization): + """ + Implements OAuth 2.0 Client Credentials Grant Flow. + + This strategy fetches an access token from a token URL, caches it in the + app's authentication state, and adds it to the request as a Bearer token. + """ + def __init__(self, asset, soar_client): self.asset = asset self.soar = soar_client self.state_key = f"oauth_token_{asset.asset_id}" def _generate_new_token(self): + """ + Fetches a new OAuth access token and saves it to the app's auth_state. + """ token_url = self.asset.oauth_token_url client_id = self.asset.client_id client_secret = self.asset.client_secret @@ -83,6 +119,13 @@ def _generate_new_token(self): return access_token def get_token(self, force_new: bool = False) -> str: + """ + Retrieves a token, either from the cached state or by fetching a new one. + + Args: + force_new (bool): If True, forces a new token to be fetched, + ignoring any cached token. + """ cached_token = self.soar.auth_state.get(self.state_key) if cached_token and not force_new: @@ -99,6 +142,10 @@ def create_auth(self, headers: dict) -> tuple[None, dict]: class NoAuth(Authorization): + """ + Represents an anonymous request with no authentication. + """ + def __init__(self, asset): pass @@ -107,6 +154,20 @@ def create_auth(self, headers): def get_auth_method(asset, soar_client): + """ + Factory function to select and instantiate the appropriate auth strategy. + + Based on the provided asset configuration, this function determines which + authentication method to use (Basic, Token, OAuth, or None) and returns + an instance of the corresponding strategy class. + + Args: + asset (Asset): The asset configuration object. + soar_client (SOARClient): The SOAR client, needed for stateful strategies like OAuth. + + Returns: + Authorization: An instance of a class that implements the Authorization strategy. + """ if asset.username and asset.password: return BasicAuth(asset) elif asset.auth_token_name and asset.auth_token: diff --git a/package/src/classes.py b/package/src/classes.py index eb6c4be..14fc6c4 100644 --- a/package/src/classes.py +++ b/package/src/classes.py @@ -3,13 +3,22 @@ from soar_sdk.params import Param, Params +class EmptyOutput(ActionOutput): + """Empty output for test_connectivity.""" + + pass + + class ParsedResponseBody(ActionOutput): + """A flexible model for any JSON response.""" class Config: extra = Extra.allow class BaseHttpOutput(GenericActionOutput): + """Base class for all standard action outputs.""" + location: str = OutputField(cef_types=["url"], example_values=["http://192.168.1.26/rest/assets"]) method: str = OutputField(example_values=["POST"]) parsed_response_body: ParsedResponseBody = OutputField(example_values=['{"failed": true, "message": "Requested item not found"}']) @@ -21,6 +30,8 @@ def generate_action_summary_message(self) -> str: class BaseHttpParams(Params): + """Base class for all standard action parameters.""" + location: str = Param( description="Location (e.g. path/to/endpoint)", primary=True, diff --git a/package/src/helpers.py b/package/src/helpers.py index fc929da..4b97a91 100644 --- a/package/src/helpers.py +++ b/package/src/helpers.py @@ -59,6 +59,11 @@ def process_text_response(response) -> ParsedResponseBody: def parse_headers(headers_str: Optional[str]) -> dict: + """ + Parses a JSON string of headers into a dictionary. + Returns an empty dictionary if the input is empty or None. + Raises ActionFailure on parsing or validation errors. + """ if headers_str is None: return {} @@ -77,6 +82,10 @@ def parse_headers(headers_str: Optional[str]) -> dict: def handle_various_response(response): + """ + Analyzes the response, routes it to the correct parser, and returns both + the parsed body and a raw text body for the final output. + """ content_type = response.headers.get("Content-Type", "").lower() if not response.text.strip() or ("application/octet-stream" in content_type): return process_empty_response(content_type), "" diff --git a/package/src/request_maker.py b/package/src/request_maker.py index 1368898..9293e79 100644 --- a/package/src/request_maker.py +++ b/package/src/request_maker.py @@ -22,6 +22,12 @@ def make_request( headers: Optional[str], body: Optional[str], ) -> ActionOutput: + """ + The central engine for making all HTTP requests. + + This function handles parameter parsing, URL construction, authentication, + request execution (with OAuth token refresh logic), and response processing. + """ logger.info(f"Preparing to make {method} http request.") parsed_headers = helpers.parse_headers(headers) From c3835d587e5a6b010d23bb82ad772eb3c9af30a8 Mon Sep 17 00:00:00 2001 From: msankows Date: Thu, 11 Sep 2025 14:34:04 +0200 Subject: [PATCH 20/29] fix: app description --- package/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/pyproject.toml b/package/pyproject.toml index 21d1db0..83e1646 100644 --- a/package/pyproject.toml +++ b/package/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "HTTP" version = "4.0.0" -description = "app" +description = "This App facilitates making HTTP requests as actions" license = "Copyright (c) 2025 Splunk Inc." requires-python = ">=3.9, <3.14" authors = [ From af2245d61fb8e3ad717f623501ec8f4ec004c6a8 Mon Sep 17 00:00:00 2001 From: msankows Date: Mon, 15 Sep 2025 14:56:27 +0200 Subject: [PATCH 21/29] logs added --- package/src/actions/get_file.py | 6 +++++- package/src/actions/put_file.py | 12 +++++++++--- package/src/auth.py | 12 +++++++++++- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/package/src/actions/get_file.py b/package/src/actions/get_file.py index 9d8f3b5..b9b510a 100644 --- a/package/src/actions/get_file.py +++ b/package/src/actions/get_file.py @@ -9,6 +9,7 @@ from ..asset import Asset from ..auth import get_auth_method +from ..common import logger class GetFileParams(Params): @@ -40,6 +41,8 @@ class GetFileOutput(ActionOutput): def get_file(params: GetFileParams, soar: SOARClient, asset: Asset) -> GetFileOutput: """Retrieve a file from the endpoint and save it to the vault.""" + logger.info("In action handler for: get_file") + hostname = params.hostname.strip(" ").strip("/") or asset.base_url file_path = params.file_path.strip() encoded_file_path = quote(file_path) @@ -79,8 +82,9 @@ def get_file(params: GetFileParams, soar: SOARClient, asset: Asset) -> GetFileOu ) ): raise ActionFailure("Failed to add file to vault.") - + logger.info(f"Saving file to vault. name: {file_name}") except Exception as e: raise ActionFailure(f"An error occurred while saving the file to the vault. Details: {e}") + logger.info("File successfully retrieved and added to vault") return GetFileOutput(vault_id=new_vault_id, file_name=file_name) diff --git a/package/src/actions/put_file.py b/package/src/actions/put_file.py index e50a20c..4a9baac 100644 --- a/package/src/actions/put_file.py +++ b/package/src/actions/put_file.py @@ -1,6 +1,7 @@ from urllib.parse import quote import requests +import validators from soar_sdk.abstract import SOARClient from soar_sdk.action_results import ActionOutput from soar_sdk.exceptions import ActionFailure @@ -40,7 +41,9 @@ class PutFileOutput(ActionOutput): def put_file(params: PutFileParams, soar: SOARClient, asset: Asset) -> PutFileOutput: """Put a file from the vault to another location.""" + logger.info("In action handler for: put_file") try: + logger.info(f"Fetching phantom vault details for vault_id: {params.vault_id}") if not (attachments := soar.vault.get_attachment(vault_id=params.vault_id)): raise ActionFailure(f"File with vault_id '{params.vault_id}' not found in vault.") vault_attachment = attachments[0] @@ -49,14 +52,18 @@ def put_file(params: PutFileParams, soar: SOARClient, asset: Asset) -> PutFileOu logger.warning( f"Provided file_name '{params.file_name}' does not match the name in vault '{vault_attachment.name}'. Using provided name." ) + if file_name_to_send in params.file_destination: + raise ActionFailure("The filename should be excluded from the 'location' (file destination) parameter.") with vault_attachment.open() as f: base_url = params.host or asset.base_url full_url = f"{base_url.rstrip('/')}/{params.file_destination.lstrip('/')}/{quote(file_name_to_send)}" + if not validators.url(full_url): + raise ActionFailure(f"Invalid URL constructed: {full_url}") auth_strategy = get_auth_method(asset, soar) auth_object, final_headers = auth_strategy.create_auth({}) files_payload = {"file": f} query_params = {"file_path": params.file_destination} - + logger.info(f"Uploading file '{file_name_to_send}' to: {full_url}") response = requests.post( uri=full_url, auth=auth_object, @@ -67,12 +74,11 @@ def put_file(params: PutFileParams, soar: SOARClient, asset: Asset) -> PutFileOu timeout=asset.timeout, ) response.raise_for_status() - except requests.exceptions.RequestException as e: raise ActionFailure(f"Failed to upload file to {full_url}. Details: {e}") except Exception as e: raise ActionFailure(f"An unexpected error occurred. Details: {e}") - + logger.info(f"File successfully uploaded. Server status: {response.status_code}") return PutFileOutput( file_sent=full_url, ) diff --git a/package/src/auth.py b/package/src/auth.py index 762dd29..25e0ef7 100644 --- a/package/src/auth.py +++ b/package/src/auth.py @@ -19,6 +19,8 @@ from requests.auth import AuthBase, HTTPBasicAuth from soar_sdk.exceptions import ActionFailure +from .common import logger + class Authorization(ABC): """ @@ -54,6 +56,7 @@ def __init__(self, asset): self.password = asset.password def create_auth(self, headers): + logger.info("Using HTTP Basic auth to authenticate") return (self.username, self.password), headers @@ -67,6 +70,7 @@ def __init__(self, asset): self.auth_token = asset.auth_token def create_auth(self, headers): + logger.info("Using provided token to authenticate") if self.auth_token and self.auth_token_name not in headers: headers[self.auth_token_name] = self.auth_token return None, headers @@ -89,6 +93,7 @@ def _generate_new_token(self): """ Fetches a new OAuth access token and saves it to the app's auth_state. """ + logger.info("Fetching new token") token_url = self.asset.oauth_token_url client_id = self.asset.client_id client_secret = self.asset.client_secret @@ -115,7 +120,7 @@ def _generate_new_token(self): raise ActionFailure("Access token not found in response body") self.soar.auth_state[self.state_key] = access_token - + logger.info("Successfully fetched and saved new OAuth token to auth_state.") return access_token def get_token(self, force_new: bool = False) -> str: @@ -126,14 +131,18 @@ def get_token(self, force_new: bool = False) -> str: force_new (bool): If True, forces a new token to be fetched, ignoring any cached token. """ + logger.info("Fetching access token") cached_token = self.soar.auth_state.get(self.state_key) if cached_token and not force_new: + logger.info("Using old token") return cached_token return self._generate_new_token() def create_auth(self, headers: dict) -> tuple[None, dict]: + logger.info("Using OAuth to authenticate") + access_token = self.__get_token() headers["Authorization"] = f"Bearer {access_token}" @@ -150,6 +159,7 @@ def __init__(self, asset): pass def create_auth(self, headers): + logger.info("No authentication method configured. Making an anonymous request.") return None, headers From 533d7a792bd5f9d537111ecc6d5a68bb3f3f146b Mon Sep 17 00:00:00 2001 From: msankows Date: Tue, 16 Sep 2025 12:00:51 +0200 Subject: [PATCH 22/29] small changes --- package/src/actions/action_delete.py | 8 +------- package/src/actions/action_get.py | 8 +------- package/src/actions/action_head.py | 8 +------- package/src/actions/action_options.py | 8 +------- package/src/actions/action_patch.py | 8 +------- package/src/actions/action_post.py | 8 +------- package/src/actions/action_put.py | 8 +------- package/src/actions/get_file.py | 7 ++----- package/src/actions/put_file.py | 17 ++++++++--------- package/src/app.py | 6 +++--- package/src/auth.py | 11 +++-------- package/src/helpers.py | 2 +- package/src/{classes.py => schemas.py} | 0 13 files changed, 24 insertions(+), 75 deletions(-) rename package/src/{classes.py => schemas.py} (100%) diff --git a/package/src/actions/action_delete.py b/package/src/actions/action_delete.py index b25052f..96b292f 100644 --- a/package/src/actions/action_delete.py +++ b/package/src/actions/action_delete.py @@ -2,23 +2,17 @@ from soar_sdk.params import Param from ..asset import Asset -from ..classes import BaseHttpOutput, BaseHttpParams from ..common import logger from ..request_maker import make_request +from ..schemas import BaseHttpOutput, BaseHttpParams class DeleteDataOutput(BaseHttpOutput): - """ - Defines the structured output for the 'DELETE Request' action. - """ pass class DeleteDataParams(BaseHttpParams): - """ - Defines the input parameters for the 'DELETE Request' action. - """ body: str = Param(description="DELETE body (query string, JSON, etc.)", required=False) diff --git a/package/src/actions/action_get.py b/package/src/actions/action_get.py index d023df7..df471e1 100644 --- a/package/src/actions/action_get.py +++ b/package/src/actions/action_get.py @@ -14,23 +14,17 @@ from soar_sdk.abstract import SOARClient from ..asset import Asset -from ..classes import BaseHttpOutput, BaseHttpParams from ..common import logger from ..request_maker import make_request +from ..schemas import BaseHttpOutput, BaseHttpParams class GetDataOutput(BaseHttpOutput): - """ - Defines the structured output for the 'GET Request' action. - """ pass class GetDataParams(BaseHttpParams): - """ - Defines the input parameters for the 'GET Request' action. - """ pass diff --git a/package/src/actions/action_head.py b/package/src/actions/action_head.py index 1245d4b..d242671 100644 --- a/package/src/actions/action_head.py +++ b/package/src/actions/action_head.py @@ -1,23 +1,17 @@ from soar_sdk.abstract import SOARClient from ..asset import Asset -from ..classes import BaseHttpOutput, BaseHttpParams from ..common import logger from ..request_maker import make_request +from ..schemas import BaseHttpOutput, BaseHttpParams class GetHeadersOutput(BaseHttpOutput): - """ - Defines the structured output for the 'HEAD Request' action. - """ pass class GetHeadersParams(BaseHttpParams): - """ - Defines the input parameters for the 'HEAD Request' action. - """ pass diff --git a/package/src/actions/action_options.py b/package/src/actions/action_options.py index 47d9af6..8e82f1f 100644 --- a/package/src/actions/action_options.py +++ b/package/src/actions/action_options.py @@ -1,23 +1,17 @@ from soar_sdk.abstract import SOARClient from ..asset import Asset -from ..classes import BaseHttpOutput, BaseHttpParams from ..common import logger from ..request_maker import make_request +from ..schemas import BaseHttpOutput, BaseHttpParams class GetOptionsParams(BaseHttpParams): - """ - Defines the input parameters for the 'OPTIONS Request' action. - """ pass class GetOptionsOutput(BaseHttpOutput): - """ - Defines the structured output for the 'OPTIONS Request' action. - """ pass diff --git a/package/src/actions/action_patch.py b/package/src/actions/action_patch.py index 608dad5..3acc383 100644 --- a/package/src/actions/action_patch.py +++ b/package/src/actions/action_patch.py @@ -2,25 +2,19 @@ from soar_sdk.params import Param from ..asset import Asset -from ..classes import BaseHttpOutput, BaseHttpParams from ..common import logger from ..request_maker import make_request +from ..schemas import BaseHttpOutput, BaseHttpParams action_type = "generic" class PatchDataParams(BaseHttpParams): - """ - Defines the input parameters for the 'PATCH Request' action. - """ body: str = Param(description="PATCH body (query string, JSON, etc.)", required=False) class PatchDataOutput(BaseHttpOutput): - """ - Defines the structured output for the 'PATCH Request' action. - """ pass diff --git a/package/src/actions/action_post.py b/package/src/actions/action_post.py index 796b781..90c835e 100644 --- a/package/src/actions/action_post.py +++ b/package/src/actions/action_post.py @@ -2,23 +2,17 @@ from soar_sdk.params import Param from ..asset import Asset -from ..classes import BaseHttpOutput, BaseHttpParams from ..common import logger from ..request_maker import make_request +from ..schemas import BaseHttpOutput, BaseHttpParams class PostDataOutput(BaseHttpOutput): - """ - Defines the structured output for the 'POST Request' action. - """ pass class PostDataParams(BaseHttpParams): - """ - Defines the input parameters for the 'POST Request' action. - """ body: str = Param(description="POST body (query string, JSON, etc.)", required=False) diff --git a/package/src/actions/action_put.py b/package/src/actions/action_put.py index b6c0518..fe3dcbb 100644 --- a/package/src/actions/action_put.py +++ b/package/src/actions/action_put.py @@ -2,25 +2,19 @@ from soar_sdk.params import Param from ..asset import Asset -from ..classes import BaseHttpOutput, BaseHttpParams from ..common import logger from ..request_maker import make_request +from ..schemas import BaseHttpOutput, BaseHttpParams action_type = "generic" class PutDataOutput(BaseHttpOutput): - """ - Defines the structured output for the 'PUT Request' action. - """ pass class PutDataParams(BaseHttpParams): - """ - Defines the input parameters for the 'PUT Request' action. - """ body: str = Param(description="PATCH body (query string, JSON, etc.)", required=False) diff --git a/package/src/actions/get_file.py b/package/src/actions/get_file.py index b9b510a..357eae1 100644 --- a/package/src/actions/get_file.py +++ b/package/src/actions/get_file.py @@ -11,9 +11,10 @@ from ..auth import get_auth_method from ..common import logger +VERBOSE = "Provide the file path and file name to download into the vault. For example, /web_storage/file.tgz." + class GetFileParams(Params): - """Defines the input parameters for the 'Get File' action.""" hostname: str = Param( description="Hostname to execute command on", @@ -30,15 +31,11 @@ class GetFileParams(Params): class GetFileOutput(ActionOutput): - """Defines the structured output for the 'Get File' action.""" vault_id: str file_name: str -verbose = "Provide the file path and file name to download into the vault. For example, /web_storage/file.tgz." - - def get_file(params: GetFileParams, soar: SOARClient, asset: Asset) -> GetFileOutput: """Retrieve a file from the endpoint and save it to the vault.""" logger.info("In action handler for: get_file") diff --git a/package/src/actions/put_file.py b/package/src/actions/put_file.py index 4a9baac..8ac0111 100644 --- a/package/src/actions/put_file.py +++ b/package/src/actions/put_file.py @@ -11,9 +11,10 @@ from ..auth import get_auth_method from ..common import logger +VERBOSE = "Provide the path to store the file on the file server. For example, /web_storage/test_repo/." + class PutFileParams(Params): - """Defines the input parameters for the 'Put File' action.""" host: str = Param( description="Hostname/IP with port number to execute command on", @@ -31,14 +32,10 @@ class PutFileParams(Params): class PutFileOutput(ActionOutput): - """Defines the structured output for the 'Put File' action.""" file_sent: str -verbose = "Provide the path to store the file on the file server. For example, /web_storage/test_repo/." - - def put_file(params: PutFileParams, soar: SOARClient, asset: Asset) -> PutFileOutput: """Put a file from the vault to another location.""" logger.info("In action handler for: put_file") @@ -54,11 +51,13 @@ def put_file(params: PutFileParams, soar: SOARClient, asset: Asset) -> PutFileOu ) if file_name_to_send in params.file_destination: raise ActionFailure("The filename should be excluded from the 'location' (file destination) parameter.") + + base_url = params.host or asset.base_url + full_url = f"{base_url.rstrip('/')}/{params.file_destination.lstrip('/')}/{quote(file_name_to_send)}" + if not validators.url(full_url): + raise ActionFailure(f"Invalid URL constructed: {full_url}") + with vault_attachment.open() as f: - base_url = params.host or asset.base_url - full_url = f"{base_url.rstrip('/')}/{params.file_destination.lstrip('/')}/{quote(file_name_to_send)}" - if not validators.url(full_url): - raise ActionFailure(f"Invalid URL constructed: {full_url}") auth_strategy = get_auth_method(asset, soar) auth_object, final_headers = auth_strategy.create_auth({}) files_payload = {"file": f} diff --git a/package/src/app.py b/package/src/app.py index e68e5f2..1de6e24 100644 --- a/package/src/app.py +++ b/package/src/app.py @@ -3,9 +3,9 @@ from .actions import action_delete, action_get, action_head, action_options, action_patch, action_post, action_put, get_file, put_file from .asset import Asset -from .classes import EmptyOutput from .common import logger from .request_maker import make_request +from .schemas import EmptyOutput app = App( name="HTTP", @@ -47,8 +47,8 @@ def test_connectivity(soar: SOARClient, asset: Asset) -> None: app.register_action(action_delete.delete_data, action_type="generic", read_only=False) app.register_action(action_head.get_headers, action_type="investigate") app.register_action(action_options.get_options, action_type="investigate") -app.register_action(put_file.put_file, action_type="generic", read_only=False, verbose=put_file.verbose) -app.register_action(get_file.get_file, action_type="investigate", verbose=get_file.verbose) +app.register_action(put_file.put_file, action_type="generic", read_only=False, verbose=put_file.VERBOSE) +app.register_action(get_file.get_file, action_type="investigate", verbose=get_file.VERBOSE) if __name__ == "__main__": diff --git a/package/src/auth.py b/package/src/auth.py index 25e0ef7..a2b01cb 100644 --- a/package/src/auth.py +++ b/package/src/auth.py @@ -17,8 +17,10 @@ import requests from requests.auth import AuthBase, HTTPBasicAuth +from soar_sdk.abstract import SOARClient from soar_sdk.exceptions import ActionFailure +from .asset import Asset from .common import logger @@ -163,20 +165,13 @@ def create_auth(self, headers): return None, headers -def get_auth_method(asset, soar_client): +def get_auth_method(asset: Asset, soar_client: SOARClient): """ Factory function to select and instantiate the appropriate auth strategy. Based on the provided asset configuration, this function determines which authentication method to use (Basic, Token, OAuth, or None) and returns an instance of the corresponding strategy class. - - Args: - asset (Asset): The asset configuration object. - soar_client (SOARClient): The SOAR client, needed for stateful strategies like OAuth. - - Returns: - Authorization: An instance of a class that implements the Authorization strategy. """ if asset.username and asset.password: return BasicAuth(asset) diff --git a/package/src/helpers.py b/package/src/helpers.py index 4b97a91..8c0d064 100644 --- a/package/src/helpers.py +++ b/package/src/helpers.py @@ -6,8 +6,8 @@ from pydantic import ValidationError from soar_sdk.exceptions import ActionFailure -from .classes import ParsedResponseBody from .common import logger +from .schemas import ParsedResponseBody def process_xml_response(response) -> dict: diff --git a/package/src/classes.py b/package/src/schemas.py similarity index 100% rename from package/src/classes.py rename to package/src/schemas.py From ce509a5b7b546957ec9d4ba67ef3c207a2d226ca Mon Sep 17 00:00:00 2001 From: Tapish Jain Date: Wed, 17 Sep 2025 10:24:16 -0700 Subject: [PATCH 23/29] fix: pre commit file needs to be at top level --- package/.pre-commit-config.yaml => .pre-commit-config.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) rename package/.pre-commit-config.yaml => .pre-commit-config.yaml (87%) diff --git a/package/.pre-commit-config.yaml b/.pre-commit-config.yaml similarity index 87% rename from package/.pre-commit-config.yaml rename to .pre-commit-config.yaml index 22b03af..a565313 100644 --- a/package/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,8 +1,7 @@ repos: - repo: https://github.com/phantomcyber/dev-cicd-tools - rev: v1.26 + rev: v2.0.9 hooks: - - id: org-hook - id: package-app-dependencies - repo: https://github.com/Yelp/detect-secrets rev: v1.5.0 From d588e66bb3b6865edeae2740523adbc382ac09b6 Mon Sep 17 00:00:00 2001 From: Tapish Jain Date: Wed, 17 Sep 2025 13:42:01 -0700 Subject: [PATCH 24/29] fix: pre commit updates and test to get connector ci working --- .github/workflows/call-push.yml | 13 ++++ .pre-commit-config.yaml | 87 +++++++++++++++++++++++++-- package/src/__init__.py | 13 ++++ package/src/actions/action_delete.py | 23 +++++-- package/src/actions/action_head.py | 19 +++++- package/src/actions/action_options.py | 19 +++++- package/src/actions/action_patch.py | 23 +++++-- package/src/actions/action_post.py | 19 +++++- package/src/actions/action_put.py | 19 +++++- package/src/actions/get_file.py | 32 ++++++++-- package/src/actions/put_file.py | 31 ++++++++-- package/src/app.py | 33 +++++++++- package/src/asset.py | 36 +++++++++-- package/src/common.py | 13 ++++ package/src/helpers.py | 31 ++++++++-- package/src/request_maker.py | 30 ++++++++- package/src/schemas.py | 35 +++++++++-- pyproject.toml | 8 +++ tox.ini | 4 ++ 19 files changed, 430 insertions(+), 58 deletions(-) create mode 100644 .github/workflows/call-push.yml create mode 100644 pyproject.toml create mode 100644 tox.ini diff --git a/.github/workflows/call-push.yml b/.github/workflows/call-push.yml new file mode 100644 index 0000000..8b24fef --- /dev/null +++ b/.github/workflows/call-push.yml @@ -0,0 +1,13 @@ +name: Call Push Workflow + +on: + pull_request: + branches-ignore: + - main # Runs only on non-main branches + types: + - opened # When a new PR is opened + - synchronize # When a new commit is pushed to the PR branch + +jobs: + call-push: + uses: splunk-soar-connectors/.github/.github/workflows/push.yml@tapishj/PAPP-36785-update-ci-for-sdkfied-apps diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a565313..fe86072 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,10 +1,87 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks + +# By default, run each hook only in the standard pre-commit stage +default_install_hook_types: [pre-commit, commit-msg] +default_stages: [pre-commit] + +# This is a template for connector pre-commit hooks repos: -- repo: https://github.com/phantomcyber/dev-cicd-tools - rev: v2.0.9 + - repo: https://github.com/compilerla/conventional-pre-commit + rev: v4.1.0 + hooks: + - id: conventional-pre-commit + stages: [commit-msg] + args: [--verbose] + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-merge-conflict + - id: end-of-file-fixer + exclude: ^NOTICE$ + exclude_types: [ markdown ] + - id: trailing-whitespace + exclude: ^NOTICE$ + exclude_types: [ markdown ] + - id: requirements-txt-fixer + - id: check-json + - id: check-yaml + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.11.7 + hooks: + - id: ruff + args: [ "--fix", "--unsafe-fixes"] # Allow unsafe fixes (ruff pretty strict about what it can fix) + - id: ruff-format + - repo: https://github.com/djlint/djLint + rev: v1.36.4 + hooks: + - id: djlint-reformat-django + - id: djlint-django + - repo: https://github.com/phantomcyber/soar-app-linter + rev: 0.1.0 hooks: - - id: package-app-dependencies -- repo: https://github.com/Yelp/detect-secrets + - id: soar-app-linter + args: ["--single-repo", "--message-level", "error"] + - repo: https://github.com/hukkin/mdformat + rev: 0.7.22 + hooks: + - id: mdformat + exclude: "release_notes/.*" + - repo: https://github.com/returntocorp/semgrep + rev: v1.89.0 + hooks: + - id: semgrep + - repo: https://github.com/Yelp/detect-secrets rev: v1.5.0 hooks: - - id: detect-secrets + - id: detect-secrets args: ['--no-verify'] + exclude_types: [json] + exclude: "README.md" + # Central hooks + - repo: https://github.com/phantomcyber/dev-cicd-tools + rev: v2.0.9 + hooks: + - id: build-docs + language: python + additional_dependencies: ["local-hooks"] + args: ['.'] + - id: copyright + language: python + additional_dependencies: ["local-hooks"] + args: ['.'] + - id: package-app-dependencies + language: python + additional_dependencies: ["local-hooks"] + - id: notice-file + language: python + additional_dependencies: ["local-hooks"] + args: ['.'] + - id: release-notes + language: python + additional_dependencies: ["local-hooks"] + args: ['.'] + - id: static-tests + language: python + additional_dependencies: ["local-hooks"] + args: ['.'] diff --git a/package/src/__init__.py b/package/src/__init__.py index 54b6cbf..1010bb8 100644 --- a/package/src/__init__.py +++ b/package/src/__init__.py @@ -1,3 +1,16 @@ +# Copyright (c) 2025 Splunk Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. from . import app __ALL__ = [app] diff --git a/package/src/actions/action_delete.py b/package/src/actions/action_delete.py index 96b292f..0d0e1f2 100644 --- a/package/src/actions/action_delete.py +++ b/package/src/actions/action_delete.py @@ -1,3 +1,16 @@ +# Copyright (c) 2025 Splunk Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. from soar_sdk.abstract import SOARClient from soar_sdk.params import Param @@ -8,16 +21,18 @@ class DeleteDataOutput(BaseHttpOutput): - pass class DeleteDataParams(BaseHttpParams): - - body: str = Param(description="DELETE body (query string, JSON, etc.)", required=False) + body: str = Param( + description="DELETE body (query string, JSON, etc.)", required=False + ) -def delete_data(params: DeleteDataParams, soar: SOARClient, asset: Asset) -> DeleteDataOutput: +def delete_data( + params: DeleteDataParams, soar: SOARClient, asset: Asset +) -> DeleteDataOutput: """Perform a REST DELETE call to the server.""" logger.info("In action handler for: delete_data") return make_request( diff --git a/package/src/actions/action_head.py b/package/src/actions/action_head.py index d242671..d482413 100644 --- a/package/src/actions/action_head.py +++ b/package/src/actions/action_head.py @@ -1,3 +1,16 @@ +# Copyright (c) 2025 Splunk Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. from soar_sdk.abstract import SOARClient from ..asset import Asset @@ -7,16 +20,16 @@ class GetHeadersOutput(BaseHttpOutput): - pass class GetHeadersParams(BaseHttpParams): - pass -def get_headers(params: GetHeadersParams, soar: SOARClient, asset: Asset) -> GetHeadersOutput: +def get_headers( + params: GetHeadersParams, soar: SOARClient, asset: Asset +) -> GetHeadersOutput: """Perform a REST HEAD call to the server.""" logger.info("In action handler for: head_data") return make_request( diff --git a/package/src/actions/action_options.py b/package/src/actions/action_options.py index 8e82f1f..872320d 100644 --- a/package/src/actions/action_options.py +++ b/package/src/actions/action_options.py @@ -1,3 +1,16 @@ +# Copyright (c) 2025 Splunk Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. from soar_sdk.abstract import SOARClient from ..asset import Asset @@ -7,16 +20,16 @@ class GetOptionsParams(BaseHttpParams): - pass class GetOptionsOutput(BaseHttpOutput): - pass -def get_options(params: GetOptionsParams, soar: SOARClient, asset: Asset) -> GetOptionsOutput: +def get_options( + params: GetOptionsParams, soar: SOARClient, asset: Asset +) -> GetOptionsOutput: """Perform a REST OPTIONS call to the server.""" logger.info("In action handler for: get_options") return make_request( diff --git a/package/src/actions/action_patch.py b/package/src/actions/action_patch.py index 3acc383..1dd79ee 100644 --- a/package/src/actions/action_patch.py +++ b/package/src/actions/action_patch.py @@ -1,3 +1,16 @@ +# Copyright (c) 2025 Splunk Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. from soar_sdk.abstract import SOARClient from soar_sdk.params import Param @@ -10,16 +23,18 @@ class PatchDataParams(BaseHttpParams): - - body: str = Param(description="PATCH body (query string, JSON, etc.)", required=False) + body: str = Param( + description="PATCH body (query string, JSON, etc.)", required=False + ) class PatchDataOutput(BaseHttpOutput): - pass -def patch_data(params: PatchDataParams, soar: SOARClient, asset: Asset) -> PatchDataOutput: +def patch_data( + params: PatchDataParams, soar: SOARClient, asset: Asset +) -> PatchDataOutput: """Perform a REST PATCH call to the server.""" logger.info("In action handler for: patch_data") return make_request( diff --git a/package/src/actions/action_post.py b/package/src/actions/action_post.py index 90c835e..7e2f8fa 100644 --- a/package/src/actions/action_post.py +++ b/package/src/actions/action_post.py @@ -1,3 +1,16 @@ +# Copyright (c) 2025 Splunk Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. from soar_sdk.abstract import SOARClient from soar_sdk.params import Param @@ -8,13 +21,13 @@ class PostDataOutput(BaseHttpOutput): - pass class PostDataParams(BaseHttpParams): - - body: str = Param(description="POST body (query string, JSON, etc.)", required=False) + body: str = Param( + description="POST body (query string, JSON, etc.)", required=False + ) def post_data(params: PostDataParams, soar: SOARClient, asset: Asset) -> PostDataOutput: diff --git a/package/src/actions/action_put.py b/package/src/actions/action_put.py index fe3dcbb..1cbacad 100644 --- a/package/src/actions/action_put.py +++ b/package/src/actions/action_put.py @@ -1,3 +1,16 @@ +# Copyright (c) 2025 Splunk Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. from soar_sdk.abstract import SOARClient from soar_sdk.params import Param @@ -10,13 +23,13 @@ class PutDataOutput(BaseHttpOutput): - pass class PutDataParams(BaseHttpParams): - - body: str = Param(description="PATCH body (query string, JSON, etc.)", required=False) + body: str = Param( + description="PATCH body (query string, JSON, etc.)", required=False + ) def put_data(params: PutDataParams, soar: SOARClient, asset: Asset) -> PutDataOutput: diff --git a/package/src/actions/get_file.py b/package/src/actions/get_file.py index 357eae1..252a08d 100644 --- a/package/src/actions/get_file.py +++ b/package/src/actions/get_file.py @@ -1,3 +1,16 @@ +# Copyright (c) 2025 Splunk Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. from urllib.parse import quote, unquote_plus import requests @@ -15,7 +28,6 @@ class GetFileParams(Params): - hostname: str = Param( description="Hostname to execute command on", primary=True, @@ -27,11 +39,12 @@ class GetFileParams(Params): primary=True, cef_types=["file path"], ) - verify_certificate: bool = Param(description="Verify certificates (if using HTTPS)", default=False) + verify_certificate: bool = Param( + description="Verify certificates (if using HTTPS)", default=False + ) class GetFileOutput(ActionOutput): - vault_id: str file_name: str @@ -46,7 +59,9 @@ def get_file(params: GetFileParams, soar: SOARClient, asset: Asset) -> GetFileOu validate_url = f"{hostname}/{encoded_file_path}" if not validators.url(validate_url): - raise ActionFailure(f"Invalid URL constructed based on hostname and file_path: {validate_url}") + raise ActionFailure( + f"Invalid URL constructed based on hostname and file_path: {validate_url}" + ) full_url = f"{hostname}/{file_path}" file_name = unquote_plus(file_path.split("/")[-1]) @@ -75,13 +90,18 @@ def get_file(params: GetFileParams, soar: SOARClient, asset: Asset) -> GetFileOu if not ( new_vault_id := soar.vault.create_attachment( - file_content=file_content, file_name=file_name, container_id=container_id_to_use, metadata=None + file_content=file_content, + file_name=file_name, + container_id=container_id_to_use, + metadata=None, ) ): raise ActionFailure("Failed to add file to vault.") logger.info(f"Saving file to vault. name: {file_name}") except Exception as e: - raise ActionFailure(f"An error occurred while saving the file to the vault. Details: {e}") + raise ActionFailure( + f"An error occurred while saving the file to the vault. Details: {e}" + ) logger.info("File successfully retrieved and added to vault") return GetFileOutput(vault_id=new_vault_id, file_name=file_name) diff --git a/package/src/actions/put_file.py b/package/src/actions/put_file.py index 8ac0111..aacffaa 100644 --- a/package/src/actions/put_file.py +++ b/package/src/actions/put_file.py @@ -1,3 +1,16 @@ +# Copyright (c) 2025 Splunk Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. from urllib.parse import quote import requests @@ -15,24 +28,26 @@ class PutFileParams(Params): - host: str = Param( description="Hostname/IP with port number to execute command on", primary=True, cef_types=["host name"], ) - vault_id: str = Param(description="Vault ID of file", primary=True, cef_types=["vault id"]) + vault_id: str = Param( + description="Vault ID of file", primary=True, cef_types=["vault id"] + ) file_destination: str = Param( description="File destination path (exclude filename)", primary=True, cef_types=["file path"], ) file_name: str = Param(description="Name of the file to be put on endpoint") - verify_certificate: bool = Param(description="Verify certificates (if using HTTPS)", default=False) + verify_certificate: bool = Param( + description="Verify certificates (if using HTTPS)", default=False + ) class PutFileOutput(ActionOutput): - file_sent: str @@ -42,7 +57,9 @@ def put_file(params: PutFileParams, soar: SOARClient, asset: Asset) -> PutFileOu try: logger.info(f"Fetching phantom vault details for vault_id: {params.vault_id}") if not (attachments := soar.vault.get_attachment(vault_id=params.vault_id)): - raise ActionFailure(f"File with vault_id '{params.vault_id}' not found in vault.") + raise ActionFailure( + f"File with vault_id '{params.vault_id}' not found in vault." + ) vault_attachment = attachments[0] file_name_to_send = params.file_name or vault_attachment.name if params.file_name and vault_attachment.name != params.file_name: @@ -50,7 +67,9 @@ def put_file(params: PutFileParams, soar: SOARClient, asset: Asset) -> PutFileOu f"Provided file_name '{params.file_name}' does not match the name in vault '{vault_attachment.name}'. Using provided name." ) if file_name_to_send in params.file_destination: - raise ActionFailure("The filename should be excluded from the 'location' (file destination) parameter.") + raise ActionFailure( + "The filename should be excluded from the 'location' (file destination) parameter." + ) base_url = params.host or asset.base_url full_url = f"{base_url.rstrip('/')}/{params.file_destination.lstrip('/')}/{quote(file_name_to_send)}" diff --git a/package/src/app.py b/package/src/app.py index 1de6e24..ef19de2 100644 --- a/package/src/app.py +++ b/package/src/app.py @@ -1,7 +1,30 @@ +# Copyright (c) 2025 Splunk Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. from soar_sdk.abstract import SOARClient from soar_sdk.app import App -from .actions import action_delete, action_get, action_head, action_options, action_patch, action_post, action_put, get_file, put_file +from .actions import ( + action_delete, + action_get, + action_head, + action_options, + action_patch, + action_post, + action_put, + get_file, + put_file, +) from .asset import Asset from .common import logger from .request_maker import make_request @@ -47,8 +70,12 @@ def test_connectivity(soar: SOARClient, asset: Asset) -> None: app.register_action(action_delete.delete_data, action_type="generic", read_only=False) app.register_action(action_head.get_headers, action_type="investigate") app.register_action(action_options.get_options, action_type="investigate") -app.register_action(put_file.put_file, action_type="generic", read_only=False, verbose=put_file.VERBOSE) -app.register_action(get_file.get_file, action_type="investigate", verbose=get_file.VERBOSE) +app.register_action( + put_file.put_file, action_type="generic", read_only=False, verbose=put_file.VERBOSE +) +app.register_action( + get_file.get_file, action_type="investigate", verbose=get_file.VERBOSE +) if __name__ == "__main__": diff --git a/package/src/asset.py b/package/src/asset.py index 22fe0ea..baf9fae 100644 --- a/package/src/asset.py +++ b/package/src/asset.py @@ -1,5 +1,19 @@ +# Copyright (c) 2025 Splunk Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. from soar_sdk.asset import AssetField, BaseAsset + class Asset(BaseAsset): base_url: str = AssetField( required=True, @@ -14,12 +28,22 @@ class Asset(BaseAsset): description="Type of authentication token", default="ph-auth-token", ) - auth_token: str = AssetField(required=False, description="Value of authentication token") - username: str = AssetField(required=False, description="Username (for HTTP basic auth)") - password: str = AssetField(required=False, description="Password (for HTTP basic auth)") - oauth_token_url: str = AssetField(required=False, description="URL to fetch oauth token from") + auth_token: str = AssetField( + required=False, description="Value of authentication token" + ) + username: str = AssetField( + required=False, description="Username (for HTTP basic auth)" + ) + password: str = AssetField( + required=False, description="Password (for HTTP basic auth)" + ) + oauth_token_url: str = AssetField( + required=False, description="URL to fetch oauth token from" + ) client_id: str = AssetField(required=False, description="Client ID (for OAuth)") - client_secret: str = AssetField(required=False, description="Client Secret (for OAuth)") + client_secret: str = AssetField( + required=False, description="Client Secret (for OAuth)" + ) timeout: float = AssetField(required=False, description="Timeout for HTTP calls") test_http_method: str = AssetField( required=False, @@ -35,4 +59,4 @@ class Asset(BaseAsset): "TRACE", "PATCH", ], - ) \ No newline at end of file + ) diff --git a/package/src/common.py b/package/src/common.py index fd13347..9e007a5 100644 --- a/package/src/common.py +++ b/package/src/common.py @@ -1,3 +1,16 @@ +# Copyright (c) 2025 Splunk Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. from soar_sdk.logging import getLogger logger = getLogger() diff --git a/package/src/helpers.py b/package/src/helpers.py index 8c0d064..64b26a8 100644 --- a/package/src/helpers.py +++ b/package/src/helpers.py @@ -1,3 +1,16 @@ +# Copyright (c) 2025 Splunk Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import json from typing import Optional @@ -23,7 +36,9 @@ def process_json_response(response) -> dict: except json.JSONDecodeError as e: raise ActionFailure(f"Server claimed JSON but failed to parse. Error: {e}") except ValidationError as e: - raise ActionFailure(f"Response JSON did not match expected structure. Details: {e}") + raise ActionFailure( + f"Response JSON did not match expected structure. Details: {e}" + ) def process_html_response(response) -> ParsedResponseBody: @@ -41,7 +56,11 @@ def process_html_response(response) -> ParsedResponseBody: def process_empty_response(content_type) -> dict: - message = "Response includes a file" if "octet-stream" in content_type else "Empty response body" + message = ( + "Response includes a file" + if "octet-stream" in content_type + else "Empty response body" + ) return {"message": message} @@ -71,12 +90,16 @@ def parse_headers(headers_str: Optional[str]) -> dict: parsed_headers = json.loads(headers_str) except json.JSONDecodeError as e: - error_message = f"Failed to parse headers. Ensure it's a valid JSON object. Error: {e}" + error_message = ( + f"Failed to parse headers. Ensure it's a valid JSON object. Error: {e}" + ) logger.error(error_message) raise ActionFailure(error_message) if not isinstance(parsed_headers, dict): - raise ActionFailure("Headers parameter must be a valid JSON object (dictionary).") + raise ActionFailure( + "Headers parameter must be a valid JSON object (dictionary)." + ) return parsed_headers diff --git a/package/src/request_maker.py b/package/src/request_maker.py index 9293e79..4dcfa28 100644 --- a/package/src/request_maker.py +++ b/package/src/request_maker.py @@ -1,3 +1,16 @@ +# Copyright (c) 2025 Splunk Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. from typing import Optional import requests @@ -36,7 +49,11 @@ def make_request( logger.info(f"Making {method} request to: {full_url}") - body = UnicodeDammit(body).unicode_markup.encode("utf-8") if isinstance(body, str) else body + body = ( + UnicodeDammit(body).unicode_markup.encode("utf-8") + if isinstance(body, str) + else body + ) retries = 1 response = None @@ -60,8 +77,15 @@ def make_request( break except requests.exceptions.RequestException as e: - if isinstance(auth_method, OAuth) and e.response and e.response.status_code == 401 and retries > 0: - logger.warning("Request failed with 401, token might be expired. Forcing a refresh.") + if ( + isinstance(auth_method, OAuth) + and e.response + and e.response.status_code == 401 + and retries > 0 + ): + logger.warning( + "Request failed with 401, token might be expired. Forcing a refresh." + ) auth_method.get_token(force_new=True) retries -= 1 continue diff --git a/package/src/schemas.py b/package/src/schemas.py index 14fc6c4..17c0edf 100644 --- a/package/src/schemas.py +++ b/package/src/schemas.py @@ -1,3 +1,16 @@ +# Copyright (c) 2025 Splunk Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. from pydantic import Extra from soar_sdk.action_results import ActionOutput, GenericActionOutput, OutputField from soar_sdk.params import Param, Params @@ -19,10 +32,16 @@ class Config: class BaseHttpOutput(GenericActionOutput): """Base class for all standard action outputs.""" - location: str = OutputField(cef_types=["url"], example_values=["http://192.168.1.26/rest/assets"]) + location: str = OutputField( + cef_types=["url"], example_values=["http://192.168.1.26/rest/assets"] + ) method: str = OutputField(example_values=["POST"]) - parsed_response_body: ParsedResponseBody = OutputField(example_values=['{"failed": true, "message": "Requested item not found"}']) - response_body: str = OutputField(example_values=['{"failed": true, "message": "Requested item not found"}']) + parsed_response_body: ParsedResponseBody = OutputField( + example_values=['{"failed": true, "message": "Requested item not found"}'] + ) + response_body: str = OutputField( + example_values=['{"failed": true, "message": "Requested item not found"}'] + ) response_headers: str def generate_action_summary_message(self) -> str: @@ -37,5 +56,11 @@ class BaseHttpParams(Params): primary=True, cef_types=["endpoint"], ) - verify_certificate: bool = Param(description="Verify certificates (if using HTTPS)", default=False, required=False) - headers: str = Param(description="Additional headers (JSON object with headers)", required=False) + verify_certificate: bool = Param( + description="Verify certificates (if using HTTPS)", + default=False, + required=False, + ) + headers: str = Param( + description="Additional headers (JSON object with headers)", required=False + ) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4c594fc --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,8 @@ +[tool.black] +line-length = 145 +target-version = ['py39'] +verbose = true + +[tool.isort] +line_length = 145 +profile = "black" diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..720a141 --- /dev/null +++ b/tox.ini @@ -0,0 +1,4 @@ +[flake8] +max-line-length = 145 +max-complexity = 28 +extend-ignore = F403,E128,E126,E121,E127,E731,E201,E202,E203,E701,F405,E722,D,W503 From f1226d6529c144731f302f2fc265d9faadb9071d Mon Sep 17 00:00:00 2001 From: Tapish Jain Date: Wed, 17 Sep 2025 13:53:31 -0700 Subject: [PATCH 25/29] fix: trigger workflow --- package/pyproject.toml | 4 ++-- package/src/app.py | 4 ++-- package/uv.lock | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package/pyproject.toml b/package/pyproject.toml index 83e1646..9952ff2 100644 --- a/package/pyproject.toml +++ b/package/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "HTTP" +name = "http" version = "4.0.0" description = "This App facilitates making HTTP requests as actions" license = "Copyright (c) 2025 Splunk Inc." @@ -7,7 +7,7 @@ requires-python = ">=3.9, <3.14" authors = [ ] dependencies = [ - "splunk-soar-sdk>=1.0.0b55", + "splunk-soar-sdk>=1.5.2", "validators", "xmltodict", ] diff --git a/package/src/app.py b/package/src/app.py index ef19de2..2c7cb33 100644 --- a/package/src/app.py +++ b/package/src/app.py @@ -37,8 +37,8 @@ logo_dark="logo_dark.svg", product_vendor="Generic", product_name="HTTP", - publisher="Splunk Inc.", - appid="dc312038-005f-470f-badb-8a353ba9bb5b", + publisher="Splunk", + appid="290b7499-0374-4930-9cdc-5e9b05d65827", fips_compliant=False, asset_cls=Asset, ) diff --git a/package/uv.lock b/package/uv.lock index 0e99677..81066fc 100644 --- a/package/uv.lock +++ b/package/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.9, <3.14" resolution-markers = [ "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'", @@ -292,7 +292,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "splunk-soar-sdk", specifier = ">=1.0.0b55" }, + { name = "splunk-soar-sdk", specifier = ">=1.5.2" }, { name = "validators" }, { name = "xmltodict" }, ] @@ -806,7 +806,7 @@ wheels = [ [[package]] name = "splunk-soar-sdk" -version = "1.3.3" +version = "1.5.2" source = { registry = "https://pypi.python.org/simple" } dependencies = [ { name = "beautifulsoup4", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, @@ -826,9 +826,9 @@ dependencies = [ { name = "tqdm", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "typer", marker = "(python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/8f/a01e44b57f871d4830a2bc53e4d8c825e0ba72435ccbcd5e8bb668013317/splunk_soar_sdk-1.3.3.tar.gz", hash = "sha256:57f8e28c725b737eb91c574480f59207e4020b37273051991dea582da5c3076a", size = 508969, upload-time = "2025-09-08T03:48:32.871Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/49/0d8286783d4d75a3a2c593999492155157e6b843678fee4c67b66b9505ac/splunk_soar_sdk-1.5.2.tar.gz", hash = "sha256:679214a27ed708acae727d540881c7fd70bb7a56058cea6bcb828190f914ffda", size = 518877, upload-time = "2025-09-17T21:51:24.083Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/d2/85ad8db77caae830e7fb5feec75a809de97fbd1f5eb28cdbad14f4da42d4/splunk_soar_sdk-1.3.3-py3-none-any.whl", hash = "sha256:4654db5e3b3c9dad4f1b7060921d605687bdc1c195b75d3af7ecdab007f20caf", size = 154969, upload-time = "2025-09-08T03:48:31.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/c7/c0d017dc7364a63573911c1e1594e05e212d54b0a1a92fe1b916069e2b42/splunk_soar_sdk-1.5.2-py3-none-any.whl", hash = "sha256:cc6e69d43e8a553d93f648cf5ffadf79c3d10c6374ccaae918292a0637de764b", size = 157891, upload-time = "2025-09-17T21:51:22.797Z" }, ] [[package]] From 41f46f0f0b13fd3641219f599f3afb7fd8422197 Mon Sep 17 00:00:00 2001 From: msankows Date: Tue, 23 Sep 2025 11:34:50 +0200 Subject: [PATCH 26/29] add input fields for public certificate and private key --- package/src/asset.py | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/package/src/asset.py b/package/src/asset.py index baf9fae..6fb6512 100644 --- a/package/src/asset.py +++ b/package/src/asset.py @@ -28,22 +28,12 @@ class Asset(BaseAsset): description="Type of authentication token", default="ph-auth-token", ) - auth_token: str = AssetField( - required=False, description="Value of authentication token" - ) - username: str = AssetField( - required=False, description="Username (for HTTP basic auth)" - ) - password: str = AssetField( - required=False, description="Password (for HTTP basic auth)" - ) - oauth_token_url: str = AssetField( - required=False, description="URL to fetch oauth token from" - ) + auth_token: str = AssetField(required=False, description="Value of authentication token") + username: str = AssetField(required=False, description="Username (for HTTP basic auth)") + password: str = AssetField(required=False, description="Password (for HTTP basic auth)") + oauth_token_url: str = AssetField(required=False, description="URL to fetch oauth token from") client_id: str = AssetField(required=False, description="Client ID (for OAuth)") - client_secret: str = AssetField( - required=False, description="Client Secret (for OAuth)" - ) + client_secret: str = AssetField(required=False, description="Client Secret (for OAuth)") timeout: float = AssetField(required=False, description="Timeout for HTTP calls") test_http_method: str = AssetField( required=False, @@ -60,3 +50,5 @@ class Asset(BaseAsset): "PATCH", ], ) + public_cert: str = AssetField(required=False, sensitive=True, description="Public part of the client certificate") + private_key: str = AssetField(required=False, sensitive=True, description="Private key for the client certificate") From 258ac45800f0a2c71f7d5296947873be0ca0506b Mon Sep 17 00:00:00 2001 From: msankows Date: Tue, 23 Sep 2025 12:55:18 +0200 Subject: [PATCH 27/29] temporary file creator --- package/src/helpers.py | 55 ++++++++++++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/package/src/helpers.py b/package/src/helpers.py index 64b26a8..1c957fe 100644 --- a/package/src/helpers.py +++ b/package/src/helpers.py @@ -13,14 +13,17 @@ # limitations under the License. import json from typing import Optional - +import tempfile +from contextlib import contextmanager, nullcontext import xmltodict from bs4 import BeautifulSoup from pydantic import ValidationError from soar_sdk.exceptions import ActionFailure - +import base64 from .common import logger from .schemas import ParsedResponseBody +from .asset import Asset +import os def process_xml_response(response) -> dict: @@ -36,9 +39,7 @@ def process_json_response(response) -> dict: except json.JSONDecodeError as e: raise ActionFailure(f"Server claimed JSON but failed to parse. Error: {e}") except ValidationError as e: - raise ActionFailure( - f"Response JSON did not match expected structure. Details: {e}" - ) + raise ActionFailure(f"Response JSON did not match expected structure. Details: {e}") def process_html_response(response) -> ParsedResponseBody: @@ -56,11 +57,7 @@ def process_html_response(response) -> ParsedResponseBody: def process_empty_response(content_type) -> dict: - message = ( - "Response includes a file" - if "octet-stream" in content_type - else "Empty response body" - ) + message = "Response includes a file" if "octet-stream" in content_type else "Empty response body" return {"message": message} @@ -90,16 +87,12 @@ def parse_headers(headers_str: Optional[str]) -> dict: parsed_headers = json.loads(headers_str) except json.JSONDecodeError as e: - error_message = ( - f"Failed to parse headers. Ensure it's a valid JSON object. Error: {e}" - ) + error_message = f"Failed to parse headers. Ensure it's a valid JSON object. Error: {e}" logger.error(error_message) raise ActionFailure(error_message) if not isinstance(parsed_headers, dict): - raise ActionFailure( - "Headers parameter must be a valid JSON object (dictionary)." - ) + raise ActionFailure("Headers parameter must be a valid JSON object (dictionary).") return parsed_headers @@ -126,3 +119,33 @@ def handle_various_response(response): else: raw_body = response.text return parsed_body, raw_body + + +@contextmanager +def _temp_cert_files(asset: Asset): + """ + Safely creates temporary files for a client certificate and key from Base64 strings + and yields their paths. Cleans up the files automatically. + """ + if not (asset.public_cert and asset.private_key): + yield None + return + + cert_path, key_path = None, None + + try: + cert_bytes = base64.b64decode(asset.public_cert) + key_bytes = base64.b64decode(asset.private_key) + + with tempfile.NamedTemporaryFile(mode="wb", delete=False) as cert_f, tempfile.NamedTemporaryFile(mode="wb", delete=False) as key_f: + cert_f.write(cert_bytes) + key_f.write(key_bytes) + cert_path = cert_f.name + key_path = key_f.name + + yield (cert_path, key_path) + finally: + if cert_path and os.path.exists(cert_path): + os.remove(cert_path) + if key_path and os.path.exists(key_path): + os.remove(key_path) From 66821acf2f7209fc3c8150d19c26cb4d7f4ef768 Mon Sep 17 00:00:00 2001 From: msankows Date: Tue, 23 Sep 2025 13:58:10 +0200 Subject: [PATCH 28/29] request_maker updated - enable certificate auth in requests --- package/src/helpers.py | 4 +- package/src/request_maker.py | 76 +++++++++++++++++------------------- 2 files changed, 37 insertions(+), 43 deletions(-) diff --git a/package/src/helpers.py b/package/src/helpers.py index 1c957fe..c906852 100644 --- a/package/src/helpers.py +++ b/package/src/helpers.py @@ -122,13 +122,13 @@ def handle_various_response(response): @contextmanager -def _temp_cert_files(asset: Asset): +def temp_cert_files(asset: Asset): """ Safely creates temporary files for a client certificate and key from Base64 strings and yields their paths. Cleans up the files automatically. """ if not (asset.public_cert and asset.private_key): - yield None + yield (None, None) return cert_path, key_path = None, None diff --git a/package/src/request_maker.py b/package/src/request_maker.py index 4dcfa28..6168538 100644 --- a/package/src/request_maker.py +++ b/package/src/request_maker.py @@ -23,6 +23,8 @@ from .asset import Asset from .auth import OAuth, get_auth_method from .common import logger +from contextlib import nullcontext +from .helpers import temp_cert_files def make_request( @@ -49,48 +51,40 @@ def make_request( logger.info(f"Making {method} request to: {full_url}") - body = ( - UnicodeDammit(body).unicode_markup.encode("utf-8") - if isinstance(body, str) - else body - ) - - retries = 1 - response = None - - while retries >= 0: - auth_method = get_auth_method(asset, soar) - auth_object, final_headers = auth_method.create_auth(parsed_headers) - - try: - response = requests.request( - method=method, - url=full_url, - auth=auth_object, - data=body, - verify=verify, - headers=final_headers, - timeout=asset.timeout, - ) - response.raise_for_status() - - break - - except requests.exceptions.RequestException as e: - if ( - isinstance(auth_method, OAuth) - and e.response - and e.response.status_code == 401 - and retries > 0 - ): - logger.warning( - "Request failed with 401, token might be expired. Forcing a refresh." + cert_manager = temp_cert_files(asset) if (asset.client_cert and asset.client_key) else nullcontext() + + body = UnicodeDammit(body).unicode_markup.encode("utf-8") if isinstance(body, str) else body + with cert_manager as cert_param: + retries = 1 + response = None + + while retries >= 0: + auth_method = get_auth_method(asset, soar) + auth_object, final_headers = auth_method.create_auth(parsed_headers) + + try: + response = requests.request( + method=method, + url=full_url, + auth=auth_object, + data=body, + verify=verify, + headers=final_headers, + cert=cert_param, + timeout=asset.timeout, ) - auth_method.get_token(force_new=True) - retries -= 1 - continue - else: - raise ActionFailure(f"Request failed for {full_url}. Details: {e}") + response.raise_for_status() + + break + + except requests.exceptions.RequestException as e: + if isinstance(auth_method, OAuth) and e.response and e.response.status_code == 401 and retries > 0: + logger.warning("Request failed with 401, token might be expired. Forcing a refresh.") + auth_method.get_token(force_new=True) + retries -= 1 + continue + else: + raise ActionFailure(f"Request failed for {full_url}. Details: {e}") parsed_body, raw_body = helpers.handle_various_response(response) logger.info(f"Successfully processed data. Status: {response.status_code}") From 6c58252adaa8d984e1b2a2c10e73969e6e14c384 Mon Sep 17 00:00:00 2001 From: msankows Date: Tue, 23 Sep 2025 14:32:22 +0200 Subject: [PATCH 29/29] refactoring --- package/src/helpers.py | 8 ++++---- package/src/request_maker.py | 6 ++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/package/src/helpers.py b/package/src/helpers.py index c906852..1556d83 100644 --- a/package/src/helpers.py +++ b/package/src/helpers.py @@ -122,20 +122,20 @@ def handle_various_response(response): @contextmanager -def temp_cert_files(asset: Asset): +def temp_cert_files(cert_b64: str, key_b64: str): """ Safely creates temporary files for a client certificate and key from Base64 strings and yields their paths. Cleans up the files automatically. """ - if not (asset.public_cert and asset.private_key): + if not (cert_b64 and key_b64): yield (None, None) return cert_path, key_path = None, None + cert_bytes = base64.b64decode(asset.public_cert) + key_bytes = base64.b64decode(asset.private_key) try: - cert_bytes = base64.b64decode(asset.public_cert) - key_bytes = base64.b64decode(asset.private_key) with tempfile.NamedTemporaryFile(mode="wb", delete=False) as cert_f, tempfile.NamedTemporaryFile(mode="wb", delete=False) as key_f: cert_f.write(cert_bytes) diff --git a/package/src/request_maker.py b/package/src/request_maker.py index 6168538..4811fa6 100644 --- a/package/src/request_maker.py +++ b/package/src/request_maker.py @@ -23,7 +23,6 @@ from .asset import Asset from .auth import OAuth, get_auth_method from .common import logger -from contextlib import nullcontext from .helpers import temp_cert_files @@ -51,10 +50,9 @@ def make_request( logger.info(f"Making {method} request to: {full_url}") - cert_manager = temp_cert_files(asset) if (asset.client_cert and asset.client_key) else nullcontext() - body = UnicodeDammit(body).unicode_markup.encode("utf-8") if isinstance(body, str) else body - with cert_manager as cert_param: + + with temp_cert_files(asset.public_cert, asset.private_key) as cert_param: retries = 1 response = None